From ace157dc4730d0896df26155838583d2466f672b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 7 Jul 2025 23:14:11 +0200 Subject: [PATCH 1/5] Moved config values to ConfigService --- lib/main.dart | 5 ++--- lib/services/config_service.dart | 13 ++++++++----- lib/utility/globals.dart | 2 -- lib/views/create_game_view.dart | 10 +++++----- lib/views/main_menu_view.dart | 4 ++-- lib/views/settings_view.dart | 8 ++++---- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2a2a91e..cd3d05f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'package:cabo_counter/l10n/app_localizations.dart'; import 'package:cabo_counter/services/config_service.dart'; import 'package:cabo_counter/services/local_storage_service.dart'; import 'package:cabo_counter/utility/custom_theme.dart'; -import 'package:cabo_counter/utility/globals.dart'; import 'package:cabo_counter/views/tab_view.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; @@ -12,8 +11,8 @@ Future main() async { await SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); await ConfigService.initConfig(); - Globals.pointLimit = await ConfigService.getPointLimit(); - Globals.caboPenalty = await ConfigService.getCaboPenalty(); + ConfigService.pointLimit = await ConfigService.getPointLimit(); + ConfigService.caboPenalty = await ConfigService.getCaboPenalty(); runApp(const App()); } diff --git a/lib/services/config_service.dart b/lib/services/config_service.dart index 1c8275a..70f6133 100644 --- a/lib/services/config_service.dart +++ b/lib/services/config_service.dart @@ -1,4 +1,3 @@ -import 'package:cabo_counter/utility/globals.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// This class handles the configuration settings for the app. @@ -7,8 +6,12 @@ import 'package:shared_preferences/shared_preferences.dart'; class ConfigService { static const String _keyPointLimit = 'pointLimit'; static const String _keyCaboPenalty = 'caboPenalty'; - static const int _defaultPointLimit = 100; // Default Value - static const int _defaultCaboPenalty = 5; // Default Value + // Actual values used in the app + static int pointLimit = 100; + static int caboPenalty = 5; + // Default values + static const int _defaultPointLimit = 100; + static const int _defaultCaboPenalty = 5; static Future initConfig() async { final prefs = await SharedPreferences.getInstance(); @@ -48,8 +51,8 @@ class ConfigService { /// Resets the configuration to default values. static Future resetConfig() async { - Globals.pointLimit = _defaultPointLimit; - Globals.caboPenalty = _defaultCaboPenalty; + ConfigService.pointLimit = _defaultPointLimit; + ConfigService.caboPenalty = _defaultCaboPenalty; final prefs = await SharedPreferences.getInstance(); await prefs.setInt(_keyPointLimit, _defaultPointLimit); await prefs.setInt(_keyCaboPenalty, _defaultCaboPenalty); diff --git a/lib/utility/globals.dart b/lib/utility/globals.dart index 54cf2d0..e11a118 100644 --- a/lib/utility/globals.dart +++ b/lib/utility/globals.dart @@ -1,5 +1,3 @@ class Globals { - static int pointLimit = 100; - static int caboPenalty = 5; static String appDevPhase = 'Beta'; } diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index fd59529..21e8d54 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -1,8 +1,8 @@ import 'package:cabo_counter/data/game_manager.dart'; import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/l10n/app_localizations.dart'; +import 'package:cabo_counter/services/config_service.dart'; import 'package:cabo_counter/utility/custom_theme.dart'; -import 'package:cabo_counter/utility/globals.dart'; import 'package:cabo_counter/views/active_game_view.dart'; import 'package:cabo_counter/views/mode_selection_view.dart'; import 'package:flutter/cupertino.dart'; @@ -96,7 +96,7 @@ class _CreateGameViewState extends State { _isPointsLimitEnabled == null ? AppLocalizations.of(context).select_mode : (_isPointsLimitEnabled! - ? '${Globals.pointLimit} ${AppLocalizations.of(context).points}' + ? '${ConfigService.pointLimit} ${AppLocalizations.of(context).points}' : AppLocalizations.of(context).unlimited), ), const SizedBox(width: 3), @@ -108,7 +108,7 @@ class _CreateGameViewState extends State { context, CupertinoPageRoute( builder: (context) => ModeSelectionMenu( - pointLimit: Globals.pointLimit, + pointLimit: ConfigService.pointLimit, ), ), ); @@ -315,8 +315,8 @@ class _CreateGameViewState extends State { createdAt: DateTime.now(), gameTitle: _gameTitleTextController.text, players: players, - pointLimit: Globals.pointLimit, - caboPenalty: Globals.caboPenalty, + pointLimit: ConfigService.pointLimit, + caboPenalty: ConfigService.caboPenalty, isPointsLimitEnabled: _isPointsLimitEnabled!, ); final index = await gameManager.addGameSession(gameSession); diff --git a/lib/views/main_menu_view.dart b/lib/views/main_menu_view.dart index 3281c6f..e087cfc 100644 --- a/lib/views/main_menu_view.dart +++ b/lib/views/main_menu_view.dart @@ -1,8 +1,8 @@ import 'package:cabo_counter/data/game_manager.dart'; import 'package:cabo_counter/l10n/app_localizations.dart'; +import 'package:cabo_counter/services/config_service.dart'; import 'package:cabo_counter/services/local_storage_service.dart'; import 'package:cabo_counter/utility/custom_theme.dart'; -import 'package:cabo_counter/utility/globals.dart'; import 'package:cabo_counter/views/active_game_view.dart'; import 'package:cabo_counter/views/create_game_view.dart'; import 'package:cabo_counter/views/settings_view.dart'; @@ -199,7 +199,7 @@ class _MainMenuViewState extends State { /// If [pointLimit] is true, it returns '101 Punkte', otherwise it returns 'Unbegrenzt'. String _translateGameMode(bool pointLimit) { if (pointLimit) { - return '${Globals.pointLimit} ${AppLocalizations.of(context).points}'; + return '${ConfigService.pointLimit} ${AppLocalizations.of(context).points}'; } return AppLocalizations.of(context).unlimited; } diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index f98c542..2e86122 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -52,14 +52,14 @@ class _SettingsViewState extends State { AppLocalizations.of(context).cabo_penalty_subtitle), trailing: Stepper( key: _stepperKey1, - initialValue: Globals.caboPenalty, + initialValue: ConfigService.caboPenalty, minValue: 0, maxValue: 50, step: 1, onChanged: (newCaboPenalty) { setState(() { ConfigService.setCaboPenalty(newCaboPenalty); - Globals.caboPenalty = newCaboPenalty; + ConfigService.caboPenalty = newCaboPenalty; }); }, ), @@ -73,14 +73,14 @@ class _SettingsViewState extends State { Text(AppLocalizations.of(context).point_limit_subtitle), trailing: Stepper( key: _stepperKey2, - initialValue: Globals.pointLimit, + initialValue: ConfigService.pointLimit, minValue: 30, maxValue: 1000, step: 10, onChanged: (newPointLimit) { setState(() { ConfigService.setPointLimit(newPointLimit); - Globals.pointLimit = newPointLimit; + ConfigService.pointLimit = newPointLimit; }); }, ), From 751f490c44df32065c5cf8f57acf348b08028df9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 7 Jul 2025 23:18:15 +0200 Subject: [PATCH 2/5] Renamed current json scheme --- assets/{schema.json => game_list-schema.json} | 0 lib/services/local_storage_service.dart | 3 ++- pubspec.yaml | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) rename assets/{schema.json => game_list-schema.json} (100%) diff --git a/assets/schema.json b/assets/game_list-schema.json similarity index 100% rename from assets/schema.json rename to assets/game_list-schema.json diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index 338a0c3..e145571 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -196,7 +196,8 @@ class LocalStorageService { /// Validates the JSON data against the schema. static Future validateJsonSchema(String jsonString) async { try { - final schemaString = await rootBundle.loadString('assets/schema.json'); + final schemaString = + await rootBundle.loadString('assets/game_list-schema.json'); final schema = JsonSchema.create(json.decode(schemaString)); final jsonData = json.decode(jsonString); final result = schema.validate(jsonData); diff --git a/pubspec.yaml b/pubspec.yaml index b786c31..144adf4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: cabo_counter description: "Mobile app for the card game Cabo" publish_to: 'none' -version: 0.3.7+325 +version: 0.3.7+326 environment: sdk: ^3.5.4 @@ -39,4 +39,4 @@ flutter: uses-material-design: false assets: - assets/cabo_counter-logo_rounded.png - - assets/schema.json + - assets/game_list-schema.json From 5bfac732af215514a976b64d8d051a35adaa56ff Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 7 Jul 2025 23:19:30 +0200 Subject: [PATCH 3/5] Added new schema file --- assets/game-schema.json | 291 +++++++++++++++++++++++++++++++++++ assets/game_list-schema.json | 2 +- 2 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 assets/game-schema.json diff --git a/assets/game-schema.json b/assets/game-schema.json new file mode 100644 index 0000000..9991c74 --- /dev/null +++ b/assets/game-schema.json @@ -0,0 +1,291 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Generated schema for a single cabo counter game", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "gameTitle": { + "type": "string" + }, + "players": { + "type": "array", + "items": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "pointLimit": { + "type": "integer" + }, + "caboPenalty": { + "type": "integer" + }, + "isPointsLimitEnabled": { + "type": "boolean" + }, + "isGameFinished": { + "type": "boolean" + }, + "winner": { + "type": "string" + }, + "roundNumber": { + "type": "integer" + }, + "playerScores": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + }, + "roundList": { + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "roundNum": { + "type": "integer" + }, + "caboPlayerIndex": { + "type": "integer" + }, + "kamikazePlayerIndex": { + "type": "null" + }, + "scores": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + }, + "scoreUpdates": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + }, + "required": [ + "roundNum", + "caboPlayerIndex", + "kamikazePlayerIndex", + "scores", + "scoreUpdates" + ] + }, + { + "type": "object", + "properties": { + "roundNum": { + "type": "integer" + }, + "caboPlayerIndex": { + "type": "integer" + }, + "kamikazePlayerIndex": { + "type": "null" + }, + "scores": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + }, + "scoreUpdates": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + }, + "required": [ + "roundNum", + "caboPlayerIndex", + "kamikazePlayerIndex", + "scores", + "scoreUpdates" + ] + }, + { + "type": "object", + "properties": { + "roundNum": { + "type": "integer" + }, + "caboPlayerIndex": { + "type": "integer" + }, + "kamikazePlayerIndex": { + "type": "null" + }, + "scores": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + }, + "scoreUpdates": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + }, + "required": [ + "roundNum", + "caboPlayerIndex", + "kamikazePlayerIndex", + "scores", + "scoreUpdates" + ] + }, + { + "type": "object", + "properties": { + "roundNum": { + "type": "integer" + }, + "caboPlayerIndex": { + "type": "integer" + }, + "kamikazePlayerIndex": { + "type": "null" + }, + "scores": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + }, + "scoreUpdates": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + }, + "required": [ + "roundNum", + "caboPlayerIndex", + "kamikazePlayerIndex", + "scores", + "scoreUpdates" + ] + }, + { + "type": "object", + "properties": { + "roundNum": { + "type": "integer" + }, + "caboPlayerIndex": { + "type": "integer" + }, + "kamikazePlayerIndex": { + "type": "null" + }, + "scores": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + }, + "scoreUpdates": { + "type": "array", + "items": [ + { + "type": "integer" + }, + { + "type": "integer" + } + ] + } + }, + "required": [ + "roundNum", + "caboPlayerIndex", + "kamikazePlayerIndex", + "scores", + "scoreUpdates" + ] + } + ] + } + }, + "required": [ + "id", + "createdAt", + "gameTitle", + "players", + "pointLimit", + "caboPenalty", + "isPointsLimitEnabled", + "isGameFinished", + "winner", + "roundNumber", + "playerScores", + "roundList" + ] +} + diff --git a/assets/game_list-schema.json b/assets/game_list-schema.json index a9a07e5..26e4278 100644 --- a/assets/game_list-schema.json +++ b/assets/game_list-schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Generated schema for cabo game data", + "title": "Generated schema for the cabo counter game data", "type": "array", "items": { "type": "object", From 4a6f69ab85d9ff8fd3eb62a8363dc15f335d6ffd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 7 Jul 2025 23:31:04 +0200 Subject: [PATCH 4/5] Implemented single game detection --- lib/services/local_storage_service.dart | 54 ++++++++++++++++++++----- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index e145571..68097da 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -70,7 +70,7 @@ class LocalStorageService { return false; } - if (!await validateJsonSchema(jsonString)) { + if (!await validateJsonSchema(jsonString, true)) { print( '[local_storage_service.dart] Die Datei konnte nicht validiert werden'); gameManager.gameList = []; @@ -161,17 +161,29 @@ class LocalStorageService { try { final jsonString = await _readFileContent(path.files.single); + final jsonData = json.decode(jsonString) as List; - if (!await validateJsonSchema(jsonString)) { + if (await validateJsonSchema(jsonString, true)) { + print( + '[local_storage_service.dart] Die Datei wurde erfolgreich als Liste validiert'); + List tempList = jsonData + .map((jsonItem) => + GameSession.fromJson(jsonItem as Map)) + .toList(); + + for (GameSession s in tempList) { + importSession(s); + } + } else if (await validateJsonSchema(jsonString, false)) { + print( + '[local_storage_service.dart] Die Datei wurde erfolgreich als einzelnes Spiel validiert'); + importSession(GameSession.fromJson(jsonData as Map)); + } else { return ImportStatus.validationError; } - final jsonData = json.decode(jsonString) as List; - gameManager.gameList = jsonData - .map((jsonItem) => - GameSession.fromJson(jsonItem as Map)) - .toList(); + print( - '[local_storage_service.dart] Die Datei wurde erfolgreich Importiertn'); + '[local_storage_service.dart] Die Datei wurde erfolgreich Importiert'); await saveGameSessions(); return ImportStatus.success; } on FormatException catch (e) { @@ -185,6 +197,18 @@ class LocalStorageService { } } + /// Imports a single game session into the gameList. + static Future importSession(GameSession session) async { + if (gameManager.gameList.any((s) => s.id == session.id)) { + print( + '[local_storage_service.dart] Die Session mit der ID ${session.id} existiert bereits. Sie wird aktualisiert.'); + gameManager.gameList.removeWhere((s) => s.id == session.id); + } + gameManager.gameList.add(session); + print( + '[local_storage_service.dart] Die Session mit der ID ${session.id} wurde erfolgreich importiert.'); + } + /// 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!); @@ -194,10 +218,18 @@ class LocalStorageService { } /// Validates the JSON data against the schema. - static Future validateJsonSchema(String jsonString) async { - try { - final schemaString = + static Future validateJsonSchema( + String jsonString, bool isGameList) async { + final String schemaString; + + if (isGameList) { + schemaString = await rootBundle.loadString('assets/game_list-schema.json'); + } else { + schemaString = await rootBundle.loadString('assets/game-schema.json'); + } + + try { final schema = JsonSchema.create(json.decode(schemaString)); final jsonData = json.decode(jsonString); final result = schema.validate(jsonData); From c279acfdebcb225156c9ea45ae4d23b82ea95df4 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 7 Jul 2025 23:47:55 +0200 Subject: [PATCH 5/5] Organized LocalStorageService --- lib/services/local_storage_service.dart | 30 ++++++++++++++----------- pubspec.yaml | 3 ++- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index 68097da..12130a1 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -161,23 +161,23 @@ class LocalStorageService { try { final jsonString = await _readFileContent(path.files.single); - final jsonData = json.decode(jsonString) as List; if (await validateJsonSchema(jsonString, true)) { - print( - '[local_storage_service.dart] Die Datei wurde erfolgreich als Liste validiert'); - List tempList = jsonData + // Checks if the JSON String is in the gameList format + + final jsonData = json.decode(jsonString) as List; + List importedList = jsonData .map((jsonItem) => GameSession.fromJson(jsonItem as Map)) .toList(); - for (GameSession s in tempList) { + for (GameSession s in importedList) { importSession(s); } } else if (await validateJsonSchema(jsonString, false)) { - print( - '[local_storage_service.dart] Die Datei wurde erfolgreich als einzelnes Spiel validiert'); - importSession(GameSession.fromJson(jsonData as Map)); + // Checks if the JSON String is in the single game format + final jsonData = json.decode(jsonString) as Map; + importSession(GameSession.fromJson(jsonData)); } else { return ImportStatus.validationError; } @@ -199,12 +199,12 @@ class LocalStorageService { /// Imports a single game session into the gameList. static Future importSession(GameSession session) async { - if (gameManager.gameList.any((s) => s.id == session.id)) { + if (gameManager.gameExistsInGameList(session.id)) { print( - '[local_storage_service.dart] Die Session mit der ID ${session.id} existiert bereits. Sie wird aktualisiert.'); - gameManager.gameList.removeWhere((s) => s.id == session.id); + '[local_storage_service.dart] Die Session mit der ID ${session.id} existiert bereits. Sie wird überschrieben.'); + gameManager.removeGameSessionById(session.id); } - gameManager.gameList.add(session); + gameManager.addGameSession(session); print( '[local_storage_service.dart] Die Session mit der ID ${session.id} wurde erfolgreich importiert.'); } @@ -218,6 +218,9 @@ class LocalStorageService { } /// Validates the JSON data against the schema. + /// This method checks if the provided [jsonString] is valid against the + /// JSON schema. It takes a boolean [isGameList] to determine + /// which schema to use (game list or single game). static Future validateJsonSchema( String jsonString, bool isGameList) async { final String schemaString; @@ -235,7 +238,8 @@ class LocalStorageService { final result = schema.validate(jsonData); if (result.isValid) { - print('[local_storage_service.dart] JSON ist erfolgreich validiert.'); + print( + '[local_storage_service.dart] JSON ist erfolgreich validiert. Typ: ${isGameList ? 'Game List' : 'Single Game'}'); return true; } print( diff --git a/pubspec.yaml b/pubspec.yaml index 144adf4..7ac98ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: cabo_counter description: "Mobile app for the card game Cabo" publish_to: 'none' -version: 0.3.7+326 +version: 0.3.8+327 environment: sdk: ^3.5.4 @@ -40,3 +40,4 @@ flutter: assets: - assets/cabo_counter-logo_rounded.png - assets/game_list-schema.json + - assets/game-schema.json