diff --git a/wrestling_scoreboard_client/lib/l10n/app_de.arb b/wrestling_scoreboard_client/lib/l10n/app_de.arb index 30c1ce21..1999d0dc 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_de.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_de.arb @@ -15,6 +15,7 @@ "themeModeDark": "Dunkler Modus", "apiUrl": "Api-Url", "wsUrl": "Websocket-Url", + "database": "Datenbank", "exportDatabase": "Datenbank exportieren", "restoreDatabase": "Datenbank wiederherstellen", "restoreDefaultDatabase": "Standard-Datenbank wiederherstellen", @@ -55,6 +56,7 @@ "invalidParameterException": "Die Änderung war nicht erfolgreich, bitte überprüfe deine Eingabeparameter.", "warningBoutGenerate": "Diese Aktion überschreibt alle existierenden Kämpfe dieser Begegnung. Um einzelne Kämpfe zu bearbeiten, nutze die Seite zur Kampf-Bearbeitung. Bist du sicher, dass du fortfahren möchtest?", "retry": "Erneut versuchen", + "networkTimeout": "Netzwerk-Timeout", "date": "Datum", "place": "Ort", diff --git a/wrestling_scoreboard_client/lib/l10n/app_en.arb b/wrestling_scoreboard_client/lib/l10n/app_en.arb index 0ec62b38..2478d8a0 100644 --- a/wrestling_scoreboard_client/lib/l10n/app_en.arb +++ b/wrestling_scoreboard_client/lib/l10n/app_en.arb @@ -18,6 +18,7 @@ "themeModeDark": "Dark mode", "apiUrl": "Api-Url", "wsUrl": "Websocket-Url", + "database": "Database", "exportDatabase": "Export database", "restoreDatabase": "Restore database", "restoreDefaultDatabase": "Restore default database", @@ -58,6 +59,7 @@ "invalidParameterException": "The change was not successful, please check your input parameters.", "warningBoutGenerate": "This action overrides all existing bouts of this match. To edit single bouts, use the bout editing page. Are you sure, you want to continue?", "retry": "Retry", + "networkTimeout": "Network timeout", "date": "Date", "place": "Place", diff --git a/wrestling_scoreboard_client/lib/localization/duration.dart b/wrestling_scoreboard_client/lib/localization/duration.dart index e2f799d3..abd4822e 100644 --- a/wrestling_scoreboard_client/lib/localization/duration.dart +++ b/wrestling_scoreboard_client/lib/localization/duration.dart @@ -1,5 +1,9 @@ extension DurationLocalization on Duration { String formatMinutesAndSeconds() { - return '${inMinutes.remainder(60)}:${inSeconds.remainder(60).toString().padLeft(2, '0')}'; + return '$inMinutes:${inSeconds.remainder(60).toString().padLeft(2, '0')}'; + } + + String formatSecondsAndMilliseconds() { + return '$inSeconds.${inMilliseconds.remainder(1000).toString().padLeft(3, '0')}s'; } } diff --git a/wrestling_scoreboard_client/lib/provider/local_preferences.dart b/wrestling_scoreboard_client/lib/provider/local_preferences.dart index 82974b98..2778119a 100644 --- a/wrestling_scoreboard_client/lib/provider/local_preferences.dart +++ b/wrestling_scoreboard_client/lib/provider/local_preferences.dart @@ -9,6 +9,8 @@ class Preferences { static const keyApiUrl = 'api-url'; static const keyWsUrl = 'ws-url'; static const keyBellSound = 'bell-sound'; + + /// Network timeout in milliseconds. static const keyNetworkTimeout = 'network-timeout'; static final StreamController onChangeLocale = StreamController.broadcast(); @@ -32,6 +34,15 @@ class Preferences { } } + static Future setInt(String key, int? value) async { + final prefs = await SharedPreferences.getInstance(); + if (value != null) { + await prefs.setInt(key, value); + } else { + await prefs.remove(key); + } + } + static Future getString(String key) => SharedPreferences.getInstance().then((value) => value.getString(key)); static Future getInt(String key) => SharedPreferences.getInstance().then((value) => value.getInt(key)); diff --git a/wrestling_scoreboard_client/lib/view/screens/more/settings/settings.dart b/wrestling_scoreboard_client/lib/view/screens/more/settings/settings.dart index 2e7b78c0..d4c1881a 100644 --- a/wrestling_scoreboard_client/lib/view/screens/more/settings/settings.dart +++ b/wrestling_scoreboard_client/lib/view/screens/more/settings/settings.dart @@ -6,6 +6,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:wrestling_scoreboard_client/localization/duration.dart'; import 'package:wrestling_scoreboard_client/provider/local_preferences.dart'; import 'package:wrestling_scoreboard_client/provider/local_preferences_provider.dart'; import 'package:wrestling_scoreboard_client/provider/network_provider.dart'; @@ -129,115 +130,6 @@ class CustomSettingsScreen extends ConsumerWidget { ); }, ), - LoadingBuilder( - future: ref.watch(apiUrlNotifierProvider), - builder: (context, apiUrl) { - return LoadingBuilder( - future: ref.watch(webSocketUrlNotifierProvider), - builder: (context, wsUrl) { - return SettingsSection( - title: localizations.network, - action: TextButton( - onPressed: () { - apiUrl = Env.apiUrl.fromString(); - Preferences.setString(Preferences.keyApiUrl, apiUrl); - Preferences.onChangeApiUrl.add(apiUrl); - - wsUrl = Env.webSocketUrl.fromString(); - Preferences.setString(Preferences.keyWsUrl, wsUrl); - Preferences.onChangeWsUrlWebSocket.add(wsUrl); - }, - child: Text(localizations.reset), - ), - children: [ - ListTile( - subtitle: Text(apiUrl), - title: Text(localizations.apiUrl), - leading: const Icon(Icons.link), - onTap: () async { - final val = await showDialog( - context: context, - builder: (BuildContext context) { - return TextInputDialog(initialValue: apiUrl); - }, - ); - if (val != null) { - Preferences.onChangeApiUrl.add(val); - await Preferences.setString(Preferences.keyApiUrl, val); - } - }, - ), - ListTile( - leading: const Icon(Icons.link), - title: Text(localizations.wsUrl), - subtitle: Text(wsUrl), - onTap: () async { - final val = await showDialog( - context: context, - builder: (BuildContext context) { - return TextInputDialog(initialValue: wsUrl); - }, - ); - if (val != null) { - Preferences.onChangeWsUrlWebSocket.add(val); - await Preferences.setString(Preferences.keyWsUrl, val); - } - }, - ), - ListTile( - leading: const Icon(Icons.cloud_download), - title: Text(localizations.exportDatabase), - onTap: () async { - String? outputPath = await FilePicker.platform.saveFile( - fileName: - '${DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll(RegExp(r'\.[0-9]{3}'), '')}-' - 'PostgreSQL-wrestling_scoreboard-dump.sql', - ); - if (outputPath != null) { - final dataManager = await ref.read(dataManagerNotifierProvider); - final sqlString = await dataManager.exportDatabase(); - final outputFile = File(outputPath); - await outputFile.writeAsString(sqlString, encoding: const Utf8Codec()); - } - }, - ), - ListTile( - leading: const Icon(Icons.settings_backup_restore), - title: Text(localizations.resetDatabase), - onTap: () async { - final dataManager = await ref.read(dataManagerNotifierProvider); - await dataManager.resetDatabase(); - }, - ), - ListTile( - leading: const Icon(Icons.history), - title: Text(localizations.restoreDefaultDatabase), - onTap: () async { - final dataManager = await ref.read(dataManagerNotifierProvider); - await dataManager.restoreDefaultDatabase(); - }, - ), - ListTile( - leading: const Icon(Icons.cloud_upload), - title: Text(localizations.restoreDatabase), - onTap: () async { - FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['sql'], - ); - if (filePickerResult != null) { - File file = File(filePickerResult.files.single.path!); - final dataManager = await ref.read(dataManagerNotifierProvider); - await dataManager.restoreDatabase(await file.readAsString(encoding: const Utf8Codec())); - } - }, - ), - ], - ); - }, - ); - }, - ), LoadingBuilder( future: ref.watch(bellSoundNotifierProvider), builder: (context, bellSoundPath) { @@ -293,6 +185,148 @@ class CustomSettingsScreen extends ConsumerWidget { ); }, ), + LoadingBuilder( + future: ref.watch(networkTimeoutNotifierProvider), + builder: (context, networkTimeout) { + return LoadingBuilder( + future: ref.watch(apiUrlNotifierProvider), + builder: (context, apiUrl) { + return LoadingBuilder( + future: ref.watch(webSocketUrlNotifierProvider), + builder: (context, wsUrl) { + return SettingsSection( + title: localizations.network, + action: TextButton( + onPressed: () { + apiUrl = Env.apiUrl.fromString(); + Preferences.setString(Preferences.keyApiUrl, apiUrl); + Preferences.onChangeApiUrl.add(apiUrl); + + wsUrl = Env.webSocketUrl.fromString(); + Preferences.setString(Preferences.keyWsUrl, wsUrl); + Preferences.onChangeWsUrlWebSocket.add(wsUrl); + + const defaultNetworkTimeout = Duration(seconds: 10); + Preferences.setInt(Preferences.keyNetworkTimeout, defaultNetworkTimeout.inMilliseconds); + Preferences.onChangeNetworkTimeout.add(defaultNetworkTimeout); + }, + child: Text(localizations.reset), + ), + children: [ + ListTile( + subtitle: Text(apiUrl), + title: Text(localizations.apiUrl), + leading: const Icon(Icons.link), + onTap: () async { + final val = await showDialog( + context: context, + builder: (BuildContext context) { + return TextInputDialog(initialValue: apiUrl); + }, + ); + if (val != null) { + Preferences.onChangeApiUrl.add(val); + await Preferences.setString(Preferences.keyApiUrl, val); + } + }, + ), + ListTile( + leading: const Icon(Icons.link), + title: Text(localizations.wsUrl), + subtitle: Text(wsUrl), + onTap: () async { + final val = await showDialog( + context: context, + builder: (BuildContext context) { + return TextInputDialog(initialValue: wsUrl); + }, + ); + if (val != null) { + Preferences.onChangeWsUrlWebSocket.add(val); + await Preferences.setString(Preferences.keyWsUrl, val); + } + }, + ), + ListTile( + leading: const Icon(Icons.running_with_errors), + title: Text(localizations.networkTimeout), + subtitle: Text(networkTimeout.formatSecondsAndMilliseconds()), + onTap: () async { + final val = await showDialog( + context: context, + builder: (BuildContext context) { + return DurationDialog( + initialValue: networkTimeout, + maxValue: const Duration(hours: 1), + ); + }, + ); + if (val != null) { + Preferences.onChangeNetworkTimeout.add(val); + await Preferences.setInt(Preferences.keyNetworkTimeout, val.inMilliseconds); + } + }, + ), + ], + ); + }, + ); + }, + ); + }), + SettingsSection( + title: localizations.database, + children: [ + ListTile( + leading: const Icon(Icons.cloud_download), + title: Text(localizations.exportDatabase), + onTap: () async { + String? outputPath = await FilePicker.platform.saveFile( + fileName: + '${DateTime.now().toIso8601String().replaceAll(':', '-').replaceAll(RegExp(r'\.[0-9]{3}'), '')}-' + 'PostgreSQL-wrestling_scoreboard-dump.sql', + ); + if (outputPath != null) { + final dataManager = await ref.read(dataManagerNotifierProvider); + final sqlString = await dataManager.exportDatabase(); + final outputFile = File(outputPath); + await outputFile.writeAsString(sqlString, encoding: const Utf8Codec()); + } + }, + ), + ListTile( + leading: const Icon(Icons.settings_backup_restore), + title: Text(localizations.resetDatabase), + onTap: () async { + final dataManager = await ref.read(dataManagerNotifierProvider); + await dataManager.resetDatabase(); + }, + ), + ListTile( + leading: const Icon(Icons.history), + title: Text(localizations.restoreDefaultDatabase), + onTap: () async { + final dataManager = await ref.read(dataManagerNotifierProvider); + await dataManager.restoreDefaultDatabase(); + }, + ), + ListTile( + leading: const Icon(Icons.cloud_upload), + title: Text(localizations.restoreDatabase), + onTap: () async { + FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['sql'], + ); + if (filePickerResult != null) { + File file = File(filePickerResult.files.single.path!); + final dataManager = await ref.read(dataManagerNotifierProvider); + await dataManager.restoreDatabase(await file.readAsString(encoding: const Utf8Codec())); + } + }, + ), + ], + ), ], ), );