import 'dart:async'; import 'dart:ui' as ui; import 'package:clock/clock.dart'; import 'package:drift/drift.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:restart_app/restart_app.dart'; import 'package:twonly/locator.dart'; import 'package:twonly/src/constants/routes.keys.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; import 'package:twonly/src/services/user.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/visual/components/alert.dialog.dart'; import 'package:twonly/src/visual/components/snackbar.dart'; import 'package:twonly/src/visual/views/onboarding/setup.view.dart'; import 'package:twonly/src/visual/views/settings/developer/user_discovery_developer.view.dart'; class DeveloperSettingsView extends StatefulWidget { const DeveloperSettingsView({super.key}); @override State createState() => _DeveloperSettingsViewState(); } class _DeveloperSettingsViewState extends State { bool _isGeneratingMockImages = false; @override void initState() { super.initState(); } Future _generate1000MockImages() async { if (_isGeneratingMockImages) return; setState(() { _isGeneratingMockImages = true; }); try { final now = clock.now(); const groupId = 'mock_group_gallery'; // Ensure mock group exists await twonlyDB.groupsDao.createNewGroup( const GroupsCompanion( groupId: Value(groupId), groupName: Value('Mock Gallery Group'), isDirectChat: Value(false), joinedGroup: Value(true), ), ); const size = Size(360, 640); // Batch database entries using cascades for extreme operational speed and clean linting await twonlyDB.batch((batch) async { for (var i = 0; i < 1000; i++) { final mediaId = 'mock_gen_$i'; final authorIndex = i % 12; final contactId = 9000000 + authorIndex; late DateTime itemDate; if (i < 200) { // Spread over the last month itemDate = now.subtract(Duration(minutes: i * 216)); } else if (i < 400) { // Spread between 1 month and 1 year ago final localI = i - 200; itemDate = now.subtract( Duration(days: 30, minutes: localI * 2412), ); } else if (i < 600) { // Around a year ago final localI = i - 400; itemDate = now.subtract( Duration(days: 365, minutes: localI * 216), ); } else if (i < 800) { // Around three years ago final localI = i - 600; itemDate = now.subtract( Duration(days: 1095, minutes: localI * 216), ); } else { // Around four years ago final localI = i - 800; itemDate = now.subtract( Duration(days: 1460, minutes: localI * 216), ); } batch ..insert( twonlyDB.contacts, ContactsCompanion( userId: Value(contactId), username: Value('mock_user_$authorIndex'), displayName: Value('Author $authorIndex'), ), mode: InsertMode.insertOrReplace, ) ..insert( twonlyDB.mediaFiles, MediaFilesCompanion( mediaId: Value(mediaId), type: const Value(MediaType.image), stored: const Value(true), createdAt: Value(itemDate), createdAtMonth: Value(DateFormat('MMMM yyyy').format(itemDate)), ), mode: InsertMode.insertOrReplace, ) ..insert( twonlyDB.messages, MessagesCompanion( messageId: Value('mock_msg_$i'), groupId: const Value(groupId), senderId: Value(contactId), type: const Value('media'), mediaId: Value(mediaId), mediaStored: const Value(true), openedAt: Value(now), createdAt: Value(itemDate), ), mode: InsertMode.insertOrReplace, ); } }); // Render custom vector avatars and background colors efficiently for (var i = 0; i < 1000; i++) { final mediaId = 'mock_gen_$i'; final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); // Background color final hue = (i * 137.5) % 360; final bgColor = HSLColor.fromAHSL(1, hue, 0.65, 0.45).toColor(); canvas.drawRect(Offset.zero & size, Paint()..color = bgColor); // Avatar vector representation on it final center = Offset(size.width / 2, size.height / 2); final avatarBgPaint = Paint() ..color = Colors.white.withValues(alpha: 0.25); canvas.drawCircle(center, 120, avatarBgPaint); final eyePaint = Paint() ..color = Colors.white ..style = PaintingStyle.fill; final mouthPaint = Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 8 ..strokeCap = StrokeCap.round; const eyeOffset = 35.0; final eyeRadius = 12.0 + (i % 5) * 2; canvas ..drawCircle( center + const Offset(-eyeOffset, -20), eyeRadius, eyePaint, ) ..drawCircle( center + const Offset(eyeOffset, -20), eyeRadius, eyePaint, ); final mouthRect = Rect.fromCenter( center: center + const Offset(0, 20), width: 60, height: 40, ); final startAngle = 0.2 + (i % 3) * 0.1; final sweepAngle = 2.7 - (i % 3) * 0.2; canvas.drawArc(mouthRect, startAngle, sweepAngle, false, mouthPaint); final textSpan = TextSpan( text: '#$i', style: const TextStyle( color: Colors.white, fontSize: 32, fontWeight: FontWeight.bold, ), ); final textPainter = TextPainter( text: textSpan, textDirection: ui.TextDirection.ltr, )..layout(); textPainter.paint( canvas, Offset((size.width - textPainter.width) / 2, size.height - 80), ); final picture = recorder.endRecording(); final img = await picture.toImage( size.width.toInt(), size.height.toInt(), ); final byteData = await img.toByteData(format: ui.ImageByteFormat.png); if (byteData != null) { final bytes = byteData.buffer.asUint8List(); final mediaFile = MediaFile( mediaId: mediaId, type: MediaType.image, stored: true, requiresAuthentication: false, isDraftMedia: false, isFavorite: false, hasCropAnalyzed: false, hasThumbnail: false, createdAt: now, ); final mediaService = MediaFileService(mediaFile); if (!mediaService.storedPath.parent.existsSync()) { mediaService.storedPath.parent.createSync(recursive: true); } mediaService.storedPath.writeAsBytesSync(bytes); mediaService.thumbnailPath.writeAsBytesSync(bytes); } } if (mounted) { showSnackbar( context, 'Successfully generated 1000 mock images!', level: SnackbarLevel.success, ); } } catch (e) { if (mounted) { showSnackbar(context, 'Error generating images: $e'); } } finally { if (mounted) { setState(() { _isGeneratingMockImages = false; }); } } } Future toggleDeveloperSettings() async { await UserService.update((u) => u.isDeveloper = !u.isDeveloper); } Future toggleVideoStabilization() async { await UserService.update( (u) => u.videoStabilizationEnabled = !u.videoStabilizationEnabled, ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Developer Settings'), ), body: StreamBuilder( stream: userService.onUserUpdated, builder: (context, _) { return ListView( children: [ ListTile( title: const Text('Show Developer Settings'), onTap: toggleDeveloperSettings, trailing: Switch( value: userService.currentUser.isDeveloper, onChanged: (_) => toggleDeveloperSettings(), ), ), ListTile( title: const Text('User ID'), subtitle: Text(userService.currentUser.userId.toString()), ), ListTile( title: const Text('Show Retransmission Database'), onTap: () => context.push( Routes.settingsDeveloperRetransmissionDatabase, ), ), ListTile( title: const Text('Show User Discovery Database'), onTap: () => context.navPush(const UserDiscoveryDeveloperView()), ), ListTile( title: const Text('Toggle Video Stabilization'), onTap: toggleVideoStabilization, trailing: Switch( value: userService.currentUser.videoStabilizationEnabled, onChanged: (a) => toggleVideoStabilization(), ), ), ListTile( title: const Text('Delete all (!) app data'), onTap: () async { final ok = await showAlertDialog( context, 'Sure?', 'If you do not have a backup, you have to register with a new account.', ); if (ok) { await deleteLocalUserData(); await Restart.restartApp( notificationTitle: 'Account successfully deleted', notificationBody: 'Click here to open the app again', forceKill: true, ); } }, ), ListTile( title: const Text('Reduce flames'), onTap: () => context.push(Routes.settingsDeveloperReduceFlames), ), if (!kReleaseMode) ListTile( title: const Text('Make it possible to reset flames'), onTap: () async { final chats = await twonlyDB.groupsDao.getAllDirectChats(); for (final chat in chats) { await twonlyDB.groupsDao.updateGroup( chat.groupId, GroupsCompanion( flameCounter: const Value(0), maxFlameCounter: const Value(365), lastFlameCounterChange: Value(clock.now()), maxFlameCounterFrom: Value( clock.now().subtract(const Duration(days: 1)), ), ), ); } await HapticFeedback.heavyImpact(); }, ), if (!kReleaseMode) ListTile( title: const Text('Automated Testing'), onTap: () => context.push(Routes.settingsDeveloperAutomatedTesting), ), if (kDebugMode) ListTile( title: const Text('Generate 1000 Mock Images'), trailing: _isGeneratingMockImages ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ) : null, onTap: _isGeneratingMockImages ? null : _generate1000MockImages, ), ListTile( title: const Text('Reopen Setup'), onTap: () async { await UserService.update((u) { u.currentSetupPage = SetupPages.profile.name; }); }, ), ], ); }, ), ); } }