From 310b9aa43bf356731e3a5188e03c91ee66306c9d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 22:10:02 +0100 Subject: [PATCH 01/19] Implemented StatisticsWidget tile --- .../widgets/tiles/statistics_tile.dart | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 lib/presentation/widgets/tiles/statistics_tile.dart diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart new file mode 100644 index 0000000..0d01159 --- /dev/null +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -0,0 +1,100 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; + +class StatisticsTile extends StatelessWidget { + const StatisticsTile({ + super.key, + required this.icon, + required this.title, + required this.width, + required this.values, + required this.itemCount, + required this.barColor, + }); + + final IconData icon; + final String title; + final double width; + final List<(String, int)> values; + final int itemCount; + final Color barColor; + + @override + Widget build(BuildContext context) { + final maxBarWidth = MediaQuery.of(context).size.width * 0.7; + + return InfoTile( + width: width, + title: title, + icon: icon, + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Visibility( + visible: values.isNotEmpty, + replacement: const Center( + heightFactor: 4, + child: Text('No data available.'), + ), + child: Column( + children: List.generate(min(values.length, itemCount), (index) { + /// The maximum wins among all players + final maxGames = values.isNotEmpty ? values[0].$2 : 0; + + /// Fraction of wins + final double fraction = (maxGames > 0) + ? (values[index].$2 / maxGames) + : 0.0; + + /// Calculated width for current the bar + final double barWidth = maxBarWidth * fraction; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + height: 24, + width: barWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: barColor, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + values[index].$1, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Spacer(), + Center( + child: Text( + values[index].$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + }), + ), + ), + ), + ); + } +} From b2036e4e6811439da75dc5d0b3a15af2b41fb553 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 22:10:16 +0100 Subject: [PATCH 02/19] Implemented first version of statistics view --- .../views/main_menu/statistics_view.dart | 199 +++++++++++++++++- 1 file changed, 196 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 84ccf77..fc7b262 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -1,10 +1,203 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/game.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart'; +import 'package:provider/provider.dart'; +import 'package:skeletonizer/skeletonizer.dart'; -class StatisticsView extends StatelessWidget { +class StatisticsView extends StatefulWidget { const StatisticsView({super.key}); + @override + State createState() => _StatisticsViewState(); +} + +class _StatisticsViewState extends State { + late Future> _gamesFuture; + late Future> _playersFuture; + List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 5)); + List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 5)); + + bool isLoading = true; + + @override + void initState() { + super.initState(); + final db = Provider.of(context, listen: false); + _gamesFuture = db.gameDao.getAllGames(); + _playersFuture = db.playerDao.getAllPlayers(); + + Future.wait([_gamesFuture, _playersFuture]).then((results) async { + await Future.delayed(const Duration(milliseconds: 500)); + final games = results[0] as List; + final players = results[1] as List; + winCounts = _calculateWinsForAllPlayers(games, players); + gameCounts = _calculateGameAmountsForAllPlayers(games, players); + if (mounted) { + setState(() { + isLoading = false; + }); + } + }); + } + @override Widget build(BuildContext context) { - return const Center(child: Text('Statistics View')); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Skeletonizer( + effect: PulseEffect( + from: Colors.grey[800]!, + to: Colors.grey[600]!, + duration: const Duration(milliseconds: 800), + ), + enabled: isLoading, + enableSwitchAnimation: true, + switchAnimationConfig: const SwitchAnimationConfig( + duration: Duration(milliseconds: 200), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, + layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder, + ), + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: constraints.maxHeight * 0.01), + StatisticsTile( + icon: Icons.sports_score, + title: 'Wins per Player', + width: constraints.maxWidth * 0.95, + values: winCounts, + itemCount: 6, + barColor: Colors.blue, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: 'Games per Player', + width: constraints.maxWidth * 0.95, + values: gameCounts, + itemCount: 6, + barColor: Colors.green, + ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), + ], + ), + ), + ), + ); + }, + ); + } + + /// Calculates the number of wins for each player + /// and returns a sorted list of tuples (playerName, winCount) + List<(String, int)> _calculateWinsForAllPlayers( + List games, + List players, + ) { + List<(String, int)> winCounts = []; + + // Getting the winners + for (var game in games) { + final winner = game.winner; + print('Game: ${game.id}, Winner: $winner'); + if (winner != null && winner.isNotEmpty) { + final index = winCounts.indexWhere((entry) => entry.$1 == winner); + if (index != -1) { + final current = winCounts[index].$2; + winCounts[index] = (winner, current + 1); + } else { + winCounts.add((winner, 1)); + } + } + } + + // Adding all players with zero wins + for (var player in players) { + final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + if (index == -1) { + winCounts.add((player.id, 0)); + } + } + + // Replace player IDs with names + for (int i = 0; i < winCounts.length; i++) { + final playerId = winCounts[i].$1; + final player = players.firstWhere( + (p) => p.id == playerId, + orElse: () => Player(id: playerId, name: 'N.a.'), + ); + winCounts[i] = (player.name, winCounts[i].$2); + } + + winCounts.sort((a, b) => b.$2.compareTo(a.$2)); + + return winCounts; + } + + /// Calculates the number of games played for each player + /// and returns a sorted list of tuples (playerName, gameCount) + List<(String, int)> _calculateGameAmountsForAllPlayers( + List games, + List players, + ) { + List<(String, int)> gameCounts = []; + + // Counting games for each player + for (var game in games) { + if (game.group != null) { + final members = game.group!.members.map((p) => p.id).toList(); + for (var playerId in members) { + final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + if (index != -1) { + final current = gameCounts[index].$2; + gameCounts[index] = (playerId, current + 1); + } else { + gameCounts.add((playerId, 1)); + } + } + } + if (game.players != null) { + final members = game.players!.map((p) => p.id).toList(); + for (var playerId in members) { + final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + if (index != -1) { + final current = gameCounts[index].$2; + gameCounts[index] = (playerId, current + 1); + } else { + gameCounts.add((playerId, 1)); + } + } + } + } + + // Adding all players with zero games + for (var player in players) { + final index = gameCounts.indexWhere((entry) => entry.$1 == player.id); + if (index == -1) { + gameCounts.add((player.id, 0)); + } + } + + // Replace player IDs with names + for (int i = 0; i < gameCounts.length; i++) { + final playerId = gameCounts[i].$1; + final player = players.firstWhere( + (p) => p.id == playerId, + orElse: () => Player(id: playerId, name: 'N.a.'), + ); + gameCounts[i] = (player.name, gameCounts[i].$2); + } + + gameCounts.sort((a, b) => b.$2.compareTo(a.$2)); + + return gameCounts; } } From 546a3e37174b4187e4e2d9c48b55e6bbba69f0d4 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:49:17 +0100 Subject: [PATCH 03/19] implemented feature to automatically add newly created player to selected players --- .../views/main_menu/create_group_view.dart | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/lib/presentation/views/main_menu/create_group_view.dart b/lib/presentation/views/main_menu/create_group_view.dart index c54369e..75fdb83 100644 --- a/lib/presentation/views/main_menu/create_group_view.dart +++ b/lib/presentation/views/main_menu/create_group_view.dart @@ -49,8 +49,7 @@ class _CreateGroupViewState extends State { @override void dispose() { _groupNameController.dispose(); - _searchBarController - .dispose(); // Listener entfernen und Controller aufräumen + _searchBarController.dispose(); super.dispose(); } @@ -123,12 +122,7 @@ class _CreateGroupViewState extends State { .trim() .isNotEmpty, onTrailingButtonPressed: () async { - addNewPlayerFromSearch( - context: context, - searchBarController: _searchBarController, - db: db, - loadPlayerList: loadPlayerList, - ); + addNewPlayerFromSearch(context: context); }, onChanged: (value) { setState(() { @@ -339,48 +333,47 @@ class _CreateGroupViewState extends State { ), ); } -} -/// Adds a new player to the database from the search bar input. -/// Shows a snackbar indicating success or failure. -/// [context] - BuildContext to show the snackbar. -/// [searchBarController] - TextEditingController of the search bar. -/// [db] - AppDatabase instance to interact with the database. -/// [loadPlayerList] - Function to reload the player list after adding. -void addNewPlayerFromSearch({ - required BuildContext context, - required TextEditingController searchBarController, - required AppDatabase db, - required Function loadPlayerList, -}) async { - String playerName = searchBarController.text.trim(); - bool success = await db.playerDao.addPlayer(player: Player(name: playerName)); - if (!context.mounted) return; - if (success) { - loadPlayerList(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: CustomTheme.boxColor, - content: Center( - child: Text( - 'Successfully added player $playerName.', - style: const TextStyle(color: Colors.white), + /// Adds a new player to the database from the search bar input. + /// Shows a snackbar indicating success or failure. + /// [context] - BuildContext to show the snackbar. + void addNewPlayerFromSearch({required BuildContext context}) async { + String playerName = _searchBarController.text.trim(); + Player createdPlayer = Player(name: playerName); + bool success = await db.playerDao.addPlayer(player: createdPlayer); + if (!context.mounted) return; + if (success) { + selectedPlayers.add(createdPlayer); + allPlayers.add(createdPlayer); + setState(() { + _searchBarController.clear(); + suggestedPlayers = allPlayers.where((player) { + return !selectedPlayers.contains(player); + }).toList(); + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomTheme.boxColor, + content: Center( + child: Text( + 'Successfully added player $playerName.', + style: const TextStyle(color: Colors.white), + ), ), ), - ), - ); - searchBarController.clear(); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: CustomTheme.boxColor, - content: Center( - child: Text( - 'Could not add player $playerName.', - style: const TextStyle(color: Colors.white), + ); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomTheme.boxColor, + content: Center( + child: Text( + 'Could not add player $playerName.', + style: const TextStyle(color: Colors.white), + ), ), ), - ), - ); + ); + } } } From cc04e0555768396560f28b20f108a6a0a5d29d02 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:49:28 +0100 Subject: [PATCH 04/19] Adjust bottom padding in GroupsView list based on media query padding --- lib/presentation/views/main_menu/groups_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index 4bf9cba..a23a615 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -93,7 +93,9 @@ class _GroupsViewState extends State { itemCount: groups.length + 1, itemBuilder: (BuildContext context, int index) { if (index == groups.length) { - return const SizedBox(height: 60); + return SizedBox( + height: MediaQuery.paddingOf(context).bottom - 20, + ); } return GroupTile(group: groups[index]); }, From 692b412fe2e3d7d727717aa36bd24ad29a5cd349 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:52:09 +0100 Subject: [PATCH 05/19] Fix black bars on the screens bottom and top by not wrapping scaffold in safearea, but scaffolds body children --- .../views/main_menu/create_group_view.dart | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/presentation/views/main_menu/create_group_view.dart b/lib/presentation/views/main_menu/create_group_view.dart index 75fdb83..b077bbc 100644 --- a/lib/presentation/views/main_menu/create_group_view.dart +++ b/lib/presentation/views/main_menu/create_group_view.dart @@ -66,19 +66,19 @@ class _CreateGroupViewState extends State { @override Widget build(BuildContext context) { - return SafeArea( - child: Scaffold( + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - backgroundColor: CustomTheme.backgroundColor, - scrolledUnderElevation: 0, - title: const Text( - 'Create new group', - style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), - ), - centerTitle: true, + scrolledUnderElevation: 0, + title: const Text( + 'Create new group', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), ), - body: Column( + centerTitle: true, + ), + body: SafeArea( + child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ Container( From c170aa17752f808954b85d6270e33df46d91cad8 Mon Sep 17 00:00:00 2001 From: mathiskirchner Date: Sat, 22 Nov 2025 22:55:19 +0100 Subject: [PATCH 06/19] sort groups by creation date in GroupsView --- lib/presentation/views/main_menu/groups_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index a23a615..e9af8b9 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -69,9 +69,9 @@ class _GroupsViewState extends State { } final bool isLoading = snapshot.connectionState == ConnectionState.waiting; - final List groups = isLoading - ? skeletonData - : (snapshot.data ?? []); + final List groups = + isLoading ? skeletonData : (snapshot.data ?? []) + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); return Skeletonizer( effect: PulseEffect( from: Colors.grey[800]!, From 59c041699dc3edd983959f207b33470b9a6673af Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:20:31 +0100 Subject: [PATCH 07/19] Changed values attribute & maxBarWidth --- lib/presentation/widgets/tiles/statistics_tile.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 0d01159..279c492 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -17,13 +17,13 @@ class StatisticsTile extends StatelessWidget { final IconData icon; final String title; final double width; - final List<(String, int)> values; + final List<(String, num)> values; final int itemCount; final Color barColor; @override Widget build(BuildContext context) { - final maxBarWidth = MediaQuery.of(context).size.width * 0.7; + final maxBarWidth = MediaQuery.of(context).size.width * 0.65; return InfoTile( width: width, From e60961730f64ec78f2ab17da15297990bbdca4d0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:20:58 +0100 Subject: [PATCH 08/19] Added new metric & changed layout builder of Skeletonizer --- .../views/main_menu/statistics_view.dart | 92 +++++++++++++++---- 1 file changed, 72 insertions(+), 20 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index fc7b262..2a8dedf 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -16,9 +16,9 @@ class StatisticsView extends StatefulWidget { class _StatisticsViewState extends State { late Future> _gamesFuture; late Future> _playersFuture; - List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 5)); - List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 5)); - + List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1)); + List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 1)); + List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1)); bool isLoading = true; @override @@ -29,11 +29,12 @@ class _StatisticsViewState extends State { _playersFuture = db.playerDao.getAllPlayers(); Future.wait([_gamesFuture, _playersFuture]).then((results) async { - await Future.delayed(const Duration(milliseconds: 500)); + await Future.delayed(const Duration(milliseconds: 200)); final games = results[0] as List; final players = results[1] as List; winCounts = _calculateWinsForAllPlayers(games, players); gameCounts = _calculateGameAmountsForAllPlayers(games, players); + winRates = computeWinRatePercent(wins: winCounts, games: gameCounts); if (mounted) { setState(() { isLoading = false; @@ -46,22 +47,31 @@ class _StatisticsViewState extends State { Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return Skeletonizer( - effect: PulseEffect( - from: Colors.grey[800]!, - to: Colors.grey[600]!, - duration: const Duration(milliseconds: 800), - ), - enabled: isLoading, - enableSwitchAnimation: true, - switchAnimationConfig: const SwitchAnimationConfig( - duration: Duration(milliseconds: 200), - switchInCurve: Curves.linear, - switchOutCurve: Curves.linear, - transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, - layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder, - ), - child: SingleChildScrollView( + return SingleChildScrollView( + child: Skeletonizer( + effect: PulseEffect( + from: Colors.grey[800]!, + to: Colors.grey[600]!, + duration: const Duration(milliseconds: 800), + ), + enabled: isLoading, + enableSwitchAnimation: true, + switchAnimationConfig: SwitchAnimationConfig( + duration: const Duration(milliseconds: 1000), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, + layoutBuilder: + (Widget? currentChild, List previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + ), child: ConstrainedBox( constraints: BoxConstraints(minWidth: constraints.maxWidth), child: Column( @@ -78,6 +88,15 @@ class _StatisticsViewState extends State { barColor: Colors.blue, ), SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: 'Winrate per Player', + width: constraints.maxWidth * 0.95, + values: winRates, + itemCount: 6, + barColor: Colors.orange[700]!, + ), + SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( icon: Icons.casino, title: 'Games per Player', @@ -86,6 +105,7 @@ class _StatisticsViewState extends State { itemCount: 6, barColor: Colors.green, ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), @@ -200,4 +220,36 @@ class _StatisticsViewState extends State { return gameCounts; } + + // dart + List<(String, double)> computeWinRatePercent({ + required List<(String, int)> wins, // [(name, wins)] + required List<(String, int)> games, // [(name, games)] + }) { + final Map winsMap = {for (var e in wins) e.$1: e.$2}; + final Map gamesMap = {for (var e in games) e.$1: e.$2}; + + final names = {...winsMap.keys, ...gamesMap.keys}; + + final result = names.map((name) { + final int w = winsMap[name] ?? 0; + final int g = gamesMap[name] ?? 0; + final double percent = (g > 0) + ? double.parse(((w / g)).toStringAsFixed(2)) + : 0; + return (name, percent); + }).toList(); + + // Sort the result: first by winrate descending, + // then by wins descending in case of a tie + result.sort((a, b) { + final cmp = b.$2.compareTo(a.$2); + if (cmp != 0) return cmp; + final wa = winsMap[a.$1] ?? 0; + final wb = winsMap[b.$1] ?? 0; + return wb.compareTo(wa); + }); + + return result; + } } From fba35521cbcfddfc401c91c67f344ba4b6203ab5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:23:02 +0100 Subject: [PATCH 09/19] changed skeletonizer transition duration back to normal --- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 2a8dedf..a365b2e 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -57,7 +57,7 @@ class _StatisticsViewState extends State { enabled: isLoading, enableSwitchAnimation: true, switchAnimationConfig: SwitchAnimationConfig( - duration: const Duration(milliseconds: 1000), + duration: const Duration(milliseconds: 200), switchInCurve: Curves.linear, switchOutCurve: Curves.linear, transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, From feb5fa061557bc8b4fa9d206bde076fc7e559095 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 22 Nov 2025 23:30:24 +0100 Subject: [PATCH 10/19] Docs, small changes --- .../views/main_menu/statistics_view.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index a365b2e..8830118 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -84,7 +84,7 @@ class _StatisticsViewState extends State { title: 'Wins per Player', width: constraints.maxWidth * 0.95, values: winCounts, - itemCount: 6, + itemCount: 3, barColor: Colors.blue, ), SizedBox(height: constraints.maxHeight * 0.02), @@ -93,7 +93,7 @@ class _StatisticsViewState extends State { title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, - itemCount: 6, + itemCount: 5, barColor: Colors.orange[700]!, ), SizedBox(height: constraints.maxHeight * 0.02), @@ -102,7 +102,7 @@ class _StatisticsViewState extends State { title: 'Games per Player', width: constraints.maxWidth * 0.95, values: gameCounts, - itemCount: 6, + itemCount: 10, barColor: Colors.green, ), @@ -127,7 +127,6 @@ class _StatisticsViewState extends State { // Getting the winners for (var game in games) { final winner = game.winner; - print('Game: ${game.id}, Winner: $winner'); if (winner != null && winner.isNotEmpty) { final index = winCounts.indexWhere((entry) => entry.$1 == winner); if (index != -1) { @@ -223,17 +222,21 @@ class _StatisticsViewState extends State { // dart List<(String, double)> computeWinRatePercent({ - required List<(String, int)> wins, // [(name, wins)] - required List<(String, int)> games, // [(name, games)] + required List<(String, int)> wins, + required List<(String, int)> games, }) { final Map winsMap = {for (var e in wins) e.$1: e.$2}; final Map gamesMap = {for (var e in games) e.$1: e.$2}; + // Get all unique player names final names = {...winsMap.keys, ...gamesMap.keys}; + // Calculate win rates final result = names.map((name) { final int w = winsMap[name] ?? 0; final int g = gamesMap[name] ?? 0; + // Calculate percentage and round to 2 decimal places + // Avoid division by zero final double percent = (g > 0) ? double.parse(((w / g)).toStringAsFixed(2)) : 0; From fa841e328ec63a13655e43a22b7c24bf11a1eac1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:11 +0100 Subject: [PATCH 11/19] Altered game class --- lib/data/dto/game.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/data/dto/game.dart b/lib/data/dto/game.dart index 4188bc4..48ef902 100644 --- a/lib/data/dto/game.dart +++ b/lib/data/dto/game.dart @@ -9,7 +9,7 @@ class Game { final String name; final List? players; final Group? group; - final String? winner; + final Player? winner; Game({ String? id, @@ -17,7 +17,7 @@ class Game { required this.name, this.players, this.group, - this.winner = '', + this.winner, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(); @@ -37,7 +37,7 @@ class Game { .toList() : null, group = json['group'] != null ? Group.fromJson(json['group']) : null, - winner = json['winner'] ?? ''; + winner = json['winner'] != null ? Player.fromJson(json['winner']) : null; /// Converts the Game instance to a JSON object. Map toJson() => { @@ -46,6 +46,6 @@ class Game { 'name': name, 'players': players?.map((player) => player.toJson()).toList(), 'group': group?.toJson(), - 'winner': winner, + 'winner': winner?.toJson(), }; } From cfed05595ca9cbec81213b8128c52c9d442f923e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:27 +0100 Subject: [PATCH 12/19] Updated methods in gameDao --- lib/data/dao/game_dao.dart | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index a211849..18792b5 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -20,13 +20,16 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { result.map((row) async { final group = await db.groupGameDao.getGroupOfGame(gameId: row.id); final players = await db.playerGameDao.getPlayersOfGame(gameId: row.id); + final winner = row.winnerId != null + ? await db.playerDao.getPlayerById(playerId: row.winnerId!) + : null; return Game( id: row.id, name: row.name, group: group, players: players, createdAt: row.createdAt, - winner: row.winnerId, + winner: winner, ); }), ); @@ -45,13 +48,17 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { if (await db.groupGameDao.gameHasGroup(gameId: gameId)) { group = await db.groupGameDao.getGroupOfGame(gameId: gameId); } + Player? winner; + if (result.winnerId != null) { + winner = await db.playerDao.getPlayerById(playerId: result.winnerId!); + } return Game( id: result.id, name: result.name, players: players, group: group, - winner: result.winnerId, + winner: winner, createdAt: result.createdAt, ); } @@ -64,7 +71,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { GameTableCompanion.insert( id: game.id, name: game.name, - winnerId: Value(game.winner), + winnerId: Value(game.winner?.id), createdAt: game.createdAt, ), mode: InsertMode.insertOrReplace, @@ -100,7 +107,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { id: game.id, name: game.name, createdAt: game.createdAt, - winnerId: Value(game.winner), + winnerId: Value(game.winner?.id), ), ) .toList(), From 338f4294dc17ee4f2e1288a3bcc07b654ef5a073 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:31 +0100 Subject: [PATCH 13/19] Updated json schema --- assets/schema.json | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 1883122..c80915c 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -88,13 +88,12 @@ ] }, "winner": { - "type": ["string","null"] + "type": ["object","null"] }, "required": [ "id", "createdAt", - "name", - "winner" + "name" ] } ] From 82b344a145f829e7be92f42f6c05a140975fc79f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:17:48 +0100 Subject: [PATCH 14/19] Changed winner access in statistics view --- lib/presentation/views/main_menu/statistics_view.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 8830118..13e9aae 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -127,13 +127,13 @@ class _StatisticsViewState extends State { // Getting the winners for (var game in games) { final winner = game.winner; - if (winner != null && winner.isNotEmpty) { - final index = winCounts.indexWhere((entry) => entry.$1 == winner); + if (winner != null) { + final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); if (index != -1) { final current = winCounts[index].$2; - winCounts[index] = (winner, current + 1); + winCounts[index] = (winner.id, current + 1); } else { - winCounts.add((winner, 1)); + winCounts.add((winner.id, 1)); } } } From 4ff131770e205f4c112dc703330c7ec69b25ea8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 00:22:53 +0100 Subject: [PATCH 15/19] Adjust tests --- test/db_tests/game_test.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index 0d33c1e..4cf6982 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -50,15 +50,18 @@ void main() { name: 'First Test Game', group: testGroup1, players: [testPlayer4, testPlayer5], + winner: testPlayer4, ); testGame2 = Game( name: 'Second Test Game', group: testGroup2, players: [testPlayer1, testPlayer2, testPlayer3], + winner: testPlayer2, ); testGameOnlyPlayers = Game( name: 'Test Game with Players', players: [testPlayer1, testPlayer2, testPlayer3], + winner: testPlayer3, ); testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2); }); @@ -75,9 +78,16 @@ void main() { expect(result.id, testGame1.id); expect(result.name, testGame1.name); - expect(result.winner, testGame1.winner); expect(result.createdAt, testGame1.createdAt); + if (result.winner != null && testGame1.winner != null) { + expect(result.winner!.id, testGame1.winner!.id); + expect(result.winner!.name, testGame1.winner!.name); + expect(result.winner!.createdAt, testGame1.winner!.createdAt); + } else { + expect(result.winner, testGame1.winner); + } + if (result.group != null) { expect(result.group!.members.length, testGroup1.members.length); @@ -123,7 +133,13 @@ void main() { expect(game.id, testGame.id); expect(game.name, testGame.name); expect(game.createdAt, testGame.createdAt); - expect(game.winner, testGame.winner); + if (game.winner != null && testGame.winner != null) { + expect(game.winner!.id, testGame.winner!.id); + expect(game.winner!.name, testGame.winner!.name); + expect(game.winner!.createdAt, testGame.winner!.createdAt); + } else { + expect(game.winner, testGame.winner); + } // Group-Checks if (testGame.group != null) { From fee5c57207f9f1c77039a0111e117698e957f239 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:13:30 +0100 Subject: [PATCH 16/19] Added comments for return value -1 --- lib/presentation/views/main_menu/statistics_view.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 8830118..58dec3a 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -129,6 +129,7 @@ class _StatisticsViewState extends State { final winner = game.winner; if (winner != null && winner.isNotEmpty) { final index = winCounts.indexWhere((entry) => entry.$1 == winner); + // -1 means winner not found in winCounts if (index != -1) { final current = winCounts[index].$2; winCounts[index] = (winner, current + 1); @@ -141,6 +142,7 @@ class _StatisticsViewState extends State { // Adding all players with zero wins for (var player in players) { final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + // -1 means player not found in winCounts if (index == -1) { winCounts.add((player.id, 0)); } @@ -175,6 +177,7 @@ class _StatisticsViewState extends State { final members = game.group!.members.map((p) => p.id).toList(); for (var playerId in members) { final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + // -1 means player not found in gameCounts if (index != -1) { final current = gameCounts[index].$2; gameCounts[index] = (playerId, current + 1); @@ -187,6 +190,7 @@ class _StatisticsViewState extends State { final members = game.players!.map((p) => p.id).toList(); for (var playerId in members) { final index = gameCounts.indexWhere((entry) => entry.$1 == playerId); + // -1 means player not found in gameCounts if (index != -1) { final current = gameCounts[index].$2; gameCounts[index] = (playerId, current + 1); @@ -200,6 +204,7 @@ class _StatisticsViewState extends State { // Adding all players with zero games for (var player in players) { final index = gameCounts.indexWhere((entry) => entry.$1 == player.id); + // -1 means player not found in gameCounts if (index == -1) { gameCounts.add((player.id, 0)); } From d411f58134f256bfbb1e6aed73e10a9bc9ae8840 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:18:05 +0100 Subject: [PATCH 17/19] Changed icon for second statistics tile --- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 58dec3a..96e2203 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -89,7 +89,7 @@ class _StatisticsViewState extends State { ), SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( - icon: Icons.casino, + icon: Icons.percent, title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, From e9b041e43ac89fdf775e36704fddeac8700b96c7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:33:13 +0100 Subject: [PATCH 18/19] Changed double depiction --- lib/presentation/views/main_menu/statistics_view.dart | 3 +-- lib/presentation/widgets/tiles/statistics_tile.dart | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 7a6f861..56bdcf5 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -93,7 +93,7 @@ class _StatisticsViewState extends State { title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, - itemCount: 5, + itemCount: 115, barColor: Colors.orange[700]!, ), SizedBox(height: constraints.maxHeight * 0.02), @@ -105,7 +105,6 @@ class _StatisticsViewState extends State { itemCount: 10, barColor: Colors.green, ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 279c492..3692167 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -80,7 +80,9 @@ class StatisticsTile extends StatelessWidget { const Spacer(), Center( child: Text( - values[index].$2.toString(), + values[index].$2 <= 1 + ? values[index].$2.toStringAsFixed(2) + : values[index].$2.toString(), textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, From 7cda25a380af58b4714bb537ac59d0ec696d11df Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 23 Nov 2025 12:34:42 +0100 Subject: [PATCH 19/19] Changed item count back to normal --- lib/presentation/views/main_menu/statistics_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 56bdcf5..02eab46 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -93,7 +93,7 @@ class _StatisticsViewState extends State { title: 'Winrate per Player', width: constraints.maxWidth * 0.95, values: winRates, - itemCount: 115, + itemCount: 5, barColor: Colors.orange[700]!, ), SizedBox(height: constraints.maxHeight * 0.02),