diff --git a/assets/schema.json b/assets/schema.json index 17d7faa..a9a07e5 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -5,6 +5,9 @@ "items": { "type": "object", "properties": { + "id": { + "type": "string" + }, "createdAt": { "type": "string" }, diff --git a/lib/data/game_manager.dart b/lib/data/game_manager.dart index 94b6287..24c55da 100644 --- a/lib/data/game_manager.dart +++ b/lib/data/game_manager.dart @@ -15,14 +15,9 @@ class GameManager extends ChangeNotifier { notifyListeners(); // Propagate session changes }); gameList.add(session); - print( - '[game_manager.dart] Added game session: ${session.gameTitle} at ${session.createdAt}'); gameList.sort((a, b) => b.createdAt.compareTo(a.createdAt)); - print( - '[game_manager.dart] Sorted game sessions by creation date. Total sessions: ${gameList.length}'); notifyListeners(); await LocalStorageService.saveGameSessions(); - print('[game_manager.dart] Saved game sessions to local storage.'); return gameList.indexOf(session); } @@ -30,12 +25,26 @@ class GameManager extends ChangeNotifier { /// Takes a [index] as input. It then removes the session at the specified index from the `gameList`, /// sorts the list in descending order based on the creation date, and notifies listeners of the change. /// It also saves the updated game sessions to local storage. - void removeGameSession(int index) { + void removeGameSessionByIndex(int index) { gameList[index].removeListener(notifyListeners); gameList.removeAt(index); notifyListeners(); LocalStorageService.saveGameSessions(); } + + /// Removes a game session by its ID. + /// Takes a String [id] as input. It finds the index of the game session with the matching ID + /// in the `gameList`, and then calls `removeGameSessionByIndex` with that index. + void removeGameSessionById(String id) { + final int index = + gameList.indexWhere((session) => session.id.toString() == id); + if (index == -1) return; + removeGameSessionByIndex(index); + } + + bool gameExistsInGameList(String id) { + return gameList.any((session) => session.id.toString() == id); + } } final gameManager = GameManager(); diff --git a/lib/data/game_session.dart b/lib/data/game_session.dart index a741ae8..36c4c4e 100644 --- a/lib/data/game_session.dart +++ b/lib/data/game_session.dart @@ -1,5 +1,6 @@ import 'package:cabo_counter/data/round.dart'; import 'package:flutter/cupertino.dart'; +import 'package:uuid/uuid.dart'; /// This class represents a game session for Cabo game. /// [createdAt] is the timestamp of when the game session was created. @@ -12,6 +13,7 @@ import 'package:flutter/cupertino.dart'; /// [isGameFinished] is a boolean indicating if the game has ended yet. /// [winner] is the name of the player who won the game. class GameSession extends ChangeNotifier { + late String id; final DateTime createdAt; final String gameTitle; final List players; @@ -33,17 +35,20 @@ class GameSession extends ChangeNotifier { required this.isPointsLimitEnabled, }) { playerScores = List.filled(players.length, 0); + var uuid = const Uuid(); + id = uuid.v1(); } @override toString() { - return ('GameSession: [createdAt: $createdAt, gameTitle: $gameTitle, ' + return ('GameSession: [id: $id, createdAt: $createdAt, gameTitle: $gameTitle, ' 'isPointsLimitEnabled: $isPointsLimitEnabled, pointLimit: $pointLimit, caboPenalty: $caboPenalty,' ' players: $players, playerScores: $playerScores, roundList: $roundList, winner: $winner]'); } /// Converts the GameSession object to a JSON map. Map toJson() => { + 'id': id, 'createdAt': createdAt.toIso8601String(), 'gameTitle': gameTitle, 'players': players, @@ -59,7 +64,8 @@ class GameSession extends ChangeNotifier { /// Creates a GameSession object from a JSON map. GameSession.fromJson(Map json) - : createdAt = DateTime.parse(json['createdAt']), + : id = json['id'] ?? const Uuid().v1(), + createdAt = DateTime.parse(json['createdAt']), gameTitle = json['gameTitle'], players = List.from(json['players']), pointLimit = json['pointLimit'], diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 51147d0..7fa3710 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -22,7 +22,7 @@ "empty_text_1": "Ganz schön leer hier...", "empty_text_2": "Füge über den Button oben rechts eine neue Runde hinzu", "delete_game_title": "Spiel löschen?", - "delete_game_message": "Bist du sicher, dass du die Runde {gameTitle} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", + "delete_game_message": "Bist du sicher, dass du das Spiel \"{gameTitle}\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.", "@delete_game_message": { "placeholders": { "gameTitle": { @@ -68,6 +68,9 @@ "delete_game": "Spiel löschen", "new_game_same_settings": "Neues Spiel mit gleichen Einstellungen", "export_game": "Spiel exportieren", + "id_error_title": "ID Fehler", + "id_error_message": "Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.", + "game_process": "Spielverlauf", @@ -80,7 +83,6 @@ "game_data": "Spieldaten", "import_data": "Daten importieren", "export_data": "Daten exportieren", - "error": "Fehler", "import_success_title": "Import erfolgreich", "import_success_message":"Die Spieldaten wurden erfolgreich importiert.", @@ -91,7 +93,8 @@ "import_generic_error_title": "Import fehlgeschlagen", "import_generic_error_message": "Der Import ist fehlgeschlagen.", - "error_export": "Datei konnte nicht exportiert werden", + "export_error_title": "Fehler", + "export_error_message": "Datei konnte nicht exportiert werden", "error_found": "Fehler gefunden?", "create_issue": "Issue erstellen", "app_version": "App-Version", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a6b1a8f..f8dbb2f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -22,7 +22,7 @@ "empty_text_1": "Pretty empty here...", "empty_text_2": "Add a new round using the button in the top right corner.", "delete_game_title": "Delete game?", - "delete_game_message": "Are you sure you want to delete the game {gameTitle}? This action cannot be undone.", + "delete_game_message": "Are you sure you want to delete the game \"{gameTitle}\"? This action cannot be undone.", "@delete_game_message": { "placeholders": { "gameTitle": { @@ -80,7 +80,8 @@ "game_data": "Game Data", "import_data": "Import Data", "export_data": "Export Data", - "error": "Error", + "id_error_title": "ID Error", + "id_error_message": "The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.", "import_success_title": "Import successful", "import_success_message":"The game data has been successfully imported.", @@ -91,7 +92,8 @@ "import_generic_error_title": "Import failed", "import_generic_error_message": "The import has failed.", - "error_export": "Could not export file", + "export_error_title": "Export failed", + "export_error_message": "Could not export file", "error_found": "Found a bug?", "create_issue": "Create Issue", "app_version": "App Version", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a4e1edb..eb858f5 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -215,7 +215,7 @@ abstract class AppLocalizations { /// No description provided for @delete_game_message. /// /// In de, this message translates to: - /// **'Bist du sicher, dass du die Runde {gameTitle} löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'** + /// **'Bist du sicher, dass du das Spiel \"{gameTitle}\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'** String delete_game_message(String gameTitle); /// No description provided for @overview. @@ -386,6 +386,18 @@ abstract class AppLocalizations { /// **'Spiel exportieren'** String get export_game; + /// No description provided for @id_error_title. + /// + /// In de, this message translates to: + /// **'ID Fehler'** + String get id_error_title; + + /// No description provided for @id_error_message. + /// + /// In de, this message translates to: + /// **'Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.'** + String get id_error_message; + /// No description provided for @game_process. /// /// In de, this message translates to: @@ -446,12 +458,6 @@ abstract class AppLocalizations { /// **'Daten exportieren'** String get export_data; - /// No description provided for @error. - /// - /// In de, this message translates to: - /// **'Fehler'** - String get error; - /// No description provided for @import_success_title. /// /// In de, this message translates to: @@ -500,11 +506,17 @@ abstract class AppLocalizations { /// **'Der Import ist fehlgeschlagen.'** String get import_generic_error_message; - /// No description provided for @error_export. + /// No description provided for @export_error_title. + /// + /// In de, this message translates to: + /// **'Fehler'** + String get export_error_title; + + /// No description provided for @export_error_message. /// /// In de, this message translates to: /// **'Datei konnte nicht exportiert werden'** - String get error_export; + String get export_error_message; /// No description provided for @error_found. /// diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index b06d454..f72adbb 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -68,7 +68,7 @@ class AppLocalizationsDe extends AppLocalizations { @override String delete_game_message(String gameTitle) { - return 'Bist du sicher, dass du die Runde $gameTitle löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'; + return 'Bist du sicher, dass du das Spiel \"$gameTitle\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'; } @override @@ -161,6 +161,13 @@ class AppLocalizationsDe extends AppLocalizations { @override String get export_game => 'Spiel exportieren'; + @override + String get id_error_title => 'ID Fehler'; + + @override + String get id_error_message => + 'Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.'; + @override String get game_process => 'Spielverlauf'; @@ -191,9 +198,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get export_data => 'Daten exportieren'; - @override - String get error => 'Fehler'; - @override String get import_success_title => 'Import erfolgreich'; @@ -222,7 +226,10 @@ class AppLocalizationsDe extends AppLocalizations { String get import_generic_error_message => 'Der Import ist fehlgeschlagen.'; @override - String get error_export => 'Datei konnte nicht exportiert werden'; + String get export_error_title => 'Fehler'; + + @override + String get export_error_message => 'Datei konnte nicht exportiert werden'; @override String get error_found => 'Fehler gefunden?'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2dfc72d..657f2ce 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -68,7 +68,7 @@ class AppLocalizationsEn extends AppLocalizations { @override String delete_game_message(String gameTitle) { - return 'Are you sure you want to delete the game $gameTitle? This action cannot be undone.'; + return 'Are you sure you want to delete the game \"$gameTitle\"? This action cannot be undone.'; } @override @@ -158,6 +158,13 @@ class AppLocalizationsEn extends AppLocalizations { @override String get export_game => 'Export Game'; + @override + String get id_error_title => 'ID Error'; + + @override + String get id_error_message => + 'The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.'; + @override String get game_process => 'Spielverlauf'; @@ -188,9 +195,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get export_data => 'Export Data'; - @override - String get error => 'Error'; - @override String get import_success_title => 'Import successful'; @@ -219,7 +223,10 @@ class AppLocalizationsEn extends AppLocalizations { String get import_generic_error_message => 'The import has failed.'; @override - String get error_export => 'Could not export file'; + String get export_error_title => 'Export failed'; + + @override + String get export_error_message => 'Could not export file'; @override String get error_found => 'Found a bug?'; diff --git a/lib/main.dart b/lib/main.dart index 27c3835..2a2a91e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -65,6 +65,7 @@ class _AppState extends State with WidgetsBindingObserver { return supportedLocales.first; }, theme: CupertinoThemeData( + applyThemeToAll: true, brightness: Brightness.dark, primaryColor: CustomTheme.primaryColor, scaffoldBackgroundColor: CustomTheme.backgroundColor, diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index 6039a7b..71dd332 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -86,6 +86,11 @@ class LocalStorageService { GameSession.fromJson(jsonItem as Map)) .toList(); + for (GameSession session in gameManager.gameList) { + print( + '[local_storage_service.dart] Geladene Session: ${session.gameTitle} - ${session.id}'); + } + print( '[local_storage_service.dart] Die Spieldaten wurden erfolgreich geladen und verarbeitet'); return true; diff --git a/lib/views/active_game_view.dart b/lib/views/active_game_view.dart index d4ff7bd..1b5e546 100644 --- a/lib/views/active_game_view.dart +++ b/lib/views/active_game_view.dart @@ -1,3 +1,4 @@ +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/utility/custom_theme.dart'; @@ -17,15 +18,23 @@ class ActiveGameView extends StatefulWidget { } class _ActiveGameViewState extends State { + late final GameSession gameSession; + + @override + void initState() { + super.initState(); + gameSession = widget.gameSession; + } + @override Widget build(BuildContext context) { return ListenableBuilder( - listenable: widget.gameSession, + listenable: gameSession, builder: (context, _) { List sortedPlayerIndices = _getSortedPlayerIndices(); return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( - middle: Text(widget.gameSession.gameTitle), + middle: Text(gameSession.gameTitle), ), child: SafeArea( child: SingleChildScrollView( @@ -42,7 +51,7 @@ class _ActiveGameViewState extends State { ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: widget.gameSession.players.length, + itemCount: gameSession.players.length, itemBuilder: (BuildContext context, int index) { int playerIndex = sortedPlayerIndices[index]; return CupertinoListTile( @@ -51,7 +60,7 @@ class _ActiveGameViewState extends State { _getPlacementPrefix(index), const SizedBox(width: 5), Text( - widget.gameSession.players[playerIndex], + gameSession.players[playerIndex], style: const TextStyle( fontWeight: FontWeight.bold), ), @@ -60,8 +69,7 @@ class _ActiveGameViewState extends State { trailing: Row( children: [ const SizedBox(width: 5), - Text( - '${widget.gameSession.playerScores[playerIndex]} ' + Text('${gameSession.playerScores[playerIndex]} ' '${AppLocalizations.of(context).points}') ], ), @@ -78,7 +86,7 @@ class _ActiveGameViewState extends State { ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: widget.gameSession.roundNumber, + itemCount: gameSession.roundNumber, itemBuilder: (BuildContext context, int index) { return Padding( padding: const EdgeInsets.all(1), @@ -88,14 +96,13 @@ class _ActiveGameViewState extends State { title: Text( '${AppLocalizations.of(context).round} ${index + 1}', ), - trailing: index + 1 != - widget.gameSession.roundNumber || - widget.gameSession.isGameFinished == - true - ? (const Text('\u{2705}', - style: TextStyle(fontSize: 22))) - : const Text('\u{23F3}', - style: TextStyle(fontSize: 22)), + trailing: + index + 1 != gameSession.roundNumber || + gameSession.isGameFinished == true + ? (const Text('\u{2705}', + style: TextStyle(fontSize: 22))) + : const Text('\u{23F3}', + style: TextStyle(fontSize: 22)), onTap: () async { // ignore: unused_local_variable final val = await Navigator.of(context, @@ -104,7 +111,7 @@ class _ActiveGameViewState extends State { CupertinoPageRoute( fullscreenDialog: true, builder: (context) => RoundView( - gameSession: widget.gameSession, + gameSession: gameSession, roundNumber: index + 1), ), ); @@ -129,17 +136,21 @@ class _ActiveGameViewState extends State { ), onTap: () => Navigator.push( context, - MaterialPageRoute( + CupertinoPageRoute( builder: (_) => GraphView( - gameSession: widget.gameSession, + gameSession: gameSession, )))), CupertinoListTile( - title: - Text(AppLocalizations.of(context).delete_game, - style: const TextStyle( - color: Colors.white30, - )), - onTap: () {}, + title: Text( + AppLocalizations.of(context).delete_game, + ), + onTap: () { + _showDeleteGameDialog().then((value) { + if (value) { + _removeGameSession(gameSession); + } + }); + }, ), CupertinoListTile( title: Text( @@ -151,12 +162,11 @@ class _ActiveGameViewState extends State { context, CupertinoPageRoute( builder: (_) => CreateGameView( - gameTitle: - widget.gameSession.gameTitle, + gameTitle: gameSession.gameTitle, isPointsLimitEnabled: widget .gameSession .isPointsLimitEnabled, - players: widget.gameSession.players, + players: gameSession.players, ))); }, ), @@ -180,11 +190,11 @@ class _ActiveGameViewState extends State { /// ascending order. List _getSortedPlayerIndices() { List playerIndices = - List.generate(widget.gameSession.players.length, (index) => index); + List.generate(gameSession.players.length, (index) => index); // Sort the indices based on the summed points playerIndices.sort((a, b) { - int scoreA = widget.gameSession.playerScores[a]; - int scoreB = widget.gameSession.playerScores[b]; + int scoreA = gameSession.playerScores[a]; + int scoreB = gameSession.playerScores[b]; return scoreA.compareTo(scoreB); }); return playerIndices; @@ -217,4 +227,61 @@ class _ActiveGameViewState extends State { ); } } + + Future _showDeleteGameDialog() async { + return await showCupertinoDialog( + context: context, + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: Text(AppLocalizations.of(context).delete_game_title), + content: Text( + AppLocalizations.of(context) + .delete_game_message(gameSession.gameTitle), + ), + actions: [ + CupertinoDialogAction( + child: Text(AppLocalizations.of(context).cancel), + onPressed: () => Navigator.pop(context, false), + ), + CupertinoDialogAction( + child: Text( + AppLocalizations.of(context).delete, + style: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.red), + ), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + }, + ) ?? + false; + } + + Future _removeGameSession(GameSession gameSession) async { + if (gameManager.gameExistsInGameList(gameSession.id)) { + Navigator.pop(context); + + WidgetsBinding.instance.addPostFrameCallback((_) { + gameManager.removeGameSessionById(gameSession.id); + }); + } else { + showCupertinoDialog( + context: context, + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: Text(AppLocalizations.of(context).id_error_title), + content: Text(AppLocalizations.of(context).id_error_message), + actions: [ + CupertinoDialogAction( + child: Text(AppLocalizations.of(context).ok), + onPressed: () => Navigator.pop(context), + ), + ], + ); + }); + } + } } diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index 6c52890..fd59529 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -320,12 +320,13 @@ class _CreateGameViewState extends State { isPointsLimitEnabled: _isPointsLimitEnabled!, ); final index = await gameManager.addGameSession(gameSession); + final session = gameManager.gameList[index]; if (context.mounted) { Navigator.pushReplacement( context, CupertinoPageRoute( - builder: (context) => ActiveGameView( - gameSession: gameManager.gameList[index]))); + builder: (context) => + ActiveGameView(gameSession: session))); } }, ), diff --git a/lib/views/main_menu_view.dart b/lib/views/main_menu_view.dart index f41a0ef..3281c6f 100644 --- a/lib/views/main_menu_view.dart +++ b/lib/views/main_menu_view.dart @@ -131,7 +131,8 @@ class _MainMenuViewState extends State { gameTitle); }, onDismissed: (direction) { - gameManager.removeGameSession(index); + gameManager + .removeGameSessionByIndex(index); }, dismissThresholds: const { DismissDirection.startToEnd: 0.6 @@ -168,18 +169,19 @@ class _MainMenuViewState extends State { CupertinoIcons.person_2_fill), ], ), - onTap: () async { - //ignore: unused_local_variable - final val = await Navigator.push( + onTap: () { + final session = + gameManager.gameList[index]; + Navigator.push( context, CupertinoPageRoute( builder: (context) => ActiveGameView( - gameSession: gameManager - .gameList[index]), + gameSession: session), ), - ); - setState(() {}); + ).then((_) { + setState(() {}); + }); }, ), ), @@ -224,7 +226,11 @@ class _MainMenuViewState extends State { onPressed: () { Navigator.pop(context, true); }, - child: Text(AppLocalizations.of(context).delete), + child: Text( + AppLocalizations.of(context).delete, + style: const TextStyle( + fontWeight: FontWeight.bold, color: Colors.red), + ), ), ], ); diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart index b36347d..b9ae02c 100644 --- a/lib/views/settings_view.dart +++ b/lib/views/settings_view.dart @@ -145,10 +145,10 @@ class _SettingsViewState extends State { showCupertinoDialog( context: context, builder: (context) => CupertinoAlertDialog( - title: - Text(AppLocalizations.of(context).error), + title: Text(AppLocalizations.of(context) + .export_error_title), content: Text(AppLocalizations.of(context) - .error_export), + .export_error_message), actions: [ CupertinoDialogAction( child: diff --git a/pubspec.yaml b/pubspec.yaml index a7c91ae..56b89bc 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.5+269 +version: 0.3.6+318 environment: sdk: ^3.5.4 @@ -26,6 +26,7 @@ dependencies: sdk: flutter intl: any syncfusion_flutter_charts: ^30.1.37 + uuid: ^4.5.1 dev_dependencies: flutter_test: