From 26af9460a6a28ee5ce5cf9b5e2725fe1b94cab0a Mon Sep 17 00:00:00 2001 From: otsmr Date: Tue, 29 Apr 2025 20:29:58 +0200 Subject: [PATCH] fix #129 --- lib/src/localization/app_de.arb | 5 + lib/src/localization/app_en.arb | 10 + .../generated/app_localizations.dart | 30 +++ .../generated/app_localizations_de.dart | 15 ++ .../generated/app_localizations_en.dart | 15 ++ lib/src/model/json/userdata.dart | 2 + lib/src/model/json/userdata.g.dart | 10 +- lib/src/providers/api/media_received.dart | 48 +++- lib/src/utils/misc.dart | 11 - lib/src/views/chats/media_viewer_view.dart | 11 +- .../views/components/better_list_title.dart | 19 +- .../views/settings/data_and_storage_view.dart | 213 +++++++++++++----- lib/src/views/settings/help_view.dart | 3 - .../views/settings/settings_main_view.dart | 4 +- 14 files changed, 306 insertions(+), 90 deletions(-) diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index 9e26f8d..b344bd6 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -84,6 +84,11 @@ "settingsTitle": "Einstellungen", "settingsChats": "Chats", "settingsStorageData": "Daten und Speicher", + "settingsStorageDataStoreInGTitle": "In der Galerie speichern", + "settingsStorageDataStoreInGSubtitle": "Speichere Bilder zusätzlich in der Systemgalerie.", + "settingsStorageDataMediaAutoDownload": "Automatischer Mediendownload", + "settingsStorageDataAutoDownMobile": "Bei Nutzung mobiler Daten", + "settingsStorageDataAutoDownWifi": "Bei Nutzung von WLAN", "settingsPreSelectedReactions": "Vorgewählte Reaktions-Emojis", "settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.", "settingsProfile": "Profil", diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index 3ed0579..c9cefae 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -148,6 +148,16 @@ "@settingsProfile": {}, "settingsStorageData": "Data and storage", "@settingsStorageData": {}, + "settingsStorageDataStoreInGTitle": "Store in Gallery", + "@settingsStorageDataStoreInGTitle": {}, + "settingsStorageDataStoreInGSubtitle": "Store saved images additional in the systems gallery.", + "@settingsStorageDataStoreInGSubtitle": {}, + "settingsStorageDataMediaAutoDownload": "Media auto-download", + "@settingsStorageDataMediaAutoDownload": {}, + "settingsStorageDataAutoDownMobile": "When using mobile data", + "@settingsStorageDataAutoDownMobile": {}, + "settingsStorageDataAutoDownWifi": "When using WI-FI", + "@settingsStorageDataAutoDownWifi": {}, "settingsProfileCustomizeAvatar": "Customize your avatar", "@settingsProfileCustomizeAvatar": {}, "settingsProfileEditDisplayName": "Displayname", diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index fd68d6c..96b439a 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -527,6 +527,36 @@ abstract class AppLocalizations { /// **'Data and storage'** String get settingsStorageData; + /// No description provided for @settingsStorageDataStoreInGTitle. + /// + /// In en, this message translates to: + /// **'Store in Gallery'** + String get settingsStorageDataStoreInGTitle; + + /// No description provided for @settingsStorageDataStoreInGSubtitle. + /// + /// In en, this message translates to: + /// **'Store saved images additional in the systems gallery.'** + String get settingsStorageDataStoreInGSubtitle; + + /// No description provided for @settingsStorageDataMediaAutoDownload. + /// + /// In en, this message translates to: + /// **'Media auto-download'** + String get settingsStorageDataMediaAutoDownload; + + /// No description provided for @settingsStorageDataAutoDownMobile. + /// + /// In en, this message translates to: + /// **'When using mobile data'** + String get settingsStorageDataAutoDownMobile; + + /// No description provided for @settingsStorageDataAutoDownWifi. + /// + /// In en, this message translates to: + /// **'When using WI-FI'** + String get settingsStorageDataAutoDownWifi; + /// No description provided for @settingsProfileCustomizeAvatar. /// /// In en, this message translates to: diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index 69183a3..725b367 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -226,6 +226,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settingsStorageData => 'Daten und Speicher'; + @override + String get settingsStorageDataStoreInGTitle => 'In der Galerie speichern'; + + @override + String get settingsStorageDataStoreInGSubtitle => 'Speichere Bilder zusätzlich in der Systemgalerie.'; + + @override + String get settingsStorageDataMediaAutoDownload => 'Automatischer Mediendownload'; + + @override + String get settingsStorageDataAutoDownMobile => 'Bei Nutzung mobiler Daten'; + + @override + String get settingsStorageDataAutoDownWifi => 'Bei Nutzung von WLAN'; + @override String get settingsProfileCustomizeAvatar => 'Avatar anpassen'; diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index a8a713b..06d0438 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -226,6 +226,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settingsStorageData => 'Data and storage'; + @override + String get settingsStorageDataStoreInGTitle => 'Store in Gallery'; + + @override + String get settingsStorageDataStoreInGSubtitle => 'Store saved images additional in the systems gallery.'; + + @override + String get settingsStorageDataMediaAutoDownload => 'Media auto-download'; + + @override + String get settingsStorageDataAutoDownMobile => 'When using mobile data'; + + @override + String get settingsStorageDataAutoDownWifi => 'When using WI-FI'; + @override String get settingsProfileCustomizeAvatar => 'Customize your avatar'; diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 1ea28c5..f3b33a4 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -22,6 +22,8 @@ class UserData { bool? useHighQuality; List? preSelectedEmojies; ThemeMode? themeMode; + Map>? autoDownloadOptions; + bool? storeMediaFilesInGallery; final int userId; diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index b3cfc71..bd02f3b 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -19,7 +19,13 @@ UserData _$UserDataFromJson(Map json) => UserData( ..preSelectedEmojies = (json['preSelectedEmojies'] as List?) ?.map((e) => e as String) .toList() - ..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']); + ..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) + ..autoDownloadOptions = + (json['autoDownloadOptions'] as Map?)?.map( + (k, e) => + MapEntry(k, (e as List).map((e) => e as String).toList()), + ) + ..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool?; Map _$UserDataToJson(UserData instance) => { 'username': instance.username, @@ -31,6 +37,8 @@ Map _$UserDataToJson(UserData instance) => { 'useHighQuality': instance.useHighQuality, 'preSelectedEmojies': instance.preSelectedEmojies, 'themeMode': _$ThemeModeEnumMap[instance.themeMode], + 'autoDownloadOptions': instance.autoDownloadOptions, + 'storeMediaFilesInGallery': instance.storeMediaFilesInGallery, 'userId': instance.userId, }; diff --git a/lib/src/providers/api/media_received.dart b/lib/src/providers/api/media_received.dart index 19f4361..5d9769f 100644 --- a/lib/src/providers/api/media_received.dart +++ b/lib/src/providers/api/media_received.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:io'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:drift/drift.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -8,7 +9,6 @@ import 'package:twonly/src/database/twonly_database.dart'; import 'package:twonly/src/database/tables/messages_table.dart'; import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/providers/api/media_send.dart'; -import 'package:twonly/src/utils/misc.dart'; import 'dart:typed_data'; import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:logging/logging.dart'; @@ -17,6 +17,7 @@ import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart' as client; import 'package:twonly/src/model/protobuf/api/error.pb.dart'; import 'package:twonly/src/model/protobuf/api/server_to_client.pbserver.dart'; +import 'package:twonly/src/utils/storage.dart'; Map downloadStartedForMediaReceived = {}; @@ -31,6 +32,51 @@ Future tryDownloadAllMediaFiles() async { } } +enum DownloadMediaTypes { + video, + image, +} + +Map> defaultAutoDownloadOptions = { + ConnectivityResult.mobile.name: [], + ConnectivityResult.wifi.name: [ + DownloadMediaTypes.video.name, + DownloadMediaTypes.image.name + ] +}; + +Future isAllowedToDownload(bool isVideo) async { + final List connectivityResult = + await (Connectivity().checkConnectivity()); + + final user = await getUser(); + final options = user!.autoDownloadOptions ?? defaultAutoDownloadOptions; + + if (connectivityResult.contains(ConnectivityResult.mobile)) { + if (isVideo) { + if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.video.name)) { + return true; + } + } else if (options[ConnectivityResult.mobile.name]! + .contains(DownloadMediaTypes.image.name)) { + return true; + } + } + if (connectivityResult.contains(ConnectivityResult.wifi)) { + if (isVideo) { + if (options[ConnectivityResult.wifi.name]! + .contains(DownloadMediaTypes.video.name)) { + return true; + } + } else if (options[ConnectivityResult.wifi.name]! + .contains(DownloadMediaTypes.image.name)) { + return true; + } + } + return false; +} + Future startDownloadMedia(Message message, bool force) async { if (message.contentJson == null) return; if (downloadStartedForMediaReceived[message.messageId] != null) { diff --git a/lib/src/utils/misc.dart b/lib/src/utils/misc.dart index b7401f8..5b57f5f 100644 --- a/lib/src/utils/misc.dart +++ b/lib/src/utils/misc.dart @@ -1,6 +1,5 @@ import 'dart:io'; import 'dart:math'; -import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -161,16 +160,6 @@ Future authenticateUser(String localizedReason, return false; } -Future isAllowedToDownload(bool isVideo) async { - final List connectivityResult = - await (Connectivity().checkConnectivity()); - if (connectivityResult.contains(ConnectivityResult.mobile)) { - Logger("tryDownloadMedia").info("abort download over mobile connection"); - return false; - } - return true; -} - void setupLogger() { Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; Logger.root.onRecord.listen((record) async { diff --git a/lib/src/views/chats/media_viewer_view.dart b/lib/src/views/chats/media_viewer_view.dart index 101508e..14f0cab 100644 --- a/lib/src/views/chats/media_viewer_view.dart +++ b/lib/src/views/chats/media_viewer_view.dart @@ -298,12 +298,13 @@ class _MediaViewerViewState extends State { setState(() { imageSaved = true; }); - final res = await saveImageToGallery(imageBytes!); - if (res == null) { - setState(() { - imageSaving = false; - }); + final user = await getUser(); + if (user != null && (user.storeMediaFilesInGallery ?? true)) { + await saveImageToGallery(imageBytes!); } + setState(() { + imageSaving = false; + }); } Widget bottomNavigation() { diff --git a/lib/src/views/components/better_list_title.dart b/lib/src/views/components/better_list_title.dart index 67ed106..4ebfb27 100644 --- a/lib/src/views/components/better_list_title.dart +++ b/lib/src/views/components/better_list_title.dart @@ -7,15 +7,16 @@ class BetterListTile extends StatelessWidget { final Widget? subtitle; final Color? color; final VoidCallback onTap; + final double iconSize; - const BetterListTile({ - super.key, - required this.icon, - required this.text, - this.color, - this.subtitle, - required this.onTap, - }); + const BetterListTile( + {super.key, + required this.icon, + required this.text, + this.color, + this.subtitle, + required this.onTap, + this.iconSize = 20}); @override Widget build(BuildContext context) { @@ -27,7 +28,7 @@ class BetterListTile extends StatelessWidget { ), child: FaIcon( icon, - size: 20, + size: iconSize, color: color, ), ), diff --git a/lib/src/views/settings/data_and_storage_view.dart b/lib/src/views/settings/data_and_storage_view.dart index 892643a..67de151 100644 --- a/lib/src/views/settings/data_and_storage_view.dart +++ b/lib/src/views/settings/data_and_storage_view.dart @@ -1,60 +1,58 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; -import 'package:twonly/src/views/components/better_list_title.dart'; -import 'package:twonly/src/views/components/better_text.dart'; -import 'package:twonly/src/views/components/radio_button.dart'; -import 'package:twonly/src/providers/settings_change_provider.dart'; +import 'package:twonly/src/providers/api/media_received.dart'; +import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/misc.dart'; -class DataAndStorageView extends StatelessWidget { +class DataAndStorageView extends StatefulWidget { const DataAndStorageView({super.key}); - void _showSelectThemeMode(BuildContext context) async { - ThemeMode? selectedValue = context.read().themeMode; + @override + State createState() => _DataAndStorageViewState(); +} +class _DataAndStorageViewState extends State { + Map> autoDownloadOptions = defaultAutoDownloadOptions; + bool storeMediaFilesInGallery = true; + + @override + void initState() { + super.initState(); + initAsync(); + } + + Future initAsync() async { + final user = await getUser(); + if (user == null) return; + setState(() { + autoDownloadOptions = + user.autoDownloadOptions ?? defaultAutoDownloadOptions; + storeMediaFilesInGallery = user.storeMediaFilesInGallery ?? true; + }); + } + + void showAutoDownloadOptions( + BuildContext context, ConnectivityResult connectionMode) async { await showDialog( context: context, builder: (BuildContext context) { - return AlertDialog( - title: Text(context.lang.settingsAppearanceTheme), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - RadioButton( - value: ThemeMode.system, - groupValue: selectedValue, - label: 'System default', - onChanged: (ThemeMode? value) { - selectedValue = value; - Navigator.of(context).pop(); - }, - ), - RadioButton( - value: ThemeMode.light, - groupValue: selectedValue, - label: 'Light', - onChanged: (ThemeMode? value) { - selectedValue = value; - Navigator.of(context).pop(); - }, - ), - RadioButton( - value: ThemeMode.dark, - groupValue: selectedValue, - label: 'Dark', - onChanged: (ThemeMode? value) { - selectedValue = value; - Navigator.of(context).pop(); - }, - ), - ], - ), + return AutoDownloadOptionsDialog( + autoDownloadOptions: autoDownloadOptions, + connectionMode: connectionMode, + onUpdate: () async { + await initAsync(); + }, ); }, ); - if (selectedValue != null && context.mounted) { - context.read().updateThemeMode(selectedValue); - } + } + + void toggleStoreInGallery() async { + final user = await getUser(); + if (user == null) return; + user.storeMediaFilesInGallery = !storeMediaFilesInGallery; + await updateUser(user); + initAsync(); } @override @@ -65,26 +63,40 @@ class DataAndStorageView extends StatelessWidget { ), body: ListView( children: [ - BetterText(text: "Media auto-download"), ListTile( - title: Text("When using mobile data"), - subtitle: Text("Images", style: TextStyle(color: Colors.grey)), + title: Text(context.lang.settingsStorageDataStoreInGTitle), + subtitle: Text(context.lang.settingsStorageDataStoreInGSubtitle), + onTap: toggleStoreInGallery, + trailing: Checkbox( + value: storeMediaFilesInGallery, + onChanged: (a) => toggleStoreInGallery(), + ), + ), + Divider(), + ListTile( + title: Text( + context.lang.settingsStorageDataMediaAutoDownload, + style: TextStyle(fontSize: 13), + ), + ), + ListTile( + title: Text(context.lang.settingsStorageDataAutoDownMobile), + subtitle: Text( + autoDownloadOptions[ConnectivityResult.mobile.name]!.join(", "), + style: TextStyle(color: Colors.grey), + ), onTap: () { - _showSelectThemeMode(context); + showAutoDownloadOptions(context, ConnectivityResult.mobile); }, ), ListTile( - title: Text("When using Wi-Fi"), - subtitle: Text("Images", style: TextStyle(color: Colors.grey)), + title: Text(context.lang.settingsStorageDataAutoDownWifi), + subtitle: Text( + autoDownloadOptions[ConnectivityResult.wifi.name]!.join(", "), + style: TextStyle(color: Colors.grey), + ), onTap: () { - _showSelectThemeMode(context); - }, - ), - ListTile( - title: Text("When using roaming"), - subtitle: Text("Images", style: TextStyle(color: Colors.grey)), - onTap: () { - _showSelectThemeMode(context); + showAutoDownloadOptions(context, ConnectivityResult.wifi); }, ), ], @@ -92,3 +104,86 @@ class DataAndStorageView extends StatelessWidget { ); } } + +class AutoDownloadOptionsDialog extends StatefulWidget { + final Map> autoDownloadOptions; + final ConnectivityResult connectionMode; + final Function() onUpdate; + + const AutoDownloadOptionsDialog({ + super.key, + required this.autoDownloadOptions, + required this.connectionMode, + required this.onUpdate, + }); + + @override + State createState() => + _AutoDownloadOptionsDialogState(); +} + +class _AutoDownloadOptionsDialogState extends State { + late Map> autoDownloadOptions; + + @override + void initState() { + super.initState(); + autoDownloadOptions = widget.autoDownloadOptions; + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(context.lang.settingsStorageDataMediaAutoDownload), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CheckboxListTile( + title: Text('Image'), + value: autoDownloadOptions[widget.connectionMode.name]! + .contains(DownloadMediaTypes.image.name), + onChanged: (bool? value) async { + await _updateAutoDownloadSetting(DownloadMediaTypes.image, value); + }, + ), + CheckboxListTile( + title: Text('Video'), + value: autoDownloadOptions[widget.connectionMode.name]! + .contains(DownloadMediaTypes.video.name), + onChanged: (bool? value) async { + await _updateAutoDownloadSetting(DownloadMediaTypes.video, value); + }, + ), + ], + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(context.lang.close), + ), + ], + ); + } + + Future _updateAutoDownloadSetting( + DownloadMediaTypes type, bool? value) async { + if (value == null) return; + + // Update the autoDownloadOptions based on the checkbox state + autoDownloadOptions[widget.connectionMode.name]! + .removeWhere((element) => element == type.name); + if (value) { + autoDownloadOptions[widget.connectionMode.name]!.add(type.name); + } + + // Call the onUpdate callback to notify the parent widget + final user = await getUser(); + if (user == null) return; + user.autoDownloadOptions = autoDownloadOptions; + await updateUser(user); + widget.onUpdate(); + setState(() {}); + } +} diff --git a/lib/src/views/settings/help_view.dart b/lib/src/views/settings/help_view.dart index 614b801..fe3b3bb 100644 --- a/lib/src/views/settings/help_view.dart +++ b/lib/src/views/settings/help_view.dart @@ -25,7 +25,6 @@ class HelpView extends StatelessWidget { FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), ), Divider(), - FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { @@ -76,8 +75,6 @@ class HelpView extends StatelessWidget { style: TextStyle(color: Colors.grey, fontSize: 13), ), ), - - // ], )); } diff --git a/lib/src/views/settings/settings_main_view.dart b/lib/src/views/settings/settings_main_view.dart index af3e31b..57596a8 100644 --- a/lib/src/views/settings/settings_main_view.dart +++ b/lib/src/views/settings/settings_main_view.dart @@ -8,6 +8,7 @@ import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/settings/account_view.dart'; import 'package:twonly/src/views/settings/appearance_view.dart'; import 'package:twonly/src/views/settings/chat/chat_settings_view.dart'; +import 'package:twonly/src/views/settings/data_and_storage_view.dart'; import 'package:twonly/src/views/settings/notification_view.dart'; import 'package:twonly/src/views/settings/profile/profile_view.dart'; import 'package:twonly/src/views/settings/help_view.dart'; @@ -153,11 +154,12 @@ class _SettingsMainViewState extends State { ), BetterListTile( icon: FontAwesomeIcons.chartPie, + iconSize: 15, text: context.lang.settingsStorageData, onTap: () async { Navigator.push(context, MaterialPageRoute(builder: (context) { - return NotificationView(); + return DataAndStorageView(); })); }, ),