From d49fec2b4830b137544423d46b4a3abc09d404a7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 13:54:10 +0200 Subject: [PATCH 01/16] Replaced styles.dart with theme.dart --- lib/utility/styles.dart | 34 ------------------------------ lib/views/create_game_view.dart | 6 +++--- lib/views/mode_selection_view.dart | 7 +++--- 3 files changed, 6 insertions(+), 41 deletions(-) delete mode 100644 lib/utility/styles.dart diff --git a/lib/utility/styles.dart b/lib/utility/styles.dart deleted file mode 100644 index d0f2921..0000000 --- a/lib/utility/styles.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:flutter/cupertino.dart'; - -abstract class Styles { - static Color primaryColor = CupertinoColors.systemGreen; - static Color backgroundColor = const Color(0xFF080808); - - static TextStyle modeTitle = TextStyle( - color: primaryColor, - fontSize: 20, - fontWeight: FontWeight.bold, - ); - - static const TextStyle modeDescription = TextStyle( - fontSize: 16, - ); - - static TextStyle createGameTitle = TextStyle( - fontSize: 20, - color: primaryColor, - fontWeight: FontWeight.bold, - ); - - static TextStyle roundTitle = const TextStyle( - fontSize: 60, - color: CupertinoColors.white, - fontWeight: FontWeight.bold, - ); - - static TextStyle roundPlayers = const TextStyle( - fontSize: 20, - color: CupertinoColors.white, - fontWeight: FontWeight.bold, - ); -} diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index 2ffbe22..14e961b 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -1,5 +1,5 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/styles.dart'; +import 'package:cabo_counter/utility/theme.dart' as theme; import 'package:cabo_counter/views/active_game_view.dart'; import 'package:cabo_counter/views/mode_selection_view.dart'; import 'package:flutter/cupertino.dart'; @@ -42,7 +42,7 @@ class _CreateGameState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spiel', - style: Styles.createGameTitle, + style: theme.createGameTitle, ), ), Padding( @@ -96,7 +96,7 @@ class _CreateGameState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spieler:innen', - style: Styles.createGameTitle, + style: theme.createGameTitle, ), ), Expanded( diff --git a/lib/views/mode_selection_view.dart b/lib/views/mode_selection_view.dart index 13a4b99..5876618 100644 --- a/lib/views/mode_selection_view.dart +++ b/lib/views/mode_selection_view.dart @@ -1,4 +1,3 @@ -import 'package:cabo_counter/utility/styles.dart'; import 'package:cabo_counter/utility/theme.dart' as theme; import 'package:flutter/cupertino.dart'; @@ -16,10 +15,10 @@ class ModeSelectionMenu extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(0, 16, 0, 0), child: CupertinoListTile( - title: Text('101 Punkte', style: Styles.modeTitle), + title: Text('101 Punkte', style: theme.modeTitle), subtitle: const Text( 'Es wird solange gespielt, bis einer Spieler mehr als 100 Punkte erreicht', - style: Styles.modeDescription, + style: theme.modeDescription, maxLines: 3, ), onTap: () { @@ -34,7 +33,7 @@ class ModeSelectionMenu extends StatelessWidget { subtitle: const Text( 'Dem Spiel sind keine Grenzen gesetzt. Es wird so lange ' 'gespielt, bis Ihr keine Lust mehr habt.', - style: Styles.modeDescription, + style: theme.modeDescription, maxLines: 3, ), onTap: () { From 1adbd16687b827b89630f747432faebb60dc7837 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 13:57:54 +0200 Subject: [PATCH 02/16] Implemented Theme class in every dart file --- lib/main.dart | 8 ++--- lib/utility/theme.dart | 56 ++++++++++++++++-------------- lib/views/active_game_view.dart | 6 ++-- lib/views/create_game_view.dart | 6 ++-- lib/views/mode_selection_view.dart | 10 +++--- lib/views/round_view.dart | 10 +++--- 6 files changed, 49 insertions(+), 47 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 9580339..47e0381 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,4 @@ -import 'package:cabo_counter/utility/theme.dart' as theme; +import 'package:cabo_counter/utility/theme.dart'; import 'package:cabo_counter/views/main_menu_view.dart'; import 'package:flutter/cupertino.dart'; @@ -14,10 +14,10 @@ class App extends StatelessWidget { return CupertinoApp( theme: CupertinoThemeData( brightness: Brightness.dark, - primaryColor: theme.primaryColor, - scaffoldBackgroundColor: theme.backgroundColor, + primaryColor: Theme.primaryColor, + scaffoldBackgroundColor: Theme.backgroundColor, textTheme: CupertinoTextThemeData( - primaryColor: theme.primaryColor, + primaryColor: Theme.primaryColor, ), ), debugShowCheckedModeBanner: false, diff --git a/lib/utility/theme.dart b/lib/utility/theme.dart index 6b4f963..8d8ec8e 100644 --- a/lib/utility/theme.dart +++ b/lib/utility/theme.dart @@ -1,34 +1,36 @@ import 'package:flutter/cupertino.dart'; -Color white = CupertinoColors.white; -Color primaryColor = CupertinoColors.systemGreen; -Color backgroundColor = const Color(0xFF101010); -Color backgroundTintColor = CupertinoColors.darkBackgroundGray; +class Theme { + static Color white = CupertinoColors.white; + static Color primaryColor = CupertinoColors.systemGreen; + static Color backgroundColor = const Color(0xFF101010); + static Color backgroundTintColor = CupertinoColors.darkBackgroundGray; -TextStyle modeTitle = TextStyle( - color: primaryColor, - fontSize: 20, - fontWeight: FontWeight.bold, -); + static TextStyle modeTitle = TextStyle( + color: primaryColor, + fontSize: 20, + fontWeight: FontWeight.bold, + ); -const TextStyle modeDescription = TextStyle( - fontSize: 16, -); + static const TextStyle modeDescription = TextStyle( + fontSize: 16, + ); -TextStyle createGameTitle = TextStyle( - fontSize: 20, - color: primaryColor, - fontWeight: FontWeight.bold, -); + static TextStyle createGameTitle = TextStyle( + fontSize: 20, + color: primaryColor, + fontWeight: FontWeight.bold, + ); -TextStyle roundTitle = const TextStyle( - fontSize: 60, - color: CupertinoColors.white, - fontWeight: FontWeight.bold, -); + static TextStyle roundTitle = TextStyle( + fontSize: 60, + color: white, + fontWeight: FontWeight.bold, + ); -TextStyle roundPlayers = const TextStyle( - fontSize: 20, - color: CupertinoColors.white, - fontWeight: FontWeight.bold, -); + static TextStyle roundPlayers = TextStyle( + fontSize: 20, + color: white, + fontWeight: FontWeight.bold, + ); +} diff --git a/lib/views/active_game_view.dart b/lib/views/active_game_view.dart index 59d3a1b..10c296f 100644 --- a/lib/views/active_game_view.dart +++ b/lib/views/active_game_view.dart @@ -1,5 +1,5 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/theme.dart' as theme; +import 'package:cabo_counter/utility/theme.dart'; import 'package:cabo_counter/views/round_view.dart'; import 'package:flutter/cupertino.dart'; @@ -28,7 +28,7 @@ class _ActiveGameViewState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spieler:innen', - style: theme.createGameTitle, + style: Theme.createGameTitle, ), ), ListView.builder( @@ -61,7 +61,7 @@ class _ActiveGameViewState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Runden', - style: theme.createGameTitle, + style: Theme.createGameTitle, ), ), ListView.builder( diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index 14e961b..a239337 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -1,5 +1,5 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/theme.dart' as theme; +import 'package:cabo_counter/utility/theme.dart'; import 'package:cabo_counter/views/active_game_view.dart'; import 'package:cabo_counter/views/mode_selection_view.dart'; import 'package:flutter/cupertino.dart'; @@ -42,7 +42,7 @@ class _CreateGameState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spiel', - style: theme.createGameTitle, + style: Theme.createGameTitle, ), ), Padding( @@ -96,7 +96,7 @@ class _CreateGameState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spieler:innen', - style: theme.createGameTitle, + style: Theme.createGameTitle, ), ), Expanded( diff --git a/lib/views/mode_selection_view.dart b/lib/views/mode_selection_view.dart index 5876618..949060b 100644 --- a/lib/views/mode_selection_view.dart +++ b/lib/views/mode_selection_view.dart @@ -1,4 +1,4 @@ -import 'package:cabo_counter/utility/theme.dart' as theme; +import 'package:cabo_counter/utility/theme.dart'; import 'package:flutter/cupertino.dart'; class ModeSelectionMenu extends StatelessWidget { @@ -15,10 +15,10 @@ class ModeSelectionMenu extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(0, 16, 0, 0), child: CupertinoListTile( - title: Text('101 Punkte', style: theme.modeTitle), + title: Text('101 Punkte', style: Theme.modeTitle), subtitle: const Text( 'Es wird solange gespielt, bis einer Spieler mehr als 100 Punkte erreicht', - style: theme.modeDescription, + style: Theme.modeDescription, maxLines: 3, ), onTap: () { @@ -29,11 +29,11 @@ class ModeSelectionMenu extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: CupertinoListTile( - title: Text('Unbegrenzt', style: theme.modeTitle), + title: Text('Unbegrenzt', style: Theme.modeTitle), subtitle: const Text( 'Dem Spiel sind keine Grenzen gesetzt. Es wird so lange ' 'gespielt, bis Ihr keine Lust mehr habt.', - style: theme.modeDescription, + style: Theme.modeDescription, maxLines: 3, ), onTap: () { diff --git a/lib/views/round_view.dart b/lib/views/round_view.dart index 9520aa5..ec6a3c8 100644 --- a/lib/views/round_view.dart +++ b/lib/views/round_view.dart @@ -1,5 +1,5 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/theme.dart' as theme; +import 'package:cabo_counter/utility/theme.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; @@ -86,7 +86,7 @@ class _RoundViewState extends State { children: [ const SizedBox(height: 40), Text('Runde ${widget.roundNumber}', - style: theme.roundTitle), + style: Theme.roundTitle), const SizedBox(height: 10), const Text( 'Wer hat CABO gesagt?', @@ -101,8 +101,8 @@ class _RoundViewState extends State { child: SizedBox( height: 40, child: CupertinoSegmentedControl( - unselectedColor: theme.backgroundTintColor, - selectedColor: theme.primaryColor, + unselectedColor: Theme.backgroundTintColor, + selectedColor: Theme.primaryColor, groupValue: _caboPlayerIndex, children: Map.fromEntries(widget.gameSession.players .asMap() @@ -267,7 +267,7 @@ class _RoundViewState extends State { return Container( height: 80, padding: const EdgeInsets.only(bottom: 20), - color: theme.backgroundTintColor, + color: Theme.backgroundTintColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ From 3cf8e91905a9c7df944506744a7ea5a3429b565a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 14:15:08 +0200 Subject: [PATCH 03/16] Updates global list for storing games --- lib/utility/globals.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/utility/globals.dart b/lib/utility/globals.dart index 2d22891..62e64bd 100644 --- a/lib/utility/globals.dart +++ b/lib/utility/globals.dart @@ -1,5 +1,11 @@ import 'package:cabo_counter/data/game_session.dart'; class Globals { - static Map gamesMap = {}; + /// The [gameList] contains all active game sessions. + static List gameList = []; + + static void addGameSession(GameSession session) { + gameList.add(session); + gameList.sort((a, b) => b.createdAt.compareTo(a.createdAt)); + } } From 9f315a5db8780a8b6edec0f55f7a089d164fd1f8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 14:33:50 +0200 Subject: [PATCH 04/16] Implementing gameList in Code --- lib/main.dart | 40 ++++++++++++++++++++++++++++++ lib/views/main_menu_view.dart | 46 +++-------------------------------- 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 47e0381..c662d14 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'package:cabo_counter/data/game_session.dart'; +import 'package:cabo_counter/utility/globals.dart'; import 'package:cabo_counter/utility/theme.dart'; import 'package:cabo_counter/views/main_menu_view.dart'; import 'package:flutter/cupertino.dart'; @@ -25,4 +27,42 @@ class App extends StatelessWidget { home: const MainMenuView(), ); } + + /// FIXME Just for Debugging + /// Fills the game list with some test data. + void fillGameList() { + Globals.addGameSession(GameSession( + gameTitle: 'Spiel am 27.02.2025', + players: ['Clara', 'Tobias', 'Yannik', 'Lena', 'Lekaia'], + gameHasPointLimit: true)); + Globals.addGameSession(GameSession( + gameTitle: 'Freundschaftsrunde', + players: ['Felix', 'Jonas', 'Nils'], + gameHasPointLimit: false)); + Globals.addGameSession(GameSession( + gameTitle: 'Familienabend', + players: ['Mama', 'Papa', 'Lisa'], + gameHasPointLimit: true, + )); + Globals.addGameSession(GameSession( + gameTitle: 'Turnier 1. Runde', + players: ['Tim', 'Max', 'Sophie', 'Lena'], + gameHasPointLimit: false)); + Globals.addGameSession(GameSession( + gameTitle: '2 Namen max length', + players: ['Heinrich', 'Johannes'], + gameHasPointLimit: true)); + Globals.addGameSession(GameSession( + gameTitle: '3 Namen max length', + players: ['Benjamin', 'Stefanie', 'Wolfgang'], + gameHasPointLimit: false)); + Globals.addGameSession(GameSession( + gameTitle: '4 Namen max length', + players: ['Leonhard', 'Mathilde', 'Bernhard', 'Gerlinde'], + gameHasPointLimit: true)); + Globals.addGameSession(GameSession( + gameTitle: '5 Namen max length', + players: ['Hartmuth', 'Elisabet', 'Rosalind', 'Theresia', 'Karoline'], + gameHasPointLimit: false)); + } } diff --git a/lib/views/main_menu_view.dart b/lib/views/main_menu_view.dart index d89a91e..718d35d 100644 --- a/lib/views/main_menu_view.dart +++ b/lib/views/main_menu_view.dart @@ -1,4 +1,4 @@ -import 'package:cabo_counter/data/game_session.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/information_view.dart'; @@ -14,46 +14,8 @@ class MainMenuView extends StatefulWidget { } class _MainMenuViewState extends State { - final List gameSessionArray = [ - GameSession( - gameTitle: 'Spiel am 27.02.2025', - players: ['Clara', 'Tobias', 'Yannik', 'Lena', 'Lekaia'], - gameHasPointLimit: true), - GameSession( - gameTitle: 'Freundschaftsrunde', - players: ['Felix', 'Jonas', 'Nils'], - gameHasPointLimit: false), - GameSession( - gameTitle: 'Familienabend', - players: ['Mama', 'Papa', 'Lisa'], - gameHasPointLimit: true, - ), - GameSession( - gameTitle: 'Turnier 1. Runde', - players: ['Tim', 'Max', 'Sophie', 'Lena'], - gameHasPointLimit: false), - GameSession( - gameTitle: '2 Namen max length', - players: ['Heinrich', 'Johannes'], - gameHasPointLimit: true), - GameSession( - gameTitle: '3 Namen max length', - players: ['Benjamin', 'Stefanie', 'Wolfgang'], - gameHasPointLimit: false), - GameSession( - gameTitle: '4 Namen max length', - players: ['Leonhard', 'Mathilde', 'Bernhard', 'Gerlinde'], - gameHasPointLimit: true), - GameSession( - gameTitle: '5 Namen max length', - players: ['Hartmuth', 'Elisabet', 'Rosalind', 'Theresia', 'Karoline'], - gameHasPointLimit: false), - ]; - @override Widget build(BuildContext context) { - gameSessionArray.sort((b, a) => a.createdAt.compareTo(b.createdAt)); - return CupertinoPageScaffold( navigationBar: CupertinoNavigationBar( leading: IconButton( @@ -84,9 +46,9 @@ class _MainMenuViewState extends State { child: CupertinoPageScaffold( child: SafeArea( child: ListView.builder( - itemCount: gameSessionArray.length, + itemCount: Globals.gameList.length, itemBuilder: (context, index) { - final session = gameSessionArray[index]; + final session = Globals.gameList[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 10.0), child: CupertinoListTile( @@ -118,7 +80,7 @@ class _MainMenuViewState extends State { context, CupertinoPageRoute( builder: (context) => ActiveGameView( - gameSession: gameSessionArray[index]), + gameSession: Globals.gameList[index]), ), ); setState(() {}); From 6b6e824860d5a145aac4dd7477c3d153632287ee Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 14:34:14 +0200 Subject: [PATCH 05/16] Impementing toJson() and fromJson() Method in GameSession and Round class --- lib/data/game_session.dart | 24 ++++++++++++++++++++++++ lib/data/round.dart | 17 +++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/data/game_session.dart b/lib/data/game_session.dart index 0403822..d021ceb 100644 --- a/lib/data/game_session.dart +++ b/lib/data/game_session.dart @@ -38,6 +38,30 @@ class GameSession { 'winner: $winner]'); } + /// Converts the GameSession object to a JSON map. + Map toJson() => { + 'gameTitle': gameTitle, + 'gameHasPointLimit': gameHasPointLimit, + 'players': players, + 'playerScores': playerScores, + 'roundNumber': roundNumber, + 'isGameFinished': isGameFinished, + 'winner': winner, + 'roundList': roundList.map((e) => e.toJson()).toList() + }; + + /// Creates a GameSession object from a JSON map. + GameSession.fromJson(Map json) + : gameTitle = json['gameTitle'], + gameHasPointLimit = json['gameHasPointLimit'], + players = List.from(json['players']), + playerScores = List.from(json['playerScores']), + roundNumber = json['roundNumber'], + isGameFinished = json['isGameFinished'], + winner = json['winner'], + roundList = + (json['roundList'] as List).map((e) => Round.fromJson(e)).toList(); + /// Returns the length of all player names combined. int getLengthOfPlayerNames() { int length = 0; diff --git a/lib/data/round.dart b/lib/data/round.dart index 1f03b33..d98a4f4 100644 --- a/lib/data/round.dart +++ b/lib/data/round.dart @@ -1,3 +1,5 @@ +import 'package:cabo_counter/data/game_session.dart'; + /// This class represents a single round in the game. /// It is stored within the [GameSession] class. /// [roundNum] is the number of the round its reppresenting. @@ -16,4 +18,19 @@ class Round { required this.scores, required this.scoreUpdates, this.kamikazePlayerIndex}); + + /// Converts the Round object to a JSON map. + Map toJson() => { + 'roundNum': roundNum, + 'scores': scores, + 'scoreUpdates': scoreUpdates, + 'kamikazePlayerIndex': kamikazePlayerIndex, + }; + + /// Creates a Round object from a JSON map. + Round.fromJson(Map json) + : roundNum = json['roundNum'], + scores = List.from(json['scores']), + scoreUpdates = List.from(json['scoreUpdates']), + kamikazePlayerIndex = json['kamikazePlayerIndex']; } From 882a781b26a987c15364dc447b37bb693396fe1a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 17:16:04 +0200 Subject: [PATCH 06/16] Renamed Theme to AppTheme --- lib/utility/{theme.dart => apptheme.dart} | 2 +- lib/views/active_game_view.dart | 6 +++--- lib/views/create_game_view.dart | 10 +++++++--- lib/views/information_view.dart | 1 + lib/views/mode_selection_view.dart | 10 +++++----- 5 files changed, 17 insertions(+), 12 deletions(-) rename lib/utility/{theme.dart => apptheme.dart} (98%) diff --git a/lib/utility/theme.dart b/lib/utility/apptheme.dart similarity index 98% rename from lib/utility/theme.dart rename to lib/utility/apptheme.dart index 8d8ec8e..ebbbab6 100644 --- a/lib/utility/theme.dart +++ b/lib/utility/apptheme.dart @@ -1,6 +1,6 @@ import 'package:flutter/cupertino.dart'; -class Theme { +class AppTheme { static Color white = CupertinoColors.white; static Color primaryColor = CupertinoColors.systemGreen; static Color backgroundColor = const Color(0xFF101010); diff --git a/lib/views/active_game_view.dart b/lib/views/active_game_view.dart index 10c296f..85d5a70 100644 --- a/lib/views/active_game_view.dart +++ b/lib/views/active_game_view.dart @@ -1,5 +1,5 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/theme.dart'; +import 'package:cabo_counter/utility/apptheme.dart'; import 'package:cabo_counter/views/round_view.dart'; import 'package:flutter/cupertino.dart'; @@ -28,7 +28,7 @@ class _ActiveGameViewState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spieler:innen', - style: Theme.createGameTitle, + style: AppTheme.createGameTitle, ), ), ListView.builder( @@ -61,7 +61,7 @@ class _ActiveGameViewState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Runden', - style: Theme.createGameTitle, + style: AppTheme.createGameTitle, ), ), ListView.builder( diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index a239337..15ea53f 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -1,5 +1,7 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/theme.dart'; +import 'package:cabo_counter/utility/apptheme.dart'; +import 'package:cabo_counter/utility/globals.dart'; +import 'package:cabo_counter/utility/local_storage_service.dart'; import 'package:cabo_counter/views/active_game_view.dart'; import 'package:cabo_counter/views/mode_selection_view.dart'; import 'package:flutter/cupertino.dart'; @@ -42,7 +44,7 @@ class _CreateGameState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spiel', - style: Theme.createGameTitle, + style: AppTheme.createGameTitle, ), ), Padding( @@ -96,7 +98,7 @@ class _CreateGameState extends State { padding: const EdgeInsets.fromLTRB(10, 10, 0, 0), child: Text( 'Spieler:innen', - style: Theme.createGameTitle, + style: AppTheme.createGameTitle, ), ), Expanded( @@ -285,6 +287,8 @@ class _CreateGameState extends State { players: players, gameHasPointLimit: selectedMode!, ); + Globals.addGameSession(gameSession); + LocalStorageService.saveGameSessions(); Navigator.pushReplacement( context, CupertinoPageRoute( diff --git a/lib/views/information_view.dart b/lib/views/information_view.dart index 667d180..e2f3ba8 100644 --- a/lib/views/information_view.dart +++ b/lib/views/information_view.dart @@ -14,6 +14,7 @@ class InformationView extends StatelessWidget { @override Widget build(BuildContext context) { return CupertinoPageScaffold( + resizeToAvoidBottomInset: false, navigationBar: const CupertinoNavigationBar( middle: Text('Über'), ), diff --git a/lib/views/mode_selection_view.dart b/lib/views/mode_selection_view.dart index 949060b..7b55b30 100644 --- a/lib/views/mode_selection_view.dart +++ b/lib/views/mode_selection_view.dart @@ -1,4 +1,4 @@ -import 'package:cabo_counter/utility/theme.dart'; +import 'package:cabo_counter/utility/apptheme.dart'; import 'package:flutter/cupertino.dart'; class ModeSelectionMenu extends StatelessWidget { @@ -15,10 +15,10 @@ class ModeSelectionMenu extends StatelessWidget { Padding( padding: const EdgeInsets.fromLTRB(0, 16, 0, 0), child: CupertinoListTile( - title: Text('101 Punkte', style: Theme.modeTitle), + title: Text('101 Punkte', style: AppTheme.modeTitle), subtitle: const Text( 'Es wird solange gespielt, bis einer Spieler mehr als 100 Punkte erreicht', - style: Theme.modeDescription, + style: AppTheme.modeDescription, maxLines: 3, ), onTap: () { @@ -29,11 +29,11 @@ class ModeSelectionMenu extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(vertical: 16.0), child: CupertinoListTile( - title: Text('Unbegrenzt', style: Theme.modeTitle), + title: Text('Unbegrenzt', style: AppTheme.modeTitle), subtitle: const Text( 'Dem Spiel sind keine Grenzen gesetzt. Es wird so lange ' 'gespielt, bis Ihr keine Lust mehr habt.', - style: Theme.modeDescription, + style: AppTheme.modeDescription, maxLines: 3, ), onTap: () { From 8fed846eac9041b2450e22c40efcf6428c3ef3dd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 17:16:15 +0200 Subject: [PATCH 07/16] Implemented LocalStorageService --- lib/utility/local_storage_service.dart | 58 ++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 lib/utility/local_storage_service.dart diff --git a/lib/utility/local_storage_service.dart b/lib/utility/local_storage_service.dart new file mode 100644 index 0000000..ada8492 --- /dev/null +++ b/lib/utility/local_storage_service.dart @@ -0,0 +1,58 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cabo_counter/data/game_session.dart'; +import 'package:cabo_counter/utility/globals.dart'; +import 'package:path_provider/path_provider.dart'; + +class LocalStorageService { + static const String _fileName = 'game_data.json'; + + /// Speichert GameSessions im App-Dokumentenverzeichnis + static Future saveGameSessions() async { + try { + List sessions = Globals.gameList; + final file = await _getLocalFile(); + final jsonList = sessions.map((session) => session.toJson()).toList(); + await file.writeAsString(json.encode(jsonList)); + print('Daten gespeichert'); + } catch (e) { + print('Fehler beim Speichern: $e'); + } + } + + /// Lädt GameSessions aus dem App-Dokumentenverzeichnis + static Future loadGameSessions() async { + print('Versuche, Daten zu laden...'); // FIXME Debug-Ausgabe + try { + final file = await _getLocalFile(); + if (await file.exists()) { + print('Datei existiert'); // FIXME Debug-Ausgabe + final jsonString = await file.readAsString(); + if (jsonString.isNotEmpty) { + print('Datei ist nicht leer'); // FIXME Debug-Ausgabe + final jsonList = json.decode(jsonString) as List; + print('JSON: $jsonList'); // FIXME Debug-Ausgabe + Globals.gameList = + jsonList.map((json) => GameSession.fromJson(json)).toList(); + print('Daten erfolgreich geladen'); + } else { + print('Datei ist leer'); + } + } else { + print('Datei existiert nicht'); + } + } catch (e) { + print('Fehler beim Laden: $e'); + // Bei Fehler eine leere Liste setzen + Globals.gameList = []; + } + } + + static Future _getLocalFile() async { + final directory = await getApplicationDocumentsDirectory(); + final path = '${directory.path}/$_fileName'; + print('Speicherpfad: $path'); // FIXME Debug-Ausgabe + return File(path); + } +} From 11f87bc198f6bbdb786002cd958a26a9dd9bd48d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 17:17:01 +0200 Subject: [PATCH 08/16] Implemented save & load method --- .../xcshareddata/xcschemes/Runner.xcscheme | 4 +- lib/main.dart | 87 +++++++------ lib/views/main_menu_view.dart | 121 ++++++++++++------ lib/views/round_view.dart | 18 ++- pubspec.yaml | 3 +- 5 files changed, 140 insertions(+), 93 deletions(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1728d10..4502f08 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -52,8 +52,8 @@ { + @override + initState() { + super.initState(); + LocalStorageService.loadGameSessions().then((_) { + setState(() {}); + }); + } + @override Widget build(BuildContext context) { + LocalStorageService.loadGameSessions(); return CupertinoPageScaffold( + resizeToAvoidBottomInset: false, navigationBar: CupertinoNavigationBar( leading: IconButton( onPressed: () { @@ -45,49 +57,74 @@ class _MainMenuViewState extends State { ), child: CupertinoPageScaffold( child: SafeArea( - child: ListView.builder( - itemCount: Globals.gameList.length, - itemBuilder: (context, index) { - final session = Globals.gameList[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10.0), - child: CupertinoListTile( - title: Text(session.gameTitle), - subtitle: session.isGameFinished == true - ? Text( - '\u{1F947} ${session.winner}', - style: const TextStyle(fontSize: 14), - ) - : Text( - 'Modus: ${_translateGameMode(session.gameHasPointLimit)}', - style: const TextStyle(fontSize: 14), - ), - trailing: Row( - children: [ - Text('${session.roundNumber}'), - const SizedBox(width: 3), - const Icon( - CupertinoIcons.arrow_2_circlepath_circle_fill), - const SizedBox(width: 15), - Text('${session.players.length}'), - const SizedBox(width: 3), - const Icon(CupertinoIcons.person_2_fill), - ], + child: Globals.gameList.isEmpty + ? Column( + mainAxisAlignment: + MainAxisAlignment.center, // Oben ausrichten + children: [ + const SizedBox(height: 30), // Abstand von oben + Center( + child: GestureDetector( + onTap: () => setState(() {}), + child: Icon( + CupertinoIcons.plus, + size: 60, + color: AppTheme.primaryColor, + ), + )), + const SizedBox(height: 10), // Abstand von oben + const Padding( + padding: EdgeInsets.symmetric(horizontal: 70), + child: Text( + 'Ganz schön leer hier...\nFüge über den Button oben rechts eine neue Runde hinzu.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 16), + ), ), - onTap: () async { - //ignore: unused_local_variable - final val = await Navigator.push( - context, - CupertinoPageRoute( - builder: (context) => ActiveGameView( - gameSession: Globals.gameList[index]), - ), - ); - setState(() {}); - }, - )); - }, - ), + ], + ) + : ListView.builder( + itemCount: Globals.gameList.length, + itemBuilder: (context, index) { + final session = Globals.gameList[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0), + child: CupertinoListTile( + title: Text(session.gameTitle), + subtitle: session.isGameFinished == true + ? Text( + '\u{1F947} ${session.winner}', + style: const TextStyle(fontSize: 14), + ) + : Text( + 'Modus: ${_translateGameMode(session.gameHasPointLimit)}', + style: const TextStyle(fontSize: 14), + ), + trailing: Row( + children: [ + Text('${session.roundNumber}'), + const SizedBox(width: 3), + const Icon(CupertinoIcons + .arrow_2_circlepath_circle_fill), + const SizedBox(width: 15), + Text('${session.players.length}'), + const SizedBox(width: 3), + const Icon(CupertinoIcons.person_2_fill), + ], + ), + onTap: () async { + //ignore: unused_local_variable + final val = await Navigator.push( + context, + CupertinoPageRoute( + builder: (context) => ActiveGameView( + gameSession: Globals.gameList[index]), + ), + ); + setState(() {}); + }, + )); + }), ), ), ); diff --git a/lib/views/round_view.dart b/lib/views/round_view.dart index ec6a3c8..9a5a035 100644 --- a/lib/views/round_view.dart +++ b/lib/views/round_view.dart @@ -1,5 +1,6 @@ import 'package:cabo_counter/data/game_session.dart'; -import 'package:cabo_counter/utility/theme.dart'; +import 'package:cabo_counter/utility/apptheme.dart'; +import 'package:cabo_counter/utility/local_storage_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/services.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; @@ -71,7 +72,10 @@ class _RoundViewState extends State { previousPageTitle: 'Übersicht', leading: CupertinoButton( padding: EdgeInsets.zero, - onPressed: () => Navigator.pop(context, widget.gameSession), + onPressed: () => { + LocalStorageService.saveGameSessions(), + Navigator.pop(context, widget.gameSession) + }, child: const Text('Abbrechen'), ), ), @@ -86,7 +90,7 @@ class _RoundViewState extends State { children: [ const SizedBox(height: 40), Text('Runde ${widget.roundNumber}', - style: Theme.roundTitle), + style: AppTheme.roundTitle), const SizedBox(height: 10), const Text( 'Wer hat CABO gesagt?', @@ -101,8 +105,8 @@ class _RoundViewState extends State { child: SizedBox( height: 40, child: CupertinoSegmentedControl( - unselectedColor: Theme.backgroundTintColor, - selectedColor: Theme.primaryColor, + unselectedColor: AppTheme.backgroundTintColor, + selectedColor: AppTheme.primaryColor, groupValue: _caboPlayerIndex, children: Map.fromEntries(widget.gameSession.players .asMap() @@ -267,7 +271,7 @@ class _RoundViewState extends State { return Container( height: 80, padding: const EdgeInsets.only(bottom: 20), - color: Theme.backgroundTintColor, + color: AppTheme.backgroundTintColor, child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ @@ -275,6 +279,7 @@ class _RoundViewState extends State { onPressed: _areRoundInputsValid() ? () { _finishRound(); + LocalStorageService.saveGameSessions(); Navigator.pop(context, widget.gameSession); } : null, @@ -284,6 +289,7 @@ class _RoundViewState extends State { onPressed: _areRoundInputsValid() ? () { _finishRound(); + LocalStorageService.saveGameSessions(); if (widget.gameSession.isGameFinished == true) { Navigator.pop(context, widget.gameSession); } else { diff --git a/pubspec.yaml b/pubspec.yaml index 326e7ee..95bae58 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.1.3+65 +version: 0.1.3+101 environment: sdk: ^3.5.4 @@ -15,6 +15,7 @@ dependencies: url_launcher: any package_info_plus: any flutter_keyboard_visibility: ^6.0.0 + path_provider: ^2.1.1 dev_dependencies: flutter_test: From 59671c353c7723609c81c40777a0e89997a489ee Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 17:28:51 +0200 Subject: [PATCH 09/16] Implemented life cycle listener --- lib/main.dart | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 04eb334..dfe283a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,6 @@ +import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/utility/apptheme.dart'; +import 'package:cabo_counter/utility/globals.dart'; import 'package:cabo_counter/utility/local_storage_service.dart'; import 'package:cabo_counter/views/main_menu_view.dart'; import 'package:flutter/cupertino.dart'; @@ -6,7 +8,7 @@ import 'package:flutter/cupertino.dart'; void main() { /// FIXME Just for Debugging /// Fills the game list with some test data. - /*Globals.addGameSession(GameSession( + Globals.addGameSession(GameSession( gameTitle: 'Spiel am 27.02.2025', players: ['Clara', 'Tobias', 'Yannik', 'Lena', 'Lekaia'], gameHasPointLimit: true)); @@ -39,13 +41,39 @@ void main() { gameTitle: '5 Namen max length', players: ['Hartmuth', 'Elisabet', 'Rosalind', 'Theresia', 'Karoline'], gameHasPointLimit: false)); - */ + runApp(const App()); } -class App extends StatelessWidget { +class App extends StatefulWidget { const App({super.key}); + @override + State createState() => _AppState(); +} + +class _AppState extends State with WidgetsBindingObserver { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + LocalStorageService.loadGameSessions(); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.paused || + state == AppLifecycleState.detached) { + LocalStorageService.saveGameSessions(); + } + } + @override Widget build(BuildContext context) { LocalStorageService.loadGameSessions(); @@ -64,8 +92,4 @@ class App extends StatelessWidget { home: const MainMenuView(), ); } - - dispose() { - LocalStorageService.saveGameSessions(); - } } From 6402fff7d42d7dba4a20302a654f28d33c2df059 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 17:28:57 +0200 Subject: [PATCH 10/16] Removed return --- lib/views/create_game_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index 15ea53f..210e4b2 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -275,7 +275,6 @@ class _CreateGameState extends State { ], ), ); - return; } List players = []; From c8113430f163027cbe501ecfe192b2b3a63f2772 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 30 Apr 2025 17:59:21 +0200 Subject: [PATCH 11/16] Prepared for dialog implementation --- .../xcshareddata/xcschemes/Runner.xcscheme | 4 ++-- lib/utility/local_storage_service.dart | 11 ++++++++--- lib/views/information_view.dart | 9 ++++++++- pubspec.yaml | 3 ++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 4502f08..1728d10 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -52,8 +52,8 @@ saveGameSessions() async { try { - List sessions = Globals.gameList; final file = await _getLocalFile(); - final jsonList = sessions.map((session) => session.toJson()).toList(); - await file.writeAsString(json.encode(jsonList)); + final jsonFile = getJsonFile(); + await file.writeAsString(jsonFile); print('Daten gespeichert'); } catch (e) { print('Fehler beim Speichern: $e'); @@ -49,6 +48,12 @@ class LocalStorageService { } } + static String getJsonFile() { + final jsonFile = + Globals.gameList.map((session) => session.toJson()).toList(); + return json.encode(jsonFile); + } + static Future _getLocalFile() async { final directory = await getApplicationDocumentsDirectory(); final path = '${directory.path}/$_fileName'; diff --git a/lib/views/information_view.dart b/lib/views/information_view.dart index e2f3ba8..d64f2ff 100644 --- a/lib/views/information_view.dart +++ b/lib/views/information_view.dart @@ -80,7 +80,10 @@ class InformationView extends StatelessWidget { Uri.parse('https://www.github.com/flixcoo')), icon: const Icon(FontAwesomeIcons.github)), ], - ) + ), + CupertinoButton( + child: const Text('Spieldaten exportieren'), + onPressed: () => {saveJsonToDevice()}) ], ), Positioned( @@ -112,4 +115,8 @@ class InformationView extends StatelessWidget { ], ))); } + + void saveJsonToDevice() async { + //todo: implement + } } diff --git a/pubspec.yaml b/pubspec.yaml index 95bae58..76baaef 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.1.3+101 +version: 0.1.3+103 environment: sdk: ^3.5.4 @@ -16,6 +16,7 @@ dependencies: package_info_plus: any flutter_keyboard_visibility: ^6.0.0 path_provider: ^2.1.1 + file_picker: any dev_dependencies: flutter_test: From 22ac2efea2821d55c7361851a6b69242c7f0e44f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 1 May 2025 00:41:35 +0200 Subject: [PATCH 12/16] Implemented exportJsonFile & importJsonFile methods --- lib/utility/local_storage_service.dart | 103 +++++++++++++++++++------ lib/views/information_view.dart | 10 +-- pubspec.yaml | 5 +- 3 files changed, 88 insertions(+), 30 deletions(-) diff --git a/lib/utility/local_storage_service.dart b/lib/utility/local_storage_service.dart index 79aee1e..ce9c762 100644 --- a/lib/utility/local_storage_service.dart +++ b/lib/utility/local_storage_service.dart @@ -1,17 +1,34 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:cabo_counter/data/game_session.dart'; import 'package:cabo_counter/utility/globals.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:path_provider/path_provider.dart'; class LocalStorageService { static const String _fileName = 'game_data.json'; - /// Speichert GameSessions im App-Dokumentenverzeichnis + /// Writes the game session list to a JSON file and returns it as string. + static String getJsonFile() { + final jsonFile = + Globals.gameList.map((session) => session.toJson()).toList(); + return json.encode(jsonFile); + } + + /// Returns the path to the local JSON file. + static Future _getFilePath() async { + final directory = await getApplicationDocumentsDirectory(); + final path = '${directory.path}/$_fileName'; + return File(path); + } + + /// Saves the game sessions to a local JSON file. static Future saveGameSessions() async { try { - final file = await _getLocalFile(); + final file = await _getFilePath(); final jsonFile = getJsonFile(); await file.writeAsString(jsonFile); print('Daten gespeichert'); @@ -20,44 +37,82 @@ class LocalStorageService { } } - /// Lädt GameSessions aus dem App-Dokumentenverzeichnis + /// Loads the game data from a local JSON file. static Future loadGameSessions() async { - print('Versuche, Daten zu laden...'); // FIXME Debug-Ausgabe + print('Versuche, Daten zu laden...'); try { - final file = await _getLocalFile(); + final file = await _getFilePath(); if (await file.exists()) { - print('Datei existiert'); // FIXME Debug-Ausgabe + print('Es existiert bereits eine Datei mit Spieldaten'); final jsonString = await file.readAsString(); if (jsonString.isNotEmpty) { - print('Datei ist nicht leer'); // FIXME Debug-Ausgabe + print('Die Datei ist nicht leer'); final jsonList = json.decode(jsonString) as List; - print('JSON: $jsonList'); // FIXME Debug-Ausgabe - Globals.gameList = - jsonList.map((json) => GameSession.fromJson(json)).toList(); - print('Daten erfolgreich geladen'); + print('JSON: $jsonList'); + Globals.gameList = jsonList + .map((jsonItem) => + GameSession.fromJson(jsonItem as Map)) + .toList() + .cast(); // Explicit cast to List + print('Die Daten wurden erfolgreich geladen'); } else { - print('Datei ist leer'); + print('Die Datei ist leer'); } } else { - print('Datei existiert nicht'); + print('Es existiert bisher noch keine Datei mit Spieldaten'); } } catch (e) { - print('Fehler beim Laden: $e'); - // Bei Fehler eine leere Liste setzen + print('Fehler beim Laden der Spieldaten:\n$e'); Globals.gameList = []; } } - static String getJsonFile() { - final jsonFile = - Globals.gameList.map((session) => session.toJson()).toList(); - return json.encode(jsonFile); + /// Opens the file picker to save a JSON file with the current game data. + static Future exportJsonFile() async { + final jsonString = getJsonFile(); + try { + final bytes = Uint8List.fromList(utf8.encode(jsonString)); + final result = await FileSaver.instance.saveAs( + name: 'cabo_counter_data', + bytes: bytes, + ext: 'json', + mimeType: MimeType.json, + ); + print('Datei gespeichert: $result'); + } catch (e) { + print('Fehler beim Speichern: $e'); + } } - static Future _getLocalFile() async { - final directory = await getApplicationDocumentsDirectory(); - final path = '${directory.path}/$_fileName'; - print('Speicherpfad: $path'); // FIXME Debug-Ausgabe - return File(path); + static Future importJsonFile() async { + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + String jsonString = ''; + if (result != null) { + if (result.files.single.bytes != null) { + final Uint8List fileBytes = result.files.single.bytes!; + jsonString = utf8.decode(fileBytes); + } else if (result.files.single.path != null) { + final file = File(result.files.single.path!); + jsonString = await file.readAsString(); + } + final jsonList = json.decode(jsonString) as List; + print('JSON Inhalt: $jsonList'); + Globals.gameList = jsonList + .map((jsonItem) => + GameSession.fromJson(jsonItem as Map)) + .toList(); + return true; + } else { + print('Der Dialog wurde abgebrochen'); + return false; + } + } catch (e) { + print('Fehler beim Importieren: $e'); + return false; + } } } diff --git a/lib/views/information_view.dart b/lib/views/information_view.dart index d64f2ff..04b51a4 100644 --- a/lib/views/information_view.dart +++ b/lib/views/information_view.dart @@ -1,3 +1,4 @@ +import 'package:cabo_counter/utility/local_storage_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -83,7 +84,10 @@ class InformationView extends StatelessWidget { ), CupertinoButton( child: const Text('Spieldaten exportieren'), - onPressed: () => {saveJsonToDevice()}) + onPressed: () => LocalStorageService.exportJsonFile()), + CupertinoButton( + child: const Text('Spieldaten importieren'), + onPressed: () => LocalStorageService.importJsonFile()), ], ), Positioned( @@ -115,8 +119,4 @@ class InformationView extends StatelessWidget { ], ))); } - - void saveJsonToDevice() async { - //todo: implement - } } diff --git a/pubspec.yaml b/pubspec.yaml index 76baaef..36e7f73 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.1.3+103 +version: 0.1.3+107 environment: sdk: ^3.5.4 @@ -17,6 +17,9 @@ dependencies: flutter_keyboard_visibility: ^6.0.0 path_provider: ^2.1.1 file_picker: any + file_saver: ^0.2.6 + typed_data: ^1.3.2 + dev_dependencies: flutter_test: From 6ad69671db13554063e5ab4bd38aabf3dde75195 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 1 May 2025 00:56:13 +0200 Subject: [PATCH 13/16] Added AlertDialogs for user feedback --- lib/utility/local_storage_service.dart | 8 ++++-- lib/views/information_view.dart | 39 ++++++++++++++++++++++++-- pubspec.yaml | 16 +++++------ 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/lib/utility/local_storage_service.dart b/lib/utility/local_storage_service.dart index ce9c762..ac00835 100644 --- a/lib/utility/local_storage_service.dart +++ b/lib/utility/local_storage_service.dart @@ -68,7 +68,7 @@ class LocalStorageService { } /// Opens the file picker to save a JSON file with the current game data. - static Future exportJsonFile() async { + static Future exportJsonFile() async { final jsonString = getJsonFile(); try { final bytes = Uint8List.fromList(utf8.encode(jsonString)); @@ -79,14 +79,18 @@ class LocalStorageService { mimeType: MimeType.json, ); print('Datei gespeichert: $result'); + return true; } catch (e) { print('Fehler beim Speichern: $e'); + return false; } } + /// Opens the file picker to import a JSON file and loads the game data from it. static Future importJsonFile() async { try { final result = await FilePicker.platform.pickFiles( + dialogTitle: 'Wähle eine Datei mit Spieldaten aus', type: FileType.custom, allowedExtensions: ['json'], ); @@ -108,7 +112,7 @@ class LocalStorageService { return true; } else { print('Der Dialog wurde abgebrochen'); - return false; + return true; } } catch (e) { print('Fehler beim Importieren: $e'); diff --git a/lib/views/information_view.dart b/lib/views/information_view.dart index 04b51a4..6b99e4c 100644 --- a/lib/views/information_view.dart +++ b/lib/views/information_view.dart @@ -83,11 +83,46 @@ class InformationView extends StatelessWidget { ], ), CupertinoButton( + sizeStyle: CupertinoButtonSize.medium, child: const Text('Spieldaten exportieren'), - onPressed: () => LocalStorageService.exportJsonFile()), + onPressed: () async => + await LocalStorageService.exportJsonFile() + ? null + : showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: const Text('Fehler'), + content: const Text( + 'Datei konnte nicht exportiert werden.'), + actions: [ + CupertinoDialogAction( + child: const Text('OK'), + onPressed: () => + Navigator.pop(context), + ), + ], + ))), CupertinoButton( + sizeStyle: CupertinoButtonSize.medium, child: const Text('Spieldaten importieren'), - onPressed: () => LocalStorageService.importJsonFile()), + onPressed: () async => { + await LocalStorageService.importJsonFile() + ? null + : showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: const Text('Fehler'), + content: const Text( + 'Datei konnte nicht importiert werden.'), + actions: [ + CupertinoDialogAction( + child: const Text('OK'), + onPressed: () => + Navigator.pop(context), + ), + ], + )) + }), ], ), Positioned( diff --git a/pubspec.yaml b/pubspec.yaml index 36e7f73..f298aa6 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.1.3+107 +version: 0.1.5+108 environment: sdk: ^3.5.4 @@ -11,20 +11,18 @@ dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.8 - font_awesome_flutter: ^10.8.0 - url_launcher: any - package_info_plus: any - flutter_keyboard_visibility: ^6.0.0 - path_provider: ^2.1.1 - file_picker: any + file_picker: ^10.1.2 file_saver: ^0.2.6 + flutter_keyboard_visibility: ^6.0.0 + font_awesome_flutter: ^10.8.0 + package_info_plus: any + path_provider: ^2.1.1 typed_data: ^1.3.2 - + url_launcher: any dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 test: ^1.25.15 From cd6931093e4483f1862877ea95d9d63b3fd645f2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 1 May 2025 13:06:49 +0200 Subject: [PATCH 14/16] Refactoring & toString method added --- lib/data/game_session.dart | 16 ++++++++-------- lib/data/round.dart | 6 ++++++ lib/main.dart | 16 ++++++++-------- lib/views/create_game_view.dart | 2 +- lib/views/main_menu_view.dart | 2 +- 5 files changed, 24 insertions(+), 18 deletions(-) diff --git a/lib/data/game_session.dart b/lib/data/game_session.dart index d021ceb..36629f8 100644 --- a/lib/data/game_session.dart +++ b/lib/data/game_session.dart @@ -3,7 +3,7 @@ import 'package:cabo_counter/data/round.dart'; /// This class represents a game session for Cabo game. /// [createdAt] is the timestamp of when the game session was created. /// [gameTitle] is the title of the game. -/// [gameHasPointLimit] is a boolean indicating if the game has the default +/// [isPointsLimitEnabled] is a boolean indicating if the game has the default /// point limit of 101 points or not. /// [players] is a string list of player names. /// [playerScores] is a list of the summed scores of all players. @@ -13,7 +13,7 @@ import 'package:cabo_counter/data/round.dart'; class GameSession { final DateTime createdAt = DateTime.now(); final String gameTitle; - final bool gameHasPointLimit; + final bool isPointsLimitEnabled; final List players; late List playerScores; List roundList = []; @@ -23,16 +23,16 @@ class GameSession { GameSession({ required this.gameTitle, - required this.gameHasPointLimit, + required this.isPointsLimitEnabled, required this.players, }) { playerScores = List.filled(players.length, 0); } @override - String toString() { + toString() { return ('GameSession: [createdAt: $createdAt, gameTitle: $gameTitle, ' - 'gameHasPointLimit: $gameHasPointLimit, players: $players, ' + 'isPointsLimitEnabled: $isPointsLimitEnabled, players: $players, ' 'playerScores: $playerScores, roundList: $roundList, ' 'roundNumber: $roundNumber, isGameFinished: $isGameFinished, ' 'winner: $winner]'); @@ -41,7 +41,7 @@ class GameSession { /// Converts the GameSession object to a JSON map. Map toJson() => { 'gameTitle': gameTitle, - 'gameHasPointLimit': gameHasPointLimit, + 'gameHasPointLimit': isPointsLimitEnabled, 'players': players, 'playerScores': playerScores, 'roundNumber': roundNumber, @@ -53,7 +53,7 @@ class GameSession { /// Creates a GameSession object from a JSON map. GameSession.fromJson(Map json) : gameTitle = json['gameTitle'], - gameHasPointLimit = json['gameHasPointLimit'], + isPointsLimitEnabled = json['gameHasPointLimit'], players = List.from(json['players']), playerScores = List.from(json['playerScores']), roundNumber = json['roundNumber'], @@ -208,7 +208,7 @@ class GameSession { /// the winner. void updatePoints() { _sumPoints(); - if (gameHasPointLimit) { + if (isPointsLimitEnabled) { _checkHundredPointsReached(); for (int i = 0; i < playerScores.length; i++) { diff --git a/lib/data/round.dart b/lib/data/round.dart index d98a4f4..0c5b456 100644 --- a/lib/data/round.dart +++ b/lib/data/round.dart @@ -19,6 +19,12 @@ class Round { required this.scoreUpdates, this.kamikazePlayerIndex}); + @override + toString() { + return 'Round $roundNum: scores: $scores, scoreUpdates: $scoreUpdates, ' + 'kamikazePlayerIndex: $kamikazePlayerIndex'; + } + /// Converts the Round object to a JSON map. Map toJson() => { 'roundNum': roundNum, diff --git a/lib/main.dart b/lib/main.dart index dfe283a..3ef2867 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -11,36 +11,36 @@ void main() { Globals.addGameSession(GameSession( gameTitle: 'Spiel am 27.02.2025', players: ['Clara', 'Tobias', 'Yannik', 'Lena', 'Lekaia'], - gameHasPointLimit: true)); + isPointsLimitEnabled: true)); Globals.addGameSession(GameSession( gameTitle: 'Freundschaftsrunde', players: ['Felix', 'Jonas', 'Nils'], - gameHasPointLimit: false)); + isPointsLimitEnabled: false)); Globals.addGameSession(GameSession( gameTitle: 'Familienabend', players: ['Mama', 'Papa', 'Lisa'], - gameHasPointLimit: true, + isPointsLimitEnabled: true, )); Globals.addGameSession(GameSession( gameTitle: 'Turnier 1. Runde', players: ['Tim', 'Max', 'Sophie', 'Lena'], - gameHasPointLimit: false)); + isPointsLimitEnabled: false)); Globals.addGameSession(GameSession( gameTitle: '2 Namen max length', players: ['Heinrich', 'Johannes'], - gameHasPointLimit: true)); + isPointsLimitEnabled: true)); Globals.addGameSession(GameSession( gameTitle: '3 Namen max length', players: ['Benjamin', 'Stefanie', 'Wolfgang'], - gameHasPointLimit: false)); + isPointsLimitEnabled: false)); Globals.addGameSession(GameSession( gameTitle: '4 Namen max length', players: ['Leonhard', 'Mathilde', 'Bernhard', 'Gerlinde'], - gameHasPointLimit: true)); + isPointsLimitEnabled: true)); Globals.addGameSession(GameSession( gameTitle: '5 Namen max length', players: ['Hartmuth', 'Elisabet', 'Rosalind', 'Theresia', 'Karoline'], - gameHasPointLimit: false)); + isPointsLimitEnabled: false)); runApp(const App()); } diff --git a/lib/views/create_game_view.dart b/lib/views/create_game_view.dart index 210e4b2..e07685a 100644 --- a/lib/views/create_game_view.dart +++ b/lib/views/create_game_view.dart @@ -284,7 +284,7 @@ class _CreateGameState extends State { GameSession gameSession = GameSession( gameTitle: _gameTitleTextController.text, players: players, - gameHasPointLimit: selectedMode!, + isPointsLimitEnabled: selectedMode!, ); Globals.addGameSession(gameSession); LocalStorageService.saveGameSessions(); diff --git a/lib/views/main_menu_view.dart b/lib/views/main_menu_view.dart index 611822e..fd3f33f 100644 --- a/lib/views/main_menu_view.dart +++ b/lib/views/main_menu_view.dart @@ -97,7 +97,7 @@ class _MainMenuViewState extends State { style: const TextStyle(fontSize: 14), ) : Text( - 'Modus: ${_translateGameMode(session.gameHasPointLimit)}', + 'Modus: ${_translateGameMode(session.isPointsLimitEnabled)}', style: const TextStyle(fontSize: 14), ), trailing: Row( From 966179d01de442f7879720cb38a8d45b3a70bee6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 2 May 2025 11:07:44 +0200 Subject: [PATCH 15/16] Added mounted check to popups --- lib/views/information_view.dart | 78 +++++++++++++++++---------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/views/information_view.dart b/lib/views/information_view.dart index 6b99e4c..0236427 100644 --- a/lib/views/information_view.dart +++ b/lib/views/information_view.dart @@ -83,46 +83,50 @@ class InformationView extends StatelessWidget { ], ), CupertinoButton( - sizeStyle: CupertinoButtonSize.medium, - child: const Text('Spieldaten exportieren'), - onPressed: () async => - await LocalStorageService.exportJsonFile() - ? null - : showCupertinoDialog( - context: context, - builder: (context) => CupertinoAlertDialog( - title: const Text('Fehler'), - content: const Text( - 'Datei konnte nicht exportiert werden.'), - actions: [ - CupertinoDialogAction( - child: const Text('OK'), - onPressed: () => - Navigator.pop(context), - ), - ], - ))), + sizeStyle: CupertinoButtonSize.medium, + child: const Text('Spieldaten exportieren'), + onPressed: () async { + final success = await LocalStorageService.exportJsonFile(); + if (!success && context.mounted) { + showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: const Text('Fehler'), + content: const Text( + 'Datei konnte nicht exportiert werden.'), + actions: [ + CupertinoDialogAction( + child: const Text('OK'), + onPressed: () => Navigator.pop(context), + ), + ], + ), + ); + } + }, + ), CupertinoButton( sizeStyle: CupertinoButtonSize.medium, child: const Text('Spieldaten importieren'), - onPressed: () async => { - await LocalStorageService.importJsonFile() - ? null - : showCupertinoDialog( - context: context, - builder: (context) => CupertinoAlertDialog( - title: const Text('Fehler'), - content: const Text( - 'Datei konnte nicht importiert werden.'), - actions: [ - CupertinoDialogAction( - child: const Text('OK'), - onPressed: () => - Navigator.pop(context), - ), - ], - )) - }), + onPressed: () async { + final success = + await LocalStorageService.importJsonFile(); + if (!success && context.mounted) { + showCupertinoDialog( + context: context, + builder: (context) => CupertinoAlertDialog( + title: const Text('Fehler'), + content: const Text( + 'Datei konnte nicht importiert werden.'), + actions: [ + CupertinoDialogAction( + child: const Text('OK'), + onPressed: () => Navigator.pop(context), + ), + ], + )); + } + }), ], ), Positioned( From 739bc6a34c6e1716dc173a146f3eca0949e4a468 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 2 May 2025 11:42:32 +0200 Subject: [PATCH 16/16] Small changes --- lib/utility/local_storage_service.dart | 5 ++--- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/utility/local_storage_service.dart b/lib/utility/local_storage_service.dart index ac00835..661b139 100644 --- a/lib/utility/local_storage_service.dart +++ b/lib/utility/local_storage_service.dart @@ -46,14 +46,13 @@ class LocalStorageService { print('Es existiert bereits eine Datei mit Spieldaten'); final jsonString = await file.readAsString(); if (jsonString.isNotEmpty) { - print('Die Datei ist nicht leer'); + print('Die gefundene Datei ist nicht leer'); final jsonList = json.decode(jsonString) as List; - print('JSON: $jsonList'); Globals.gameList = jsonList .map((jsonItem) => GameSession.fromJson(jsonItem as Map)) .toList() - .cast(); // Explicit cast to List + .cast(); print('Die Daten wurden erfolgreich geladen'); } else { print('Die Datei ist leer'); diff --git a/pubspec.yaml b/pubspec.yaml index f298aa6..a20057a 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.1.5+108 +version: 0.1.5+110 environment: sdk: ^3.5.4