From b584974491039e610cdc5d95aac2692a8a04bffa Mon Sep 17 00:00:00 2001 From: otsmr Date: Sat, 8 Nov 2025 21:44:55 +0100 Subject: [PATCH 1/8] flutter analyzer issues --- .../chat_list_components/group_list_item.dart | 3 ++- .../updates/62_database_migration.view.dart | 3 ++- pubspec.yaml | 23 ++++++++----------- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/lib/src/views/chats/chat_list_components/group_list_item.dart b/lib/src/views/chats/chat_list_components/group_list_item.dart index e70ebf8..5531733 100644 --- a/lib/src/views/chats/chat_list_components/group_list_item.dart +++ b/lib/src/views/chats/chat_list_components/group_list_item.dart @@ -221,7 +221,8 @@ class _UserListItem extends State { : Row( children: [ LastMessageTime( - dateTime: widget.group.lastMessageExchange), + dateTime: widget.group.lastMessageExchange, + ), FlameCounterWidget( groupId: widget.group.groupId, prefix: true, diff --git a/lib/src/views/updates/62_database_migration.view.dart b/lib/src/views/updates/62_database_migration.view.dart index b2d0bff..fb3bb01 100644 --- a/lib/src/views/updates/62_database_migration.view.dart +++ b/lib/src/views/updates/62_database_migration.view.dart @@ -44,7 +44,8 @@ class _DatabaseMigrationViewState extends State { Uint8List? avatarSvg; if (oldContact.avatarSvg != null) { avatarSvg = Uint8List.fromList( - gzip.encode(utf8.encode(oldContact.avatarSvg!))); + gzip.encode(utf8.encode(oldContact.avatarSvg!)), + ); } await twonlyDB.contactsDao.insertContact( ContactsCompanion( diff --git a/pubspec.yaml b/pubspec.yaml index f964597..284db00 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,25 +76,22 @@ dependencies: web_socket_channel: ^3.0.1 dependency_overrides: - - flutter_secure_storage_darwin: - git: - url: https://github.com/juliansteenbakker/flutter_secure_storage.git - ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop - path: flutter_secure_storage_darwin/ - - flutter_android_volume_keydown: - git: - url: https://github.com/yenchieh/flutter_android_volume_keydown.git - branch: fix/lStar-not-found-error - - # hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY camera_android_camerax: # path: ../flutter-packages/packages/camera/camera_android_camerax git: url: https://github.com/otsmr/flutter-packages.git path: packages/camera/camera_android_camerax ref: aef58af205a5f3ce6588a5c59bb2e734aab943f0 + flutter_android_volume_keydown: + git: + url: https://github.com/yenchieh/flutter_android_volume_keydown.git + branch: fix/lStar-not-found-error + flutter_secure_storage_darwin: + git: + url: https://github.com/juliansteenbakker/flutter_secure_storage.git + ref: a06ead81809c900e7fc421a30db0adf3b5919139 # from develop + path: flutter_secure_storage_darwin/ + # hardcoding the mirror mode of the VideCapture to MIRROR_MODE_ON_FRONT_ONLY dev_dependencies: build_runner: ^2.4.15 From 85d755e46beccfe28a7091b33bfd1af29e318db5 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 9 Nov 2025 22:00:21 +0100 Subject: [PATCH 2/8] fix #300 --- lib/src/database/daos/groups.dao.dart | 4 ++-- lib/src/utils/storage.dart | 4 ++++ lib/src/views/components/max_flame_list_title.dart | 6 +++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/src/database/daos/groups.dao.dart b/lib/src/database/daos/groups.dao.dart index 85ce7ec..89730f5 100644 --- a/lib/src/database/daos/groups.dao.dart +++ b/lib/src/database/daos/groups.dao.dart @@ -322,11 +322,11 @@ class GroupsDao extends DatabaseAccessor with _$GroupsDaoMixin { flameCounter += 1; lastFlameCounterChange = Value(timestamp); // Overwrite max flame counter either the current is bigger or the th max flame counter is older then 4 days - if ((flameCounter + 1) >= maxFlameCounter || + if (flameCounter >= maxFlameCounter || maxFlameCounterFrom == null || maxFlameCounterFrom .isBefore(DateTime.now().subtract(const Duration(days: 5)))) { - maxFlameCounter = flameCounter + 1; + maxFlameCounter = flameCounter; maxFlameCounterFrom = DateTime.now(); } } diff --git a/lib/src/utils/storage.dart b/lib/src/utils/storage.dart index 9466769..db474dc 100644 --- a/lib/src/utils/storage.dart +++ b/lib/src/utils/storage.dart @@ -59,6 +59,10 @@ Future updateUserdata( final userData = await updateProtection.protect(() async { final user = await getUser(); if (user == null) return null; + if (user.defaultShowTime == 999999) { + // This was the old version for infinity -> change it to null + user.defaultShowTime = null; + } final updated = updateUser(user); await const FlutterSecureStorage() .write(key: SecureStorageKeys.userData, value: jsonEncode(updated)); diff --git a/lib/src/views/components/max_flame_list_title.dart b/lib/src/views/components/max_flame_list_title.dart index ba490e8..3476797 100644 --- a/lib/src/views/components/max_flame_list_title.dart +++ b/lib/src/views/components/max_flame_list_title.dart @@ -36,8 +36,8 @@ class _MaxFlameListTitleState extends State { _flameCounterSub = stream.listen((counter) { if (mounted) { setState(() { - _flameCounter = counter - - 1; // in the watchFlameCounter a one is added, so remove this here + // in the watchFlameCounter a one is added, so remove this here + _flameCounter = counter - 1; }); } }); @@ -84,7 +84,7 @@ class _MaxFlameListTitleState extends State { @override Widget build(BuildContext context) { if (_directChat == null || - _directChat!.maxFlameCounter == 0 || + _directChat!.maxFlameCounter <= 2 || _flameCounter >= _directChat!.maxFlameCounter || _directChat!.maxFlameCounterFrom! .isBefore(DateTime.now().subtract(const Duration(days: 4)))) { From 5946420fd04d80f87264c9a97649b6ded51569c3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 9 Nov 2025 22:27:24 +0100 Subject: [PATCH 3/8] fix #298 --- .../mediafiles/mediafile.service.dart | 5 +-- lib/src/utils/misc.dart | 2 + .../save_to_gallery.dart | 17 ++++---- .../views/settings/data_and_storage.view.dart | 40 ++++++++----------- 4 files changed, 31 insertions(+), 33 deletions(-) diff --git a/lib/src/services/mediafiles/mediafile.service.dart b/lib/src/services/mediafiles/mediafile.service.dart index 2dab6fd..001ad95 100644 --- a/lib/src/services/mediafiles/mediafile.service.dart +++ b/lib/src/services/mediafiles/mediafile.service.dart @@ -231,15 +231,14 @@ class MediaFileService { ), ); - if (originalPath.existsSync()) { - await originalPath.copy(tempPath.path); + if (originalPath.existsSync() && !tempPath.existsSync()) { await compressMedia(); } if (tempPath.existsSync()) { await tempPath.copy(storedPath.path); } else { Log.error( - 'Could not store image neither tempPath nor originalPath exists.', + 'Could not store image neither as tempPath does not exists.', ); } unawaited(createThumbnail()); diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index 27b151e..2b8f8dd 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -39,6 +39,7 @@ Future saveImageToGallery(Uint8List imageBytes) async { await Gal.putImageBytes(jpgImages); return null; } on GalException catch (e) { + Log.error(e); return e.type.message; } } @@ -52,6 +53,7 @@ Future saveVideoToGallery(String videoPath) async { await Gal.putVideo(videoPath); return null; } on GalException catch (e) { + Log.error(e); return e.type.message; } } diff --git a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart index 4a2be05..1251fbb 100644 --- a/lib/src/views/camera/camera_preview_components/save_to_gallery.dart +++ b/lib/src/views/camera/camera_preview_components/save_to_gallery.dart @@ -4,6 +4,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/services/mediafiles/mediafile.service.dart'; +import 'package:twonly/src/utils/log.dart'; import 'package:twonly/src/utils/misc.dart'; class SaveToGalleryButton extends StatefulWidget { @@ -54,22 +55,24 @@ class SaveToGalleryButtonState extends State { final storedMediaPath = widget.mediaService.storedPath; - final storeToGallery = gUser.storeMediaFilesInGallery; - await widget.mediaService.storeMediaFile(); - if (storeToGallery) { - res = await saveVideoToGallery(storedMediaPath.path); + if (gUser.storeMediaFilesInGallery) { + if (widget.mediaService.mediaFile.type == MediaType.video) { + res = await saveVideoToGallery(storedMediaPath.path); + } else { + res = await saveImageToGallery( + storedMediaPath.readAsBytesSync(), + ); + } } - await widget.mediaService.compressMedia(); - await widget.mediaService.createThumbnail(); - if (res == null) { setState(() { _imageSaved = true; }); } else if (mounted && context.mounted) { + Log.error('Could not store media file in the gallery.'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(res), diff --git a/lib/src/views/settings/data_and_storage.view.dart b/lib/src/views/settings/data_and_storage.view.dart index 5f5b964..2ddbf59 100644 --- a/lib/src/views/settings/data_and_storage.view.dart +++ b/lib/src/views/settings/data_and_storage.view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/services/api/mediafiles/download.service.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; @@ -14,24 +15,6 @@ class DataAndStorageView extends StatefulWidget { } class _DataAndStorageViewState extends State { - Map> autoDownloadOptions = defaultAutoDownloadOptions; - bool storeMediaFilesInGallery = true; - - @override - void initState() { - super.initState(); - } - - Future initAsync() async { - final user = await getUser(); - if (user == null) return; - setState(() { - autoDownloadOptions = - user.autoDownloadOptions ?? defaultAutoDownloadOptions; - storeMediaFilesInGallery = user.storeMediaFilesInGallery; - }); - } - Future showAutoDownloadOptions( BuildContext context, ConnectivityResult connectionMode, @@ -41,10 +24,11 @@ class _DataAndStorageViewState extends State { context: context, builder: (BuildContext context) { return AutoDownloadOptionsDialog( - autoDownloadOptions: autoDownloadOptions, + autoDownloadOptions: + gUser.autoDownloadOptions ?? defaultAutoDownloadOptions, connectionMode: connectionMode, onUpdate: () async { - await initAsync(); + setState(() {}); }, ); }, @@ -53,14 +37,16 @@ class _DataAndStorageViewState extends State { Future toggleStoreInGallery() async { await updateUserdata((u) { - u.storeMediaFilesInGallery = !storeMediaFilesInGallery; + u.storeMediaFilesInGallery = !u.storeMediaFilesInGallery; return u; }); - await initAsync(); + setState(() {}); } @override Widget build(BuildContext context) { + final autoDownloadOptions = + gUser.autoDownloadOptions ?? defaultAutoDownloadOptions; return Scaffold( appBar: AppBar( title: Text(context.lang.settingsStorageData), @@ -72,7 +58,7 @@ class _DataAndStorageViewState extends State { subtitle: Text(context.lang.settingsStorageDataStoreInGSubtitle), onTap: toggleStoreInGallery, trailing: Switch( - value: storeMediaFilesInGallery, + value: gUser.storeMediaFilesInGallery, onChanged: (a) => toggleStoreInGallery(), ), ), @@ -157,6 +143,14 @@ class _AutoDownloadOptionsDialogState extends State { await _updateAutoDownloadSetting(DownloadMediaTypes.video, value); }, ), + CheckboxListTile( + title: const Text('Audio'), + value: autoDownloadOptions[widget.connectionMode.name]! + .contains(DownloadMediaTypes.audio.name), + onChanged: (bool? value) async { + await _updateAutoDownloadSetting(DownloadMediaTypes.audio, value); + }, + ), ], ), actions: [ From 1cc19f7834dcb13c108768d5a00984c86c7c7921 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 9 Nov 2025 22:32:53 +0100 Subject: [PATCH 4/8] fix #297 --- .../views/camera/camera_preview_controller_view.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 3c7019a..6fb566a 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -206,6 +206,10 @@ class _CameraPreviewViewState extends State { FlutterVolumeController.addListener( (volume) async { + if (!widget.isVisible) { + await deInitVolumeControl(); + return; + } if (startedVolume == null) { startedVolume = volume; return; @@ -221,7 +225,12 @@ class _CameraPreviewViewState extends State { } if (Platform.isAndroid) { androidVolumeDownSub = FlutterAndroidVolumeKeydown.stream.listen((event) { - takePicture(); + if (widget.isVisible) { + takePicture(); + } else { + deInitVolumeControl(); + return; + } }); } } From bc2c9988509759f63972b76a7d82657d81d393e2 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 9 Nov 2025 22:57:32 +0100 Subject: [PATCH 5/8] fixing sizing of the share image screen --- .../camera_preview.dart | 4 ++ .../camera_preview_controller_view.dart | 2 + .../image_editor/layers/text_layer.dart | 46 +++++++++++++------ .../views/camera/share_image_editor_view.dart | 1 + .../views/components/media_view_sizing.dart | 5 +- 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/lib/src/views/camera/camera_preview_components/camera_preview.dart b/lib/src/views/camera/camera_preview_components/camera_preview.dart index a59d5c7..8cfb61d 100644 --- a/lib/src/views/camera/camera_preview_components/camera_preview.dart +++ b/lib/src/views/camera/camera_preview_components/camera_preview.dart @@ -20,6 +20,8 @@ class HomeViewCameraPreview extends StatelessWidget { } return Positioned.fill( child: MediaViewSizing( + requiredHeight: 90, + bottomNavigation: Container(), child: Screenshot( controller: screenshotController, child: AspectRatio( @@ -58,6 +60,8 @@ class SendToCameraPreview extends StatelessWidget { } return Positioned.fill( child: MediaViewSizing( + requiredHeight: 90, + bottomNavigation: Container(), child: Screenshot( controller: screenshotController, child: AspectRatio( diff --git a/lib/src/views/camera/camera_preview_controller_view.dart b/lib/src/views/camera/camera_preview_controller_view.dart index 6fb566a..c3c7b77 100644 --- a/lib/src/views/camera/camera_preview_controller_view.dart +++ b/lib/src/views/camera/camera_preview_controller_view.dart @@ -590,6 +590,8 @@ class _CameraPreviewViewState extends State { return Container(); } return MediaViewSizing( + requiredHeight: 90, + bottomNavigation: Container(), child: GestureDetector( onPanStart: (details) async { if (isFront) { diff --git a/lib/src/views/camera/image_editor/layers/text_layer.dart b/lib/src/views/camera/image_editor/layers/text_layer.dart index 72dc428..b6b98e6 100755 --- a/lib/src/views/camera/image_editor/layers/text_layer.dart +++ b/lib/src/views/camera/image_editor/layers/text_layer.dart @@ -62,6 +62,27 @@ class _TextViewState extends State { }); } + Future onEditionComplete() async { + Future.delayed(const Duration(milliseconds: 10), () async { + setState(() { + widget.layerData.isDeleted = textController.text == ''; + widget.layerData.isEditing = false; + widget.layerData.text = textController.text; + }); + + if (!mounted) return; + + await context + .read() + .updateSomeTextViewIsAlreadyEditing(false); + if (widget.onUpdate != null) { + widget.onUpdate!(); + } + }); + } + + double maxBottomInset = 0; + @override Widget build(BuildContext context) { if (widget.layerData.isDeleted) return Container(); @@ -69,6 +90,16 @@ class _TextViewState extends State { final bottom = MediaQuery.of(context).viewInsets.bottom + MediaQuery.of(context).viewPadding.bottom; + if (maxBottomInset > bottom) { + maxBottomInset = 0; + if (widget.layerData.isEditing) { + widget.layerData.isEditing = false; + onEditionComplete(); + } + } else { + maxBottomInset = bottom; + } + if (widget.layerData.isEditing) { return Positioned( bottom: bottom - localBottom, @@ -83,20 +114,7 @@ class _TextViewState extends State { autofocus: true, maxLines: null, minLines: 1, - onEditingComplete: () async { - setState(() { - widget.layerData.isDeleted = textController.text == ''; - widget.layerData.isEditing = false; - widget.layerData.text = textController.text; - }); - - await context - .read() - .updateSomeTextViewIsAlreadyEditing(false); - if (widget.onUpdate != null) { - widget.onUpdate!(); - } - }, + onEditingComplete: onEditionComplete, onTapOutside: (a) async { widget.layerData.text = textController.text; Future.delayed(const Duration(milliseconds: 100), () async { diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index d38c36e..daa2ca8 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -477,6 +477,7 @@ class _ShareImageEditorView extends State { setState(() {}); }, child: MediaViewSizing( + requiredHeight: 90, bottomNavigation: ColoredBox( color: Theme.of(context).colorScheme.surface, child: Row( diff --git a/lib/src/views/components/media_view_sizing.dart b/lib/src/views/components/media_view_sizing.dart index 2dd7bc6..46ed904 100644 --- a/lib/src/views/components/media_view_sizing.dart +++ b/lib/src/views/components/media_view_sizing.dart @@ -58,7 +58,10 @@ class _MediaViewSizingState extends State { if (widget.bottomNavigation != null) { if (needToDownSizeImage) { imageChild = Expanded(child: imageChild); - bottomNavigation = widget.bottomNavigation!; + bottomNavigation = SizedBox( + height: widget.requiredHeight, + child: widget.bottomNavigation, + ); } else { bottomNavigation = Expanded(child: widget.bottomNavigation!); } From 6d86af155c90f555f19c7147931c6ea2273cd7f3 Mon Sep 17 00:00:00 2001 From: otsmr Date: Sun, 9 Nov 2025 23:32:46 +0100 Subject: [PATCH 6/8] ask before deleting image --- lib/src/localization/app_de.arb | 4 +- lib/src/localization/app_en.arb | 4 +- .../generated/app_localizations.dart | 12 + .../generated/app_localizations_de.dart | 7 + .../generated/app_localizations_en.dart | 7 + .../image_editor/layers/filter_layer.dart | 2 +- .../views/camera/share_image_editor_view.dart | 318 ++++++++++-------- 7 files changed, 216 insertions(+), 138 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 5e7bba5..deaf7b0 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -818,5 +818,7 @@ "deleteChatAfterAMonth": "einem Monat.", "deleteChatAfterAYear": "einem Jahr.", "yourTwonlyScore": "Dein twonly-Score", - "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal." + "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.", + "dialogAskDeleteMediaFilePopTitle": "Bist du sicher, dass du dein Meisterwerk löschen möchtest?", + "dialogAskDeleteMediaFilePopDelete": "Löschen" } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index cb78dfd..c122b12 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -596,5 +596,7 @@ "deleteChatAfterAMonth": "one month.", "deleteChatAfterAYear": "one year.", "yourTwonlyScore": "Your twonly-Score", - "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days." + "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.", + "dialogAskDeleteMediaFilePopTitle": "Are you sure you want to delete your masterpiece?", + "dialogAskDeleteMediaFilePopDelete": "Delete" } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index feedd96..549cd0d 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2683,6 +2683,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'** String get registrationClosed; + + /// No description provided for @dialogAskDeleteMediaFilePopTitle. + /// + /// In en, this message translates to: + /// **'Are you sure you want to delete your masterpiece?'** + String get dialogAskDeleteMediaFilePopTitle; + + /// No description provided for @dialogAskDeleteMediaFilePopDelete. + /// + /// In en, this message translates to: + /// **'Delete'** + String get dialogAskDeleteMediaFilePopDelete; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index fe61244..ce54fd6 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1481,4 +1481,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get registrationClosed => 'Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.'; + + @override + String get dialogAskDeleteMediaFilePopTitle => + 'Bist du sicher, dass du dein Meisterwerk löschen möchtest?'; + + @override + String get dialogAskDeleteMediaFilePopDelete => 'Löschen'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 790bd19..180e7fc 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1471,4 +1471,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get registrationClosed => 'Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.'; + + @override + String get dialogAskDeleteMediaFilePopTitle => + 'Are you sure you want to delete your masterpiece?'; + + @override + String get dialogAskDeleteMediaFilePopDelete => 'Delete'; } diff --git a/lib/src/views/camera/image_editor/layers/filter_layer.dart b/lib/src/views/camera/image_editor/layers/filter_layer.dart index 13e76a2..813ea66 100644 --- a/lib/src/views/camera/image_editor/layers/filter_layer.dart +++ b/lib/src/views/camera/image_editor/layers/filter_layer.dart @@ -75,7 +75,7 @@ class _FilterLayerState extends State { List pages = [ const FilterSkeleton(), const DateTimeFilter(), - const LocationFilter(), + // const LocationFilter(), const FilterSkeleton(), ]; diff --git a/lib/src/views/camera/share_image_editor_view.dart b/lib/src/views/camera/share_image_editor_view.dart index daa2ca8..515e8ce 100644 --- a/lib/src/views/camera/share_image_editor_view.dart +++ b/lib/src/views/camera/share_image_editor_view.dart @@ -264,6 +264,40 @@ class _ShareImageEditorView extends State { ]; } + Future _showBackDialog() { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + context.lang.dialogAskDeleteMediaFilePopTitle, + ), + actions: [ + FilledButton( + child: Text(context.lang.dialogAskDeleteMediaFilePopDelete), + onPressed: () { + Navigator.pop(context, true); + }, + ), + TextButton( + child: Text(context.lang.cancel), + onPressed: () { + Navigator.pop(context, false); + }, + ), + ], + ); + }, + ); + } + + Future askToCloseThenClose() async { + final shouldPop = await _showBackDialog() ?? false; + if (mounted && shouldPop) { + Navigator.pop(context); + } + } + List get actionsAtTheTop { if (layers.isNotEmpty && layers.last.isEditing && @@ -275,7 +309,14 @@ class _ShareImageEditorView extends State { FontAwesomeIcons.xmark, tooltipText: context.lang.close, onPressed: () async { - Navigator.pop(context, false); + final nonImageFilterLayer = layers.where( + (x) => x is! BackgroundLayerData && x is! FilterLayerData, + ); + if (nonImageFilterLayer.isEmpty) { + Navigator.pop(context, false); + } else { + await askToCloseThenClose(); + } }, ), Expanded(child: Container()), @@ -446,154 +487,161 @@ class _ShareImageEditorView extends State { Widget build(BuildContext context) { pixelRatio = MediaQuery.of(context).devicePixelRatio; - return Scaffold( - backgroundColor: - widget.sharedFromGallery ? null : Colors.white.withAlpha(0), - resizeToAvoidBottomInset: false, - body: Stack( - fit: StackFit.expand, - children: [ - GestureDetector( - onTapDown: (details) { - if (details.globalPosition.dy > 60) { - tabDownPosition = details.globalPosition.dy - 60; - } else { - tabDownPosition = details.globalPosition.dy; - } - }, - onTap: () { - if (layers.any((x) => x.isEditing)) { - return; - } - layers = layers.where((x) => !x.isDeleted).toList(); - undoLayers.clear(); - removedLayers.clear(); - layers.add( - TextLayerData( - offset: Offset(0, tabDownPosition), - textLayersBefore: layers.whereType().length, - ), - ); - setState(() {}); - }, - child: MediaViewSizing( - requiredHeight: 90, - bottomNavigation: ColoredBox( - color: Theme.of(context).colorScheme.surface, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SaveToGalleryButton( - storeImageAsOriginal: storeImageAsOriginal, - mediaService: mediaService, - displayButtonLabel: widget.sendToGroup == null, - isLoading: loadingImage, - ), - if (widget.sendToGroup != null) const SizedBox(width: 10), - if (widget.sendToGroup != null) - OutlinedButton( - style: OutlinedButton.styleFrom( - iconColor: Theme.of(context).colorScheme.primary, - foregroundColor: - Theme.of(context).colorScheme.primary, - ), - onPressed: pushShareImageView, - child: const FaIcon(FontAwesomeIcons.userPlus), + return PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, bool? result) async { + if (didPop) return; + await askToCloseThenClose(); + }, + child: Scaffold( + backgroundColor: + widget.sharedFromGallery ? null : Colors.white.withAlpha(0), + resizeToAvoidBottomInset: false, + body: Stack( + fit: StackFit.expand, + children: [ + GestureDetector( + onTapDown: (details) { + if (details.globalPosition.dy > 60) { + tabDownPosition = details.globalPosition.dy - 60; + } else { + tabDownPosition = details.globalPosition.dy; + } + }, + onTap: () { + if (layers.any((x) => x.isEditing)) { + return; + } + layers = layers.where((x) => !x.isDeleted).toList(); + undoLayers.clear(); + removedLayers.clear(); + layers.add( + TextLayerData( + offset: Offset(0, tabDownPosition), + textLayersBefore: layers.whereType().length, + ), + ); + setState(() {}); + }, + child: MediaViewSizing( + requiredHeight: 90, + bottomNavigation: ColoredBox( + color: Theme.of(context).colorScheme.surface, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SaveToGalleryButton( + storeImageAsOriginal: storeImageAsOriginal, + mediaService: mediaService, + displayButtonLabel: widget.sendToGroup == null, + isLoading: loadingImage, ), - SizedBox(width: widget.sendToGroup == null ? 20 : 10), - FilledButton.icon( - icon: sendingOrLoadingImage - ? SizedBox( - height: 12, - width: 12, - child: CircularProgressIndicator( - strokeWidth: 2, - color: Theme.of(context) - .colorScheme - .inversePrimary, - ), - ) - : const FaIcon(FontAwesomeIcons.solidPaperPlane), - onPressed: () async { - if (sendingOrLoadingImage) return; - if (widget.sendToGroup == null) { - return pushShareImageView(); - } - await sendImageToSinglePerson(); - }, - style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.symmetric( - vertical: 10, - horizontal: 30, + if (widget.sendToGroup != null) const SizedBox(width: 10), + if (widget.sendToGroup != null) + OutlinedButton( + style: OutlinedButton.styleFrom( + iconColor: Theme.of(context).colorScheme.primary, + foregroundColor: + Theme.of(context).colorScheme.primary, + ), + onPressed: pushShareImageView, + child: const FaIcon(FontAwesomeIcons.userPlus), + ), + SizedBox(width: widget.sendToGroup == null ? 20 : 10), + FilledButton.icon( + icon: sendingOrLoadingImage + ? SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Theme.of(context) + .colorScheme + .inversePrimary, + ), + ) + : const FaIcon(FontAwesomeIcons.solidPaperPlane), + onPressed: () async { + if (sendingOrLoadingImage) return; + if (widget.sendToGroup == null) { + return pushShareImageView(); + } + await sendImageToSinglePerson(); + }, + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric( + vertical: 10, + horizontal: 30, + ), ), ), + label: Text( + (widget.sendToGroup == null) + ? context.lang.shareImagedEditorShareWith + : substringBy(widget.sendToGroup!.groupName, 15), + style: const TextStyle(fontSize: 17), + ), ), - label: Text( - (widget.sendToGroup == null) - ? context.lang.shareImagedEditorShareWith - : substringBy(widget.sendToGroup!.groupName, 15), - style: const TextStyle(fontSize: 17), - ), - ), - ], + ], + ), ), - ), - child: SizedBox( - height: currentImage.height / pixelRatio, - width: currentImage.width / pixelRatio, - child: Stack( - children: [ - if (videoController != null) - Positioned.fill( - child: VideoPlayer(videoController!), - ), - Screenshot( - controller: screenshotController, - child: LayersViewer( - layers: layers.where((x) => !x.isDeleted).toList(), - onUpdate: () { - for (final layer in layers) { - layer.isEditing = false; - if (layer.isDeleted) { - removedLayers.add(layer); + child: SizedBox( + height: currentImage.height / pixelRatio, + width: currentImage.width / pixelRatio, + child: Stack( + children: [ + if (videoController != null) + Positioned.fill( + child: VideoPlayer(videoController!), + ), + Screenshot( + controller: screenshotController, + child: LayersViewer( + layers: layers.where((x) => !x.isDeleted).toList(), + onUpdate: () { + for (final layer in layers) { + layer.isEditing = false; + if (layer.isDeleted) { + removedLayers.add(layer); + } } - } - layers = layers.where((x) => !x.isDeleted).toList(); - setState(() {}); - }, + layers = layers.where((x) => !x.isDeleted).toList(); + setState(() {}); + }, + ), ), - ), - ], + ], + ), ), ), ), - ), - Positioned( - top: 10, - left: 5, - right: 0, - child: SafeArea( - child: Row( - children: actionsAtTheTop, - ), - ), - ), - Positioned( - right: 6, - top: 100, - child: Container( - alignment: Alignment.bottomCenter, - padding: const EdgeInsets.symmetric(vertical: 16), + Positioned( + top: 10, + left: 5, + right: 0, child: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: actionsAtTheRight, + child: Row( + children: actionsAtTheTop, ), ), ), - ), - ], + Positioned( + right: 6, + top: 100, + child: Container( + alignment: Alignment.bottomCenter, + padding: const EdgeInsets.symmetric(vertical: 16), + child: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: actionsAtTheRight, + ), + ), + ), + ), + ], + ), ), ); } From 091e4bbfa89fe672e95578d9a49bb71ab239ec5e Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 10 Nov 2025 00:04:44 +0100 Subject: [PATCH 7/8] adds optional sentry #299 --- lib/globals.dart | 1 + lib/main.dart | 32 ++++++++-- lib/src/database/daos/signal.dao.dart | 2 - .../signal/connect_pre_key_store.dart | 3 - lib/src/localization/app_de.arb | 4 +- lib/src/localization/app_en.arb | 4 +- .../generated/app_localizations.dart | 12 ++++ .../generated/app_localizations_de.dart | 7 +++ .../generated/app_localizations_en.dart | 7 +++ lib/src/model/json/userdata.dart | 6 +- lib/src/model/json/userdata.g.dart | 5 +- .../api/mediafiles/download.service.dart | 4 +- lib/src/services/api/messages.dart | 4 +- lib/src/utils/log.dart | 18 ++++++ .../views/settings/help/diagnostics.view.dart | 2 +- lib/src/views/settings/help/help.view.dart | 61 ++++++++++++++----- pubspec.lock | 32 ++++++++++ pubspec.yaml | 1 + 18 files changed, 168 insertions(+), 37 deletions(-) diff --git a/lib/globals.dart b/lib/globals.dart index 5a9acbf..279e447 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -33,3 +33,4 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = Map globalUserDataChangedCallBack = {}; bool globalIsAppInBackground = true; +bool globalAllowErrorTrackingViaSentry = false; diff --git a/lib/main.dart b/lib/main.dart index 7ff34fd..76b2f4d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter/services.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/app.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -69,13 +70,34 @@ void main() async { unawaited(createPushAvatars()); await twonlyDB.messagesDao.purgeMessageTable(); + final providers = [ + ChangeNotifierProvider(create: (_) => settingsController), + ChangeNotifierProvider(create: (_) => CustomChangeProvider()), + ChangeNotifierProvider(create: (_) => ImageEditorProvider()), + ]; + + if (user != null) { + if (user.allowErrorTrackingViaSentry) { + globalAllowErrorTrackingViaSentry = true; + return SentryFlutter.init( + (options) => options + ..dsn = + 'https://6b24a012c85144c9b522440a1d17d01c@glitchtip.twonly.eu/4' + ..tracesSampleRate = 0.01 + ..enableAutoSessionTracking = false, + appRunner: () => runApp( + MultiProvider( + providers: providers, + child: const App(), + ), + ), + ); + } + } + runApp( MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => settingsController), - ChangeNotifierProvider(create: (_) => CustomChangeProvider()), - ChangeNotifierProvider(create: (_) => ImageEditorProvider()), - ], + providers: providers, child: const App(), ), ); diff --git a/lib/src/database/daos/signal.dao.dart b/lib/src/database/daos/signal.dao.dart index 2b64aac..294e358 100644 --- a/lib/src/database/daos/signal.dao.dart +++ b/lib/src/database/daos/signal.dao.dart @@ -57,7 +57,6 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { tbl.preKeyId.equals(preKey.preKeyId), )) .go(); - Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId'); return preKey; } return null; @@ -68,7 +67,6 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { List preKeys, ) async { for (final preKey in preKeys) { - Log.info('[PREKEY] Inserting others ${preKey.preKeyId}'); try { await into(signalContactPreKeys).insert(preKey); } catch (e) { diff --git a/lib/src/database/signal/connect_pre_key_store.dart b/lib/src/database/signal/connect_pre_key_store.dart index 60018c5..61846c3 100644 --- a/lib/src/database/signal/connect_pre_key_store.dart +++ b/lib/src/database/signal/connect_pre_key_store.dart @@ -23,14 +23,12 @@ class ConnectPreKeyStore extends PreKeyStore { '[PREKEY] No such preKey record! - $preKeyId', ); } - Log.info('[PREKEY] Contact used my preKey $preKeyId'); final preKey = preKeyRecord.first.preKey; return PreKeyRecord.fromBuffer(preKey); } @override Future removePreKey(int preKeyId) async { - Log.info('[PREKEY] Removing $preKeyId from my own storage.'); await (twonlyDB.delete(twonlyDB.signalPreKeyStores) ..where((tbl) => tbl.preKeyId.equals(preKeyId))) .go(); @@ -43,7 +41,6 @@ class ConnectPreKeyStore extends PreKeyStore { preKey: Value(record.serialize()), ); - Log.info('[PREKEY] Storing $preKeyId from my own storage.'); try { await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); } catch (e) { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index deaf7b0..5b1a748 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -820,5 +820,7 @@ "yourTwonlyScore": "Dein twonly-Score", "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.", "dialogAskDeleteMediaFilePopTitle": "Bist du sicher, dass du dein Meisterwerk löschen möchtest?", - "dialogAskDeleteMediaFilePopDelete": "Löschen" + "dialogAskDeleteMediaFilePopDelete": "Löschen", + "allowErrorTracking": "Fehler und Crashes mit uns teilen", + "allowErrorTrackingSubtitle": "Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index c122b12..bc772d9 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -598,5 +598,7 @@ "yourTwonlyScore": "Your twonly-Score", "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.", "dialogAskDeleteMediaFilePopTitle": "Are you sure you want to delete your masterpiece?", - "dialogAskDeleteMediaFilePopDelete": "Delete" + "dialogAskDeleteMediaFilePopDelete": "Delete", + "allowErrorTracking": "Share errors and crashes with us", + "allowErrorTrackingSubtitle": "If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 549cd0d..1e8274f 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2695,6 +2695,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Delete'** String get dialogAskDeleteMediaFilePopDelete; + + /// No description provided for @allowErrorTracking. + /// + /// In en, this message translates to: + /// **'Share errors and crashes with us'** + String get allowErrorTracking; + + /// No description provided for @allowErrorTrackingSubtitle. + /// + /// In en, this message translates to: + /// **'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'** + String get allowErrorTrackingSubtitle; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index ce54fd6..b5fc863 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1488,4 +1488,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dialogAskDeleteMediaFilePopDelete => 'Löschen'; + + @override + String get allowErrorTracking => 'Fehler und Crashes mit uns teilen'; + + @override + String get allowErrorTrackingSubtitle => + 'Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 180e7fc..0fb3086 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1478,4 +1478,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get dialogAskDeleteMediaFilePopDelete => 'Delete'; + + @override + String get allowErrorTracking => 'Share errors and crashes with us'; + + @override + String get allowErrorTrackingSubtitle => + 'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 604c273..ca667ac 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -48,9 +48,6 @@ class UserData { int? defaultShowTime; - @JsonKey(defaultValue: true) - bool useHighQuality = true; - @JsonKey(defaultValue: false) bool requestedAudioPermission = false; @@ -73,6 +70,9 @@ class UserData { DateTime? signalLastSignedPreKeyUpdated; + @JsonKey(defaultValue: false) + bool allowErrorTrackingViaSentry = false; + // -- Custom DATA -- @JsonKey(defaultValue: 100_000) diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 6de5cbd..fd997f0 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -26,7 +26,6 @@ UserData _$UserDataFromJson(Map json) => UserData( $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? ThemeMode.system ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() - ..useHighQuality = json['useHighQuality'] as bool? ?? true ..requestedAudioPermission = json['requestedAudioPermission'] as bool? ?? false ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true @@ -50,6 +49,8 @@ UserData _$UserDataFromJson(Map json) => UserData( json['signalLastSignedPreKeyUpdated'] == null ? null : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) + ..allowErrorTrackingViaSentry = + json['allowErrorTrackingViaSentry'] as bool? ?? false ..currentPreKeyIndexStart = (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 ..currentSignedPreKeyIndexStart = @@ -84,7 +85,6 @@ Map _$UserDataToJson(UserData instance) => { 'todaysImageCounter': instance.todaysImageCounter, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'defaultShowTime': instance.defaultShowTime, - 'useHighQuality': instance.useHighQuality, 'requestedAudioPermission': instance.requestedAudioPermission, 'showFeedbackShortcut': instance.showFeedbackShortcut, 'preSelectedEmojies': instance.preSelectedEmojies, @@ -96,6 +96,7 @@ Map _$UserDataToJson(UserData instance) => { 'myBestFriendGroupId': instance.myBestFriendGroupId, 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated?.toIso8601String(), + 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index ec8f659..b5ff2ad 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -248,7 +248,7 @@ Future requestMediaReupload(String mediaId) async { Future handleEncryptedFile(String mediaId) async { final mediaService = await MediaFileService.fromMediaId(mediaId); if (mediaService == null) { - Log.error('Media file $mediaId not found in database.'); + Log.error('Media file not found in database.'); return; } @@ -263,7 +263,7 @@ Future handleEncryptedFile(String mediaId) async { try { encryptedBytes = await mediaService.encryptedPath.readAsBytes(); } catch (e) { - Log.error('Could not read encrypted media file: $mediaId. $e'); + Log.error('Could not read encrypted media file: $e'); await requestMediaReupload(mediaId); return; } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index b2503fc..4a8cb43 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -44,14 +44,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ if (receipt == null) { receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); if (receipt == null) { - Log.error('Receipt $receiptId not found.'); + Log.error('Receipt not found.'); return null; } } receiptId = receipt.receiptId; if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { - Log.error('$receiptId message already uploaded!'); + Log.error('message already uploaded!'); return null; } diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index af28bc6..fa9c34c 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:twonly/globals.dart'; void initLogger() { // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; @@ -20,6 +22,13 @@ void initLogger() { class Log { static void error(Object? message, [Object? error, StackTrace? stackTrace]) { + if (globalAllowErrorTrackingViaSentry) { + try { + throw Exception(message); + } catch (exception, stackTrace) { + Sentry.captureException(exception, stackTrace: stackTrace); + } + } Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace); } @@ -45,6 +54,15 @@ Future loadLogFile() async { } } +Future readLast1000Lines() async { + final dir = await getApplicationSupportDirectory(); + final file = File('${dir.path}/app.log'); + if (!file.existsSync()) return ''; + final all = await file.readAsLines(); + final start = all.length > 1000 ? all.length - 1000 : 0; + return all.sublist(start).join('\n'); +} + Future _writeLogToFile(LogRecord record) async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart index 7f27dd8..a25685a 100644 --- a/lib/src/views/settings/help/diagnostics.view.dart +++ b/lib/src/views/settings/help/diagnostics.view.dart @@ -28,7 +28,7 @@ class _DiagnosticsViewState extends State { return Scaffold( appBar: AppBar(title: const Text('Diagnostics')), body: FutureBuilder( - future: loadLogFile(), + future: readLast1000Lines(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); diff --git a/lib/src/views/settings/help/help.view.dart b/lib/src/views/settings/help/help.view.dart index a0bef20..cbffd12 100644 --- a/lib/src/views/settings/help/help.view.dart +++ b/lib/src/views/settings/help/help.view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; @@ -11,8 +12,22 @@ import 'package:twonly/src/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/views/settings/help/faq.view.dart'; import 'package:url_launcher/url_launcher.dart'; -class HelpView extends StatelessWidget { +class HelpView extends StatefulWidget { const HelpView({super.key}); + + @override + State createState() => _HelpViewState(); +} + +class _HelpViewState extends State { + Future toggleAllowErrorTrackingViaSentry() async { + await updateUserdata((u) { + u.allowErrorTrackingViaSentry = !u.allowErrorTrackingViaSentry; + return u; + }); + setState(() {}); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -49,7 +64,10 @@ class HelpView extends StatelessWidget { ), ListTile( title: Text(context.lang.settingsResetTutorials), - subtitle: Text(context.lang.settingsResetTutorialsDesc), + subtitle: Text( + context.lang.settingsResetTutorialsDesc, + style: const TextStyle(fontSize: 12), + ), onTap: () async { await updateUserdata((user) { user.tutorialDisplayed = []; @@ -65,6 +83,32 @@ class HelpView extends StatelessWidget { }, ), const Divider(), + ListTile( + title: Text(context.lang.allowErrorTracking), + subtitle: Text( + context.lang.allowErrorTrackingSubtitle, + style: const TextStyle(fontSize: 10), + ), + onTap: toggleAllowErrorTrackingViaSentry, + trailing: Switch( + value: gUser.allowErrorTrackingViaSentry, + onChanged: (a) => toggleAllowErrorTrackingViaSentry(), + ), + ), + ListTile( + title: Text(context.lang.settingsHelpDiagnostics), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const DiagnosticsView(); + }, + ), + ); + }, + ), + const Divider(), FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { @@ -97,19 +141,6 @@ class HelpView extends StatelessWidget { ); }, ), - ListTile( - title: Text(context.lang.settingsHelpDiagnostics), - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const DiagnosticsView(); - }, - ), - ); - }, - ), ListTile( title: const Text('Changelog'), onTap: () async { diff --git a/pubspec.lock b/pubspec.lock index 63801d5..76d323c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -976,6 +976,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -1160,6 +1168,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" octo_image: dependency: transitive description: @@ -1440,6 +1456,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 284db00..2222fd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: restart_app: ^1.3.2 screenshot: ^3.0.0 scrollable_positioned_list: ^0.3.8 + sentry_flutter: ^9.8.0 share_plus: ^12.0.0 tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1 From 4ff42c93b75782614723cd24599a52b631c9e59b Mon Sep 17 00:00:00 2001 From: otsmr Date: Mon, 10 Nov 2025 00:09:53 +0100 Subject: [PATCH 8/8] bump version --- CHANGELOG.md | 8 +++++++- ios/Podfile.lock | 17 +++++++++++++++++ pubspec.yaml | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6827338..2e0a71e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.0.67 + +- Adds crash reports (optional). Please consider enabling this under Settings > Help > “Share errors and crashes with us.” +- Fixes bug when saving images to the gallery +- Multiple layout issues fixed +- Multiple bug fixes + ## 0.0.62 - Support for groups with multiple administrators @@ -16,7 +23,6 @@ - Improved reliability of client-to-client messaging - Multiple bug fixes - ## 0.0.61 - Improving image editor when changing colors diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 32c6051..59e9f0b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -191,6 +191,8 @@ PODS: - "no_screenshot (0.0.1+4)": - Flutter - ScreenProtectorKit (~> 1.3.1) + - objective_c (0.0.1): + - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -208,6 +210,11 @@ PODS: - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) + - Sentry/HybridSDK (8.56.2) + - sentry_flutter (9.8.0): + - Flutter + - FlutterMacOS + - Sentry/HybridSDK (= 8.56.2) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -275,10 +282,12 @@ DEPENDENCIES: - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - no_screenshot (from `.symlinks/plugins/no_screenshot/ios`) + - objective_c (from `.symlinks/plugins/objective_c/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - restart_app (from `.symlinks/plugins/restart_app/ios`) + - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) @@ -306,6 +315,7 @@ SPEC REPOS: - ScreenProtectorKit - SDWebImage - SDWebImageWebPCoder + - Sentry - sqlite3 - SwiftProtobuf @@ -352,6 +362,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/local_auth_darwin/darwin" no_screenshot: :path: ".symlinks/plugins/no_screenshot/ios" + objective_c: + :path: ".symlinks/plugins/objective_c/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -360,6 +372,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" restart_app: :path: ".symlinks/plugins/restart_app/ios" + sentry_flutter: + :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -408,6 +422,7 @@ SPEC CHECKSUMS: Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 no_screenshot: 6d183496405a3ab709a67a54e5cd0f639e94729e + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d @@ -416,6 +431,8 @@ SPEC CHECKSUMS: ScreenProtectorKit: 83a6281b02c7a5902ee6eac4f5045f674e902ae4 SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 + Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7 + sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 diff --git a/pubspec.yaml b/pubspec.yaml index 2222fd8..89a411c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.0.66+66 +version: 0.0.67+67 environment: sdk: ^3.6.0