From f2a749cb0fe7b43f6b232437f8487c43e07f3e3c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 19 Nov 2025 00:24:08 +0100 Subject: [PATCH] First version of settings view --- .../views/main_menu/settings_view.dart | 218 +++++++++++++++--- pubspec.yaml | 8 +- 2 files changed, 192 insertions(+), 34 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index b05ae8d..2a1d193 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -1,10 +1,55 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'dart:convert'; +import 'dart:io'; -class SettingsView extends StatelessWidget { +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'package:json_schema/json_schema.dart'; +import 'package:provider/provider.dart'; + +class SettingsView extends StatefulWidget { const SettingsView({super.key}); + @override + State createState() => _SettingsViewState(); + + /// Helper method to read file content from either bytes or path + 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'); + } + + static Future validateJsonSchema(String jsonString) async { + final String schemaString; + + schemaString = await rootBundle.loadString('assets/schema.json'); + + try { + final schema = JsonSchema.create(json.decode(schemaString)); + final jsonData = json.decode(jsonString); + final result = schema.validate(jsonData); + + if (result.isValid) { + return true; + } + return false; + } catch (e, stack) { + print('[validateJsonSchema] $e'); + print(stack); + return false; + } + } +} + +class _SettingsViewState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -41,39 +86,31 @@ class SettingsView extends StatelessWidget { ), SettingsListTile( - title: 'Export Data', + title: 'Export data', icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => exportData(), + onPressed: () async { + final String json = await _getAppDataAsJson(context); + await exportData(json, 'export'); + }, ), SettingsListTile( - title: 'Import Data', + title: 'Import data', icon: Icons.download_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => importData(), - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), - child: Text( - textAlign: TextAlign.start, - 'Example Headline', - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), + onPressed: () => importData(context), ), SettingsListTile( - title: 'Example Tile', + title: 'Delete all data', + icon: Icons.download_outlined, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () => deleteAllData(context), + ), + SettingsListTile( + title: 'Add Sample Data', icon: Icons.upload_outlined, suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => print('Example Tile'), - ), - SettingsListTile( - title: 'Example Tile', - icon: Icons.download_outlined, - suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), - onPressed: () => print('Example Tile'), + onPressed: () => addSampleData(context), ), ], ), @@ -82,9 +119,128 @@ class SettingsView extends StatelessWidget { ); } - // TODO: Implement export functionality - void exportData() {} + Future deleteAllData(BuildContext context) async { + final db = Provider.of(context, listen: false); + await db.gameDao.deleteAllGames(); + await db.groupDao.deleteAllGroups(); + await db.playerDao.deleteAllPlayers(); + print('[deleteAllData] All data deleted'); + } - // TODO: Implement import functionality - void importData() {} + Future addSampleData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final player1 = Player(name: 'Alice'); + final player2 = Player(name: 'Bob'); + final group = Group(name: 'Friends', members: [player1, player2]); + final game = Game(name: 'Sample Game', group: group, winner: 'Alice'); + + await db.playerDao.addPlayer(player: player1); + await db.playerDao.addPlayer(player: player2); + await db.groupDao.addGroup(group: group); + await db.gameDao.addGame(game: game); + } + + Future _getAppDataAsJson(BuildContext context) async { + final db = Provider.of(context, listen: false); + final games = await db.gameDao.getAllGames(); + final groups = await db.groupDao.getAllGroups(); + final players = await db.playerDao.getAllPlayers(); + + // Construct a JSON representation of the data + final Map jsonMap = { + 'games': games.map((game) => game.toJson()).toList(), + 'groups': groups.map((group) => group.toJson()).toList(), + 'players': players.map((player) => player.toJson()).toList(), + }; + + return json.encode(jsonMap); + } + + Future exportData(String jsonString, String fileName) async { + try { + final bytes = Uint8List.fromList(utf8.encode(jsonString)); + await FilePicker.platform.saveFile( + fileName: '$fileName.json', + bytes: bytes, + ); + return true; + } catch (e, stack) { + print('[exportData] $e'); + print(stack); + return false; + } + } + + Future importData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final path = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + + if (path == null) { + print('[importData] No file selected'); + return; + } + + try { + final jsonString = await SettingsView._readFileContent(path.files.single); + + // Checks if the JSON String is in the gameList format + if (await SettingsView.validateJsonSchema(jsonString)) { + final Map jsonData = + json.decode(jsonString) as Map; + print('[importData] : $jsonData'); + final List? gamesJson = jsonData['games'] as List?; + final List? groupsJson = jsonData['groups'] as List?; + final List? playersJson = + jsonData['players'] as List?; + + final List importedGames = + gamesJson + ?.map((g) => Game.fromJson(g as Map)) + .toList() ?? + []; + final List importedGroups = + groupsJson + ?.map((g) => Group.fromJson(g as Map)) + .toList() ?? + []; + final List importedPlayers = + playersJson + ?.map((p) => Player.fromJson(p as Map)) + .toList() ?? + []; + + for (Player player in importedPlayers) { + await db.playerDao.addPlayer(player: player); + } + + for (Group group in importedGroups) { + await db.groupDao.addGroup(group: group); + } + + for (Game game in importedGames) { + await db.gameDao.addGame(game: game); + } + } else { + print('[importData] Invalid JSON schema'); + return; + } + print('[importData] Data imported successfully'); + return; + } on FormatException catch (e, stack) { + print('[importData] FormatException'); + print('[importData] $e'); + print(stack); + return; + } on Exception catch (e, stack) { + print('[importData] Exception'); + print('[importData] $e'); + print(stack); + return; + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index c7e55d7..bce31f2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,9 @@ dependencies: provider: ^6.1.5 skeletonizer: ^2.1.0+1 uuid: ^4.5.2 + file_picker: ^10.3.6 + json_schema: ^5.2.2 + file_saver: ^0.3.1 dev_dependencies: flutter_test: @@ -30,6 +33,5 @@ dev_dependencies: flutter: uses-material-design: true - -assets: - - assets/schema.json + assets: + - assets/schema.json