mirror of
https://github.com/twonlyapp/twonly-app.git
synced 2026-05-25 06:42:12 +00:00
Some checks are pending
Flutter analyze & test / flutter_analyze_and_test (push) Waiting to run
382 lines
13 KiB
Dart
382 lines
13 KiB
Dart
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<DeveloperSettingsView> createState() => _DeveloperSettingsViewState();
|
|
}
|
|
|
|
class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
|
|
bool _isGeneratingMockImages = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
Future<void> _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<void> toggleDeveloperSettings() async {
|
|
await UserService.update((u) => u.isDeveloper = !u.isDeveloper);
|
|
}
|
|
|
|
Future<void> 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<void>(
|
|
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;
|
|
});
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|