Merge pull request #76 from flixcoo/feature/62-implement-delete-game-button-in-activegameview

Implement delete game button in activegameview
This commit is contained in:
2025-07-03 20:24:44 +02:00
committed by GitHub
15 changed files with 208 additions and 78 deletions

View File

@@ -5,6 +5,9 @@
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"createdAt": {
"type": "string"
},

View File

@@ -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();

View File

@@ -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<String> 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<String, dynamic> 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<String, dynamic> json)
: createdAt = DateTime.parse(json['createdAt']),
: id = json['id'] ?? const Uuid().v1(),
createdAt = DateTime.parse(json['createdAt']),
gameTitle = json['gameTitle'],
players = List<String>.from(json['players']),
pointLimit = json['pointLimit'],

View File

@@ -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",

View File

@@ -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",

View File

@@ -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.
///

View File

@@ -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?';

View File

@@ -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?';

View File

@@ -65,6 +65,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
return supportedLocales.first;
},
theme: CupertinoThemeData(
applyThemeToAll: true,
brightness: Brightness.dark,
primaryColor: CustomTheme.primaryColor,
scaffoldBackgroundColor: CustomTheme.backgroundColor,

View File

@@ -86,6 +86,11 @@ class LocalStorageService {
GameSession.fromJson(jsonItem as Map<String, dynamic>))
.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;

View File

@@ -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<ActiveGameView> {
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<int> 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<ActiveGameView> {
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<ActiveGameView> {
_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<ActiveGameView> {
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<ActiveGameView> {
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<ActiveGameView> {
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<ActiveGameView> {
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => RoundView(
gameSession: widget.gameSession,
gameSession: gameSession,
roundNumber: index + 1),
),
);
@@ -129,17 +136,21 @@ class _ActiveGameViewState extends State<ActiveGameView> {
),
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<ActiveGameView> {
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<ActiveGameView> {
/// ascending order.
List<int> _getSortedPlayerIndices() {
List<int> playerIndices =
List<int>.generate(widget.gameSession.players.length, (index) => index);
List<int>.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<ActiveGameView> {
);
}
}
Future<bool> _showDeleteGameDialog() async {
return await showCupertinoDialog<bool>(
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<void> _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),
),
],
);
});
}
}
}

View File

@@ -320,12 +320,13 @@ class _CreateGameViewState extends State<CreateGameView> {
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)));
}
},
),

View File

@@ -131,7 +131,8 @@ class _MainMenuViewState extends State<MainMenuView> {
gameTitle);
},
onDismissed: (direction) {
gameManager.removeGameSession(index);
gameManager
.removeGameSessionByIndex(index);
},
dismissThresholds: const {
DismissDirection.startToEnd: 0.6
@@ -168,18 +169,19 @@ class _MainMenuViewState extends State<MainMenuView> {
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<MainMenuView> {
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),
),
),
],
);

View File

@@ -145,10 +145,10 @@ class _SettingsViewState extends State<SettingsView> {
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:

View File

@@ -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: