From 322c51a7646da556e4c8a92924c5aa9ad237d872 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 09:44:48 +0100 Subject: [PATCH] Added documentation and feedback in snackbar --- .../views/main_menu/settings_view.dart | 97 ++++++++++++++++++- lib/services/data_transfer_service.dart | 56 +++++++---- 2 files changed, 133 insertions(+), 20 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index f0a530b..1a762ce 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -77,23 +77,37 @@ class _SettingsViewState extends State { onPressed: () async { final String json = await DataTransferService.getAppDataAsJson(context); - await DataTransferService.exportData( + final result = await DataTransferService.exportData( json, 'exported_data', ); + if (!context.mounted) return; + showExportSnackBar(context: context, result: result); }, ), SettingsListTile( title: 'Import data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => DataTransferService.importData(context), + onPressed: () async { + final result = await DataTransferService.importData( + context, + ); + if (!context.mounted) return; + showImportSnackBar(context: context, result: result); + }, ), SettingsListTile( title: 'Delete all data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => DataTransferService.deleteAllData(context), + onPressed: () { + DataTransferService.deleteAllData(context); + showSnackbar( + context: context, + message: 'Data successfully deleted', + ); + }, ), ], ), @@ -101,4 +115,81 @@ class _SettingsViewState extends State { ), ); } + + /// Displays a snackbar based on the import result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the import operation. + void showImportSnackBar({ + required BuildContext context, + required ImportResult result, + }) { + switch (result) { + case ImportResult.success: + showSnackbar(context: context, message: 'Data successfully imported'); + case ImportResult.invalidSchema: + showSnackbar(context: context, message: 'Invalid Schema'); + case ImportResult.fileReadError: + showSnackbar(context: context, message: 'Error reading file'); + case ImportResult.canceled: + showSnackbar(context: context, message: 'Import canceled'); + case ImportResult.formatException: + showSnackbar( + context: context, + message: 'Format Exception (see console)', + ); + case ImportResult.unknownException: + showSnackbar( + context: context, + message: 'Unknown Exception (see console)', + ); + } + } + + /// Displays a snackbar based on the export result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the export operation. + void showExportSnackBar({ + required BuildContext context, + required ExportResult result, + }) { + switch (result) { + case ExportResult.success: + showSnackbar(context: context, message: 'Data successfully exported'); + case ExportResult.canceled: + showSnackbar(context: context, message: 'Export canceled'); + case ExportResult.unknownException: + showSnackbar( + context: context, + message: 'Unknown Exception (see console)', + ); + } + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [context] The BuildContext to show the snackbar in. + /// [message] The message to display in the snackbar. + /// [duration] The duration for which the snackbar is displayed. + /// [action] An optional callback function to execute when the action button is pressed. + void showSnackbar({ + required BuildContext context, + required String message, + Duration duration = const Duration(seconds: 3), + VoidCallback? action, + }) { + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.onBoxColor, + duration: duration, + action: action != null + ? SnackBarAction(label: 'Rückgängig', onPressed: action) + : null, + ), + ); + } } diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 839c0e0..6eba1ee 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -11,6 +11,17 @@ import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; import 'package:provider/provider.dart'; +enum ImportResult { + success, + canceled, + fileReadError, + invalidSchema, + formatException, + unknownException, +} + +enum ExportResult { success, canceled, unknownException } + class DataTransferService { /// Deletes all data from the database. static Future deleteAllData(BuildContext context) async { @@ -18,9 +29,10 @@ class DataTransferService { await db.gameDao.deleteAllGames(); await db.groupDao.deleteAllGroups(); await db.playerDao.deleteAllPlayers(); - print('[deleteAllData] All data deleted'); } + /// Retrieves all application data and converts it to a JSON string. + /// Returns the JSON string representation of the data. static Future getAppDataAsJson(BuildContext context) async { final db = Provider.of(context, listen: false); final games = await db.gameDao.getAllGames(); @@ -38,23 +50,34 @@ class DataTransferService { } /// Exports the given JSON string to a file with the specified name. - static Future exportData(String jsonString, String fileName) async { + /// Returns an [ExportResult] indicating the outcome. + /// + /// [jsonString] The JSON string to be exported. + /// [fileName] The desired name for the exported file (without extension). + static Future exportData( + String jsonString, + String fileName, + ) async { try { final bytes = Uint8List.fromList(utf8.encode(jsonString)); - await FilePicker.platform.saveFile( + final path = await FilePicker.platform.saveFile( fileName: '$fileName.json', bytes: bytes, ); - return true; + if (path == null) { + return ExportResult.canceled; + } else { + return ExportResult.success; + } } catch (e, stack) { print('[exportData] $e'); print(stack); - return false; + return ExportResult.unknownException; } } /// Imports data from a selected JSON file into the database. - static Future importData(BuildContext context) async { + static Future importData(BuildContext context) async { final db = Provider.of(context, listen: false); final path = await FilePicker.platform.pickFiles( @@ -63,12 +86,14 @@ class DataTransferService { ); if (path == null) { - print('[importData] No file selected'); - return; + return ImportResult.canceled; } try { final jsonString = await _readFileContent(path.files.single); + if (jsonString == null) { + return ImportResult.fileReadError; + } if (await SettingsView.validateJsonSchema(jsonString)) { final Map jsonData = @@ -107,29 +132,26 @@ class DataTransferService { await db.gameDao.addGame(game: game); } } else { - print('[importData] Invalid JSON schema'); - return; + return ImportResult.invalidSchema; } - print('[importData] Data imported successfully'); - return; + return ImportResult.success; } on FormatException catch (e, stack) { print('[importData] FormatException'); print('[importData] $e'); print(stack); - return; + return ImportResult.formatException; } on Exception catch (e, stack) { print('[importData] Exception'); print('[importData] $e'); print(stack); - return; + return ImportResult.unknownException; } } /// Helper method to read file content from either bytes or path - static Future _readFileContent(PlatformFile file) async { + static Future _readFileContent(PlatformFile file) async { if (file.bytes != null) return utf8.decode(file.bytes!); if (file.path != null) return await File(file.path!).readAsString(); - - throw Exception('Die Datei hat keinen lesbaren Inhalt'); + return null; } }