This commit is contained in:
otsmr 2025-04-29 20:29:58 +02:00
parent 580dc3c3cf
commit 26af9460a6
14 changed files with 306 additions and 90 deletions

View file

@ -84,6 +84,11 @@
"settingsTitle": "Einstellungen", "settingsTitle": "Einstellungen",
"settingsChats": "Chats", "settingsChats": "Chats",
"settingsStorageData": "Daten und Speicher", "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", "settingsPreSelectedReactions": "Vorgewählte Reaktions-Emojis",
"settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.", "settingsPreSelectedReactionsError": "Es können maximal 12 Reaktionen ausgewählt werden.",
"settingsProfile": "Profil", "settingsProfile": "Profil",

View file

@ -148,6 +148,16 @@
"@settingsProfile": {}, "@settingsProfile": {},
"settingsStorageData": "Data and storage", "settingsStorageData": "Data and storage",
"@settingsStorageData": {}, "@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": "Customize your avatar",
"@settingsProfileCustomizeAvatar": {}, "@settingsProfileCustomizeAvatar": {},
"settingsProfileEditDisplayName": "Displayname", "settingsProfileEditDisplayName": "Displayname",

View file

@ -527,6 +527,36 @@ abstract class AppLocalizations {
/// **'Data and storage'** /// **'Data and storage'**
String get settingsStorageData; 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. /// No description provided for @settingsProfileCustomizeAvatar.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View file

@ -226,6 +226,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settingsStorageData => 'Daten und Speicher'; 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 @override
String get settingsProfileCustomizeAvatar => 'Avatar anpassen'; String get settingsProfileCustomizeAvatar => 'Avatar anpassen';

View file

@ -226,6 +226,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settingsStorageData => 'Data and storage'; 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 @override
String get settingsProfileCustomizeAvatar => 'Customize your avatar'; String get settingsProfileCustomizeAvatar => 'Customize your avatar';

View file

@ -22,6 +22,8 @@ class UserData {
bool? useHighQuality; bool? useHighQuality;
List<String>? preSelectedEmojies; List<String>? preSelectedEmojies;
ThemeMode? themeMode; ThemeMode? themeMode;
Map<String, List<String>>? autoDownloadOptions;
bool? storeMediaFilesInGallery;
final int userId; final int userId;

View file

@ -19,7 +19,13 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?) ..preSelectedEmojies = (json['preSelectedEmojies'] as List<dynamic>?)
?.map((e) => e as String) ?.map((e) => e as String)
.toList() .toList()
..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']); ..themeMode = $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode'])
..autoDownloadOptions =
(json['autoDownloadOptions'] as Map<String, dynamic>?)?.map(
(k, e) =>
MapEntry(k, (e as List<dynamic>).map((e) => e as String).toList()),
)
..storeMediaFilesInGallery = json['storeMediaFilesInGallery'] as bool?;
Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'username': instance.username, 'username': instance.username,
@ -31,6 +37,8 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'useHighQuality': instance.useHighQuality, 'useHighQuality': instance.useHighQuality,
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
'themeMode': _$ThemeModeEnumMap[instance.themeMode], 'themeMode': _$ThemeModeEnumMap[instance.themeMode],
'autoDownloadOptions': instance.autoDownloadOptions,
'storeMediaFilesInGallery': instance.storeMediaFilesInGallery,
'userId': instance.userId, 'userId': instance.userId,
}; };

View file

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.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/database/tables/messages_table.dart';
import 'package:twonly/src/model/json/message.dart'; import 'package:twonly/src/model/json/message.dart';
import 'package:twonly/src/providers/api/media_send.dart'; import 'package:twonly/src/providers/api/media_send.dart';
import 'package:twonly/src/utils/misc.dart';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:cryptography_plus/cryptography_plus.dart'; import 'package:cryptography_plus/cryptography_plus.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
@ -17,6 +17,7 @@ import 'package:twonly/src/model/protobuf/api/client_to_server.pb.dart'
as client; as client;
import 'package:twonly/src/model/protobuf/api/error.pb.dart'; 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/model/protobuf/api/server_to_client.pbserver.dart';
import 'package:twonly/src/utils/storage.dart';
Map<int, DateTime> downloadStartedForMediaReceived = {}; Map<int, DateTime> downloadStartedForMediaReceived = {};
@ -31,6 +32,51 @@ Future tryDownloadAllMediaFiles() async {
} }
} }
enum DownloadMediaTypes {
video,
image,
}
Map<String, List<String>> defaultAutoDownloadOptions = {
ConnectivityResult.mobile.name: [],
ConnectivityResult.wifi.name: [
DownloadMediaTypes.video.name,
DownloadMediaTypes.image.name
]
};
Future<bool> isAllowedToDownload(bool isVideo) async {
final List<ConnectivityResult> 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 { Future startDownloadMedia(Message message, bool force) async {
if (message.contentJson == null) return; if (message.contentJson == null) return;
if (downloadStartedForMediaReceived[message.messageId] != null) { if (downloadStartedForMediaReceived[message.messageId] != null) {

View file

@ -1,6 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -161,16 +160,6 @@ Future<bool> authenticateUser(String localizedReason,
return false; return false;
} }
Future<bool> isAllowedToDownload(bool isVideo) async {
final List<ConnectivityResult> connectivityResult =
await (Connectivity().checkConnectivity());
if (connectivityResult.contains(ConnectivityResult.mobile)) {
Logger("tryDownloadMedia").info("abort download over mobile connection");
return false;
}
return true;
}
void setupLogger() { void setupLogger() {
Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
Logger.root.onRecord.listen((record) async { Logger.root.onRecord.listen((record) async {

View file

@ -298,13 +298,14 @@ class _MediaViewerViewState extends State<MediaViewerView> {
setState(() { setState(() {
imageSaved = true; imageSaved = true;
}); });
final res = await saveImageToGallery(imageBytes!); final user = await getUser();
if (res == null) { if (user != null && (user.storeMediaFilesInGallery ?? true)) {
await saveImageToGallery(imageBytes!);
}
setState(() { setState(() {
imageSaving = false; imageSaving = false;
}); });
} }
}
Widget bottomNavigation() { Widget bottomNavigation() {
return Row( return Row(

View file

@ -7,15 +7,16 @@ class BetterListTile extends StatelessWidget {
final Widget? subtitle; final Widget? subtitle;
final Color? color; final Color? color;
final VoidCallback onTap; final VoidCallback onTap;
final double iconSize;
const BetterListTile({ const BetterListTile(
super.key, {super.key,
required this.icon, required this.icon,
required this.text, required this.text,
this.color, this.color,
this.subtitle, this.subtitle,
required this.onTap, required this.onTap,
}); this.iconSize = 20});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -27,7 +28,7 @@ class BetterListTile extends StatelessWidget {
), ),
child: FaIcon( child: FaIcon(
icon, icon,
size: 20, size: iconSize,
color: color, color: color,
), ),
), ),

View file

@ -1,60 +1,58 @@
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:twonly/src/providers/api/media_received.dart';
import 'package:twonly/src/views/components/better_list_title.dart'; import 'package:twonly/src/utils/storage.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/utils/misc.dart'; import 'package:twonly/src/utils/misc.dart';
class DataAndStorageView extends StatelessWidget { class DataAndStorageView extends StatefulWidget {
const DataAndStorageView({super.key}); const DataAndStorageView({super.key});
void _showSelectThemeMode(BuildContext context) async { @override
ThemeMode? selectedValue = context.read<SettingsChangeProvider>().themeMode; State<DataAndStorageView> createState() => _DataAndStorageViewState();
}
class _DataAndStorageViewState extends State<DataAndStorageView> {
Map<String, List<String>> 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( await showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AutoDownloadOptionsDialog(
title: Text(context.lang.settingsAppearanceTheme), autoDownloadOptions: autoDownloadOptions,
content: Column( connectionMode: connectionMode,
mainAxisSize: MainAxisSize.min, onUpdate: () async {
children: [ await initAsync();
RadioButton<ThemeMode>(
value: ThemeMode.system,
groupValue: selectedValue,
label: 'System default',
onChanged: (ThemeMode? value) {
selectedValue = value;
Navigator.of(context).pop();
}, },
),
RadioButton<ThemeMode>(
value: ThemeMode.light,
groupValue: selectedValue,
label: 'Light',
onChanged: (ThemeMode? value) {
selectedValue = value;
Navigator.of(context).pop();
},
),
RadioButton<ThemeMode>(
value: ThemeMode.dark,
groupValue: selectedValue,
label: 'Dark',
onChanged: (ThemeMode? value) {
selectedValue = value;
Navigator.of(context).pop();
},
),
],
),
); );
}, },
); );
if (selectedValue != null && context.mounted) {
context.read<SettingsChangeProvider>().updateThemeMode(selectedValue);
} }
void toggleStoreInGallery() async {
final user = await getUser();
if (user == null) return;
user.storeMediaFilesInGallery = !storeMediaFilesInGallery;
await updateUser(user);
initAsync();
} }
@override @override
@ -65,26 +63,40 @@ class DataAndStorageView extends StatelessWidget {
), ),
body: ListView( body: ListView(
children: [ children: [
BetterText(text: "Media auto-download"),
ListTile( ListTile(
title: Text("When using mobile data"), title: Text(context.lang.settingsStorageDataStoreInGTitle),
subtitle: Text("Images", style: TextStyle(color: Colors.grey)), 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: () { onTap: () {
_showSelectThemeMode(context); showAutoDownloadOptions(context, ConnectivityResult.mobile);
}, },
), ),
ListTile( ListTile(
title: Text("When using Wi-Fi"), title: Text(context.lang.settingsStorageDataAutoDownWifi),
subtitle: Text("Images", style: TextStyle(color: Colors.grey)), subtitle: Text(
onTap: () { autoDownloadOptions[ConnectivityResult.wifi.name]!.join(", "),
_showSelectThemeMode(context); style: TextStyle(color: Colors.grey),
},
), ),
ListTile(
title: Text("When using roaming"),
subtitle: Text("Images", style: TextStyle(color: Colors.grey)),
onTap: () { onTap: () {
_showSelectThemeMode(context); showAutoDownloadOptions(context, ConnectivityResult.wifi);
}, },
), ),
], ],
@ -92,3 +104,86 @@ class DataAndStorageView extends StatelessWidget {
); );
} }
} }
class AutoDownloadOptionsDialog extends StatefulWidget {
final Map<String, List<String>> autoDownloadOptions;
final ConnectivityResult connectionMode;
final Function() onUpdate;
const AutoDownloadOptionsDialog({
super.key,
required this.autoDownloadOptions,
required this.connectionMode,
required this.onUpdate,
});
@override
State<AutoDownloadOptionsDialog> createState() =>
_AutoDownloadOptionsDialogState();
}
class _AutoDownloadOptionsDialogState extends State<AutoDownloadOptionsDialog> {
late Map<String, List<String>> 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<void> _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(() {});
}
}

View file

@ -25,7 +25,6 @@ class HelpView extends StatelessWidget {
FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15), FaIcon(FontAwesomeIcons.arrowUpRightFromSquare, size: 15),
), ),
Divider(), Divider(),
FutureBuilder( FutureBuilder(
future: PackageInfo.fromPlatform(), future: PackageInfo.fromPlatform(),
builder: (context, snap) { builder: (context, snap) {
@ -76,8 +75,6 @@ class HelpView extends StatelessWidget {
style: TextStyle(color: Colors.grey, fontSize: 13), style: TextStyle(color: Colors.grey, fontSize: 13),
), ),
), ),
//
], ],
)); ));
} }

View file

@ -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/account_view.dart';
import 'package:twonly/src/views/settings/appearance_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/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/notification_view.dart';
import 'package:twonly/src/views/settings/profile/profile_view.dart'; import 'package:twonly/src/views/settings/profile/profile_view.dart';
import 'package:twonly/src/views/settings/help_view.dart'; import 'package:twonly/src/views/settings/help_view.dart';
@ -153,11 +154,12 @@ class _SettingsMainViewState extends State<SettingsMainView> {
), ),
BetterListTile( BetterListTile(
icon: FontAwesomeIcons.chartPie, icon: FontAwesomeIcons.chartPie,
iconSize: 15,
text: context.lang.settingsStorageData, text: context.lang.settingsStorageData,
onTap: () async { onTap: () async {
Navigator.push(context, Navigator.push(context,
MaterialPageRoute(builder: (context) { MaterialPageRoute(builder: (context) {
return NotificationView(); return DataAndStorageView();
})); }));
}, },
), ),