From b305145d34cea6fb461971cd76ae90ba25671634 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 20 May 2026 15:15:47 +0200 Subject: [PATCH 01/34] implement basic player_detail_view.dart --- lib/data/dao/group_dao.dart | 32 ++ lib/data/dao/match_dao.dart | 47 +++ .../views/main_menu/player_detail_view.dart | 273 ++++++++++++++++++ .../widgets/tiles/group_tile.dart | 15 + .../widgets/tiles/match_tile.dart | 15 + .../widgets/tiles/text_icon_tile.dart | 85 +++--- test/db_tests/aggregates/group_test.dart | 25 ++ test/db_tests/aggregates/match_test.dart | 28 ++ 8 files changed, 481 insertions(+), 39 deletions(-) create mode 100644 lib/presentation/views/main_menu/player_detail_view.dart diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index bffe5a4..2de2ab9 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -185,6 +185,38 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { return count ?? 0; } + /// Retrieves all groups a specific player belongs to. + /// Returns an empty list if the player is not part of any group. + Future> getGroupsByPlayer({required String playerId}) async { + final playerGroups = await (select( + playerGroupTable, + )..where((pg) => pg.playerId.equals(playerId))).get(); + + if (playerGroups.isEmpty) return []; + + final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList(); + final rows = + await (select(groupTable) + ..where((g) => g.id.isIn(groupIds)) + ..orderBy([(g) => OrderingTerm.desc(g.createdAt)])) + .get(); + + return Future.wait( + rows.map((groupData) async { + final members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupData.id, + ); + return Group( + id: groupData.id, + name: groupData.name, + description: groupData.description, + members: members, + createdAt: groupData.createdAt, + ); + }), + ); + } + /// Checks if a group with the given [groupId] exists in the database. /// Returns `true` if the group exists, `false` otherwise. Future groupExists({required String groupId}) async { diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 88cca35..e8414f4 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -352,6 +352,53 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return count ?? 0; } + Future> getMatchesByPlayer({required String playerId}) async { + final playerMatches = await (select( + playerMatchTable, + )..where((pm) => pm.playerId.equals(playerId))).get(); + + if (playerMatches.isEmpty) return []; + + final matchIds = playerMatches.map((pm) => pm.matchId).toSet().toList(); + final rows = + await (select(matchTable) + ..where((m) => m.id.isIn(matchIds)) + ..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) + .get(); + + return Future.wait( + rows.map((row) async { + final game = await db.gameDao.getGameById(gameId: row.gameId); + + Group? group; + if (row.groupId != null) { + group = await db.groupDao.getGroupById(groupId: row.groupId!); + } + + final players = await db.playerMatchDao.getPlayersOfMatch( + matchId: row.id, + ); + final scores = await db.scoreEntryDao.getAllMatchScores( + matchId: row.id, + ); + final teams = await _getMatchTeams(matchId: row.id); + + return Match( + id: row.id, + name: row.name, + game: game, + group: group, + players: players, + teams: teams.isEmpty ? null : teams, + notes: row.notes, + createdAt: row.createdAt, + endedAt: row.endedAt, + scores: scores, + ); + }), + ); + } + /// Retrieves all matches associated with the given [groupId]. /// Queries the database directly, filtering by [groupId]. Future> getMatchesByGroup({required String groupId}) async { diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart new file mode 100644 index 0000000..a946133 --- /dev/null +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -0,0 +1,273 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/colored_icon_container.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; + +class PlayerDetailView extends StatefulWidget { + const PlayerDetailView({ + super.key, + required this.player, + required this.callback, + }); + + /// The player to display + final Player player; + + final VoidCallback callback; + + @override + State createState() => _PlayerDetailViewState(); +} + +class _PlayerDetailViewState extends State { + late final AppDatabase db; + + bool isLoading = true; + + /// Total matches played by this player + int totalMatches = 0; + + /// Total matches won by this player + int matchesWon = 0; + + /// Total groups this player belongs to + int totalGroups = 0; + + /// Full list of groups this player belongs to + List playerGroups = List.filled( + 4, + Group(name: "Skeleton group", members: []), + ); + + /// Full list of matches this player played in + List playerMatches = List.filled( + 4, + Match( + name: 'Skeleton match', + game: Game(name: 'Game name', ruleset: Ruleset.singleWinner), + players: [], + ), + ); + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _loadData(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: const Text('Player Profile'), + actions: [ + HapticIconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: 'Delete player?', + content: Text(loc.this_cannot_be_undone), + actions: [ + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(true), + text: loc.delete, + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ), + ).then((confirmed) async { + if (confirmed! && context.mounted) { + //TODO: implement player deletion in db + if (!context.mounted) return; + Navigator.pop(context); + widget.callback(); + } + }); + }, + ), + ], + ), + body: SafeArea( + child: Stack( + alignment: Alignment.center, + children: [ + ListView( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 20, + bottom: 100, + ), + children: [ + const Center( + child: ColoredIconContainer( + icon: Icons.person, + containerSize: 55, + iconSize: 38, + ), + ), + const SizedBox(height: 10), + Text( + widget.player.name + getNameCountText(widget.player), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + Text( + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.player.createdAt)}', + style: const TextStyle( + fontSize: 12, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + InfoTile( + title: "Matches played in (${totalMatches})", + icon: Icons.people, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerMatches.map((match) { + return TextIconTile(text: match.name, iconEnabled: false); + }).toList(), + ), + ), + const SizedBox(height: 15), + InfoTile( + title: "Groups part of (${totalGroups})", + icon: Icons.people, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerGroups.map((group) { + return TextIconTile(text: group.name, iconEnabled: false); + }).toList(), + ), + ), + const SizedBox(height: 15), + InfoTile( + title: loc.statistics, + icon: Icons.bar_chart, + content: AppSkeleton( + enabled: isLoading, + child: Column( + children: [ + _buildStatRow( + "Matches played", + totalMatches.toString(), + ), + _buildStatRow("Matches won", matchesWon.toString()), + _buildStatRow( + "Winrate", + '${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%', + ), + ], + ), + ), + ), + ], + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom, + child: MainMenuButton( + text: "Edit player", + icon: Icons.edit, + onPressed: () async { + //TODO: update player name in popup + widget.callback(); + }, + ), + ), + ], + ), + ), + ); + } + + /// Loads statistics for this player + Future _loadData() async { + isLoading = true; + final fetchedMatches = await db.matchDao.getMatchesByPlayer( + playerId: widget.player.id, + ); + final fetchedGroups = await db.groupDao.getGroupsByPlayer( + playerId: widget.player.id, + ); + + setState(() { + playerMatches = fetchedMatches; + totalMatches = fetchedMatches.length; + matchesWon = fetchedMatches + .where((match) => match.mvp.any((mvp) => mvp.id == widget.player.id)) + .length; + playerGroups = fetchedGroups; + totalGroups = fetchedGroups.length; + isLoading = false; + }); + } + + /// Builds a single statistic row with a label and value + /// - [label]: The label of the statistic + /// - [value]: The value of the statistic + Widget _buildStatRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + ], + ), + Text( + value, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index f6c406e..6f22d39 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/models/group.dart'; +import 'package:tallee/presentation/views/main_menu/player_detail_view.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class GroupTile extends StatefulWidget { @@ -92,6 +94,19 @@ class _GroupTileState extends State { text: member.name, suffixText: getNameCountText(member), iconEnabled: false, + onTileTap: () { + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => PlayerDetailView( + player: member, + callback: () { + //TODO: implement callback + }, + ), + ), + ); + }, ), ], ), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 6a81dc3..d9583e7 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -3,11 +3,13 @@ import 'dart:core' hide Match; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/player_detail_view.dart'; import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -224,6 +226,19 @@ class _MatchTileState extends State { text: player.name, suffixText: getNameCountText(player), iconEnabled: false, + onTileTap: () { + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => PlayerDetailView( + player: player, + callback: () { + //TODO: implement callback + }, + ), + ), + ); + }, ); }).toList(), ), diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index 541b6ae..a5ef399 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -12,6 +12,7 @@ class TextIconTile extends StatelessWidget { this.suffixText = '', this.iconEnabled = true, this.onIconTap, + this.onTileTap, }); /// The text to display in the tile. @@ -25,52 +26,58 @@ class TextIconTile extends StatelessWidget { /// The callback to be invoked when the icon is tapped. final VoidCallback? onIconTap; + /// The callback to be invoked when the tile is tapped. + final VoidCallback? onTileTap; + @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(5), - decoration: BoxDecoration( - color: CustomTheme.onBoxColor, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.min, - children: [ - if (iconEnabled) const SizedBox(width: 3), - Flexible( - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: text, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, + return GestureDetector( + onTap: onTileTap, + child: Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: CustomTheme.onBoxColor, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + if (iconEnabled) const SizedBox(width: 3), + Flexible( + child: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: text, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + ), ), - ), - TextSpan( - text: suffixText, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: CustomTheme.textColor.withAlpha(120), + TextSpan( + text: suffixText, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: CustomTheme.textColor.withAlpha(120), + ), ), - ), - ], + ], + ), ), ), - ), - if (iconEnabled) ...[ - const SizedBox(width: 3), - GestureDetector( - onTap: onIconTap, - child: const Icon(Icons.close, size: 20), - ), + if (iconEnabled) ...[ + const SizedBox(width: 3), + GestureDetector( + onTap: onIconTap, + child: const Icon(Icons.close, size: 20), + ), + ], ], - ], + ), ), ); } diff --git a/test/db_tests/aggregates/group_test.dart b/test/db_tests/aggregates/group_test.dart index 1498523..3ea625f 100644 --- a/test/db_tests/aggregates/group_test.dart +++ b/test/db_tests/aggregates/group_test.dart @@ -194,6 +194,31 @@ void main() { expect(allGroups, isEmpty); }); + test('getGroupsByPlayer() works correctly', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup2], + ); + + final groups = await database.groupDao.getGroupsByPlayer( + playerId: testPlayer2.id, + ); + + expect(groups, hasLength(2)); + expect(groups.any((group) => group.id == testGroup1.id), isTrue); + expect(groups.any((group) => group.id == testGroup2.id), isTrue); + }); + + test( + 'getGroupsByPlayer() returns empty list for non-existent player', + () async { + final groups = await database.groupDao.getGroupsByPlayer( + playerId: 'non-existent-player-id', + ); + + expect(groups, isEmpty); + }, + ); + test('addGroupsAsList() with duplicate groups only adds once', () async { await database.groupDao.addGroupsAsList( groups: [testGroup1, testGroup1, testGroup1], diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 37c1cd0..00e6e46 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -260,6 +260,34 @@ void main() { expect(match.group!.id, testGroup1.id); }); + test('getMatchesByPlayer() works correctly', () async { + await database.matchDao.addMatchesAsList( + matches: [testMatch1, testMatch2], + ); + + final matches = await database.matchDao.getMatchesByPlayer( + playerId: testPlayer1.id, + ); + + expect(matches, hasLength(1)); + expect(matches.first.id, testMatch2.id); + expect( + matches.first.players.any((p) => p.id == testPlayer1.id), + isTrue, + ); + }); + + test( + 'getMatchesByPlayer() returns empty list for non-existent player', + () async { + final matches = await database.matchDao.getMatchesByPlayer( + playerId: 'non-existing-player-id', + ); + + expect(matches, isEmpty); + }, + ); + test('getMatchCount() works correctly', () async { var count = await database.matchDao.getMatchCount(); expect(count, 0); From 869c70ff6317f168edd108078753a0a0bf0f370c Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 20 May 2026 19:50:34 +0200 Subject: [PATCH 02/34] add player change callbacks and improve player detail view --- lib/data/dao/player_dao.dart | 24 ++--- .../main_menu/group_view/group_view.dart | 1 + .../main_menu/match_view/match_view.dart | 1 + .../views/main_menu/player_detail_view.dart | 94 +++++++++++++++---- .../widgets/tiles/group_tile.dart | 6 +- .../widgets/tiles/match_tile.dart | 6 +- 6 files changed, 102 insertions(+), 30 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 51e5845..71fbe6c 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -169,15 +169,18 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { .map((row) => row.name) .getSingleOrNull() ?? ''; - final previousNameCount = await getNameCount(name: previousPlayerName); + final previousNameCount = (await getNameCount(name: previousPlayerName))!; + print('previousNameCount: $previousNameCount'); + + // Update name count for the new name + final count = await calculateNameCount(name: name); + print('count: $count'); final rowsAffected = await (update(playerTable)..where((p) => p.id.equals(playerId))).write( PlayerTableCompanion(name: Value(name)), ); - // Update name count for the new name - final count = await calculateNameCount(name: name); if (count > 0) { await (update(playerTable)..where((p) => p.name.equals(name))).write( PlayerTableCompanion(nameCount: Value(count)), @@ -226,10 +229,10 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /* Name count management */ /// Retrieves the count of players with the given [name]. - Future getNameCount({required String name}) async { + Future getNameCount({required String name}) async { final query = select(playerTable)..where((p) => p.name.equals(name)); final result = await query.get(); - return result.length; + return result.isEmpty ? null : result.length; } /// Updates the nameCount for the player with the given [playerId] to [nameCount]. @@ -269,20 +272,19 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { final count = await getNameCount(name: name); final int nameCount; - if (count == 1) { + if (count == null) { + // If no other players exist with the same name, set nameCount to 0 + nameCount = 0; + } else if (count == 0) { // If one other player exists with the same name, initialize the nameCount await initializeNameCount(name: name); // And for the new player, set nameCount to 2 nameCount = 2; - } else if (count > 1) { + } else { // If more than one player exists with the same name, just increment // the nameCount for the new player nameCount = count + 1; - } else { - // If no other players exist with the same name, set nameCount to 0 - nameCount = 0; } - return nameCount; } diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index c8a9398..50bc6e8 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -77,6 +77,7 @@ class _GroupViewState extends State { ); } return GroupTile( + onPlayerChanged: loadGroups, group: groups[index], onTap: () async { await Navigator.push( diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index a7f60c6..83ff069 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -97,6 +97,7 @@ class _MatchViewState extends State { child: Padding( padding: const EdgeInsets.only(bottom: 12.0), child: MatchTile( + onPlayerEdited: loadMatches, width: MediaQuery.sizeOf(context).width * 0.95, onTap: () async { Navigator.push( diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index a946133..75c2ff6 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -16,6 +16,7 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -37,7 +38,8 @@ class PlayerDetailView extends StatefulWidget { class _PlayerDetailViewState extends State { late final AppDatabase db; - + late Player _player; + late String playerNameCount; bool isLoading = true; /// Total matches played by this player @@ -68,6 +70,7 @@ class _PlayerDetailViewState extends State { @override void initState() { super.initState(); + _player = widget.player; db = Provider.of(context, listen: false); _loadData(); } @@ -75,6 +78,7 @@ class _PlayerDetailViewState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); + playerNameCount = getNameCountText(_player); return Scaffold( appBar: AppBar( @@ -132,18 +136,32 @@ class _PlayerDetailViewState extends State { ), ), const SizedBox(height: 10), - Text( - widget.player.name + getNameCountText(widget.player), - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor, - ), - textAlign: TextAlign.center, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + _player.name, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + Text( + playerNameCount, + style: TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor.withAlpha(120), + ), + textAlign: TextAlign.center, + ), + ], ), const SizedBox(height: 5), Text( - '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.player.createdAt)}', + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_player.createdAt)}', style: const TextStyle( fontSize: 12, color: CustomTheme.textColor, @@ -152,8 +170,8 @@ class _PlayerDetailViewState extends State { ), const SizedBox(height: 20), InfoTile( - title: "Matches played in (${totalMatches})", - icon: Icons.people, + title: "Matches part of (${totalMatches})", + icon: Icons.sports_esports, horizontalAlignment: CrossAxisAlignment.start, content: Wrap( alignment: WrapAlignment.start, @@ -209,8 +227,48 @@ class _PlayerDetailViewState extends State { text: "Edit player", icon: Icons.edit, onPressed: () async { - //TODO: update player name in popup - widget.callback(); + final controller = TextEditingController(text: _player.name); + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: "Change Name", + content: TextInputField( + controller: controller, + hintText: 'Set a player name', + ), + actions: [ + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(true), + text: "Confirm", + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ), + ).then((confirmed) async { + if (confirmed! && context.mounted) { + if (controller.text != _player.name) { + await db.playerDao.updatePlayerName( + playerId: _player.id, + name: controller.text, + ); + widget.callback.call(); + setState(() { + _player = Player( + name: controller.text, + createdAt: _player.createdAt, + id: _player.id, + nameCount: _player.nameCount, + description: _player.description, + ); + playerNameCount = getNameCountText(_player); + }); + } + } + }); }, ), ), @@ -224,17 +282,19 @@ class _PlayerDetailViewState extends State { Future _loadData() async { isLoading = true; final fetchedMatches = await db.matchDao.getMatchesByPlayer( - playerId: widget.player.id, + playerId: _player.id, ); final fetchedGroups = await db.groupDao.getGroupsByPlayer( - playerId: widget.player.id, + playerId: _player.id, ); + if (!mounted) return; + setState(() { playerMatches = fetchedMatches; totalMatches = fetchedMatches.length; matchesWon = fetchedMatches - .where((match) => match.mvp.any((mvp) => mvp.id == widget.player.id)) + .where((match) => match.mvp.any((mvp) => mvp.id == _player.id)) .length; playerGroups = fetchedGroups; totalGroups = fetchedGroups.length; diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index 6f22d39..c01dcbe 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -17,6 +17,7 @@ class GroupTile extends StatefulWidget { required this.group, this.isHighlighted = false, this.onTap, + this.onPlayerChanged, }); /// The group data to be displayed. @@ -28,6 +29,9 @@ class GroupTile extends StatefulWidget { /// Callback function to be executed when the tile is tapped. final VoidCallback? onTap; + /// Callback function to be executed when the players in the group are changed. + final VoidCallback? onPlayerChanged; + @override State createState() => _GroupTileState(); } @@ -101,7 +105,7 @@ class _GroupTileState extends State { builder: (context) => PlayerDetailView( player: member, callback: () { - //TODO: implement callback + widget.onPlayerChanged?.call(); }, ), ), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index d9583e7..c3d0242 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -26,6 +26,7 @@ class MatchTile extends StatefulWidget { required this.onTap, this.width, this.compact = false, + this.onPlayerEdited, }); /// The match data to be displayed. @@ -34,6 +35,9 @@ class MatchTile extends StatefulWidget { /// The callback invoked when the tile is tapped. final VoidCallback onTap; + /// The callback invoked when the players are edited + final VoidCallback? onPlayerEdited; + /// Optional width for the tile. final double? width; @@ -233,7 +237,7 @@ class _MatchTileState extends State { builder: (context) => PlayerDetailView( player: player, callback: () { - //TODO: implement callback + widget.onPlayerEdited?.call(); }, ), ), From 679e869229d4aebc0f634cc5647f1148572ffcb8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 20 May 2026 19:58:59 +0200 Subject: [PATCH 03/34] fix: player count calc error --- lib/data/dao/player_dao.dart | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 71fbe6c..3f13410 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -170,22 +170,18 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { .getSingleOrNull() ?? ''; final previousNameCount = (await getNameCount(name: previousPlayerName))!; - print('previousNameCount: $previousNameCount'); // Update name count for the new name - final count = await calculateNameCount(name: name); - print('count: $count'); - + final newNameCount = await calculateNameCount(name: name); + // Updating player name final rowsAffected = await (update(playerTable)..where((p) => p.id.equals(playerId))).write( PlayerTableCompanion(name: Value(name)), ); - - if (count > 0) { - await (update(playerTable)..where((p) => p.name.equals(name))).write( - PlayerTableCompanion(nameCount: Value(count)), - ); - } + // Updating the name count for the new name + await (update(playerTable)..where((p) => p.name.equals(name))).write( + PlayerTableCompanion(nameCount: Value(newNameCount)), + ); if (previousNameCount > 0) { // Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount From b61a93328ffb85f1f74dae9adbe63abd39688d0d Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 09:47:49 +0200 Subject: [PATCH 04/34] made alertDialog Confirm Button deactivate based on input, fix app skeleton alignment issue, implement correct nameCount Display --- .../views/main_menu/player_detail_view.dart | 120 ++++++++++++------ lib/presentation/widgets/app_skeleton.dart | 16 ++- .../buttons/animated_dialog_button.dart | 56 ++++---- .../widgets/dialog/custom_alert_dialog.dart | 1 - .../widgets/dialog/custom_dialog_action.dart | 15 ++- 5 files changed, 133 insertions(+), 75 deletions(-) diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index 75c2ff6..512bdfd 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -67,18 +67,26 @@ class _PlayerDetailViewState extends State { ), ); + TextEditingController nameController = TextEditingController(); + @override void initState() { super.initState(); _player = widget.player; db = Provider.of(context, listen: false); + playerNameCount = getNameCountText(_player); _loadData(); } + @override + void dispose() { + nameController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - playerNameCount = getNameCountText(_player); return Scaffold( appBar: AppBar( @@ -173,14 +181,22 @@ class _PlayerDetailViewState extends State { title: "Matches part of (${totalMatches})", icon: Icons.sports_esports, horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: playerMatches.map((match) { - return TextIconTile(text: match.name, iconEnabled: false); - }).toList(), + content: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + alignment: Alignment.topLeft, + child: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerMatches.map((match) { + return TextIconTile( + text: match.name, + iconEnabled: false, + ); + }).toList(), + ), ), ), const SizedBox(height: 15), @@ -188,14 +204,22 @@ class _PlayerDetailViewState extends State { title: "Groups part of (${totalGroups})", icon: Icons.people, horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: playerGroups.map((group) { - return TextIconTile(text: group.name, iconEnabled: false); - }).toList(), + content: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + alignment: Alignment.topLeft, + child: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerGroups.map((group) { + return TextIconTile( + text: group.name, + iconEnabled: false, + ); + }).toList(), + ), ), ), const SizedBox(height: 15), @@ -204,6 +228,7 @@ class _PlayerDetailViewState extends State { icon: Icons.bar_chart, content: AppSkeleton( enabled: isLoading, + fixLayoutBuilder: true, child: Column( children: [ _buildStatRow( @@ -227,44 +252,57 @@ class _PlayerDetailViewState extends State { text: "Edit player", icon: Icons.edit, onPressed: () async { - final controller = TextEditingController(text: _player.name); + nameController.text = _player.name; showDialog( context: context, - builder: (context) => CustomAlertDialog( - title: "Change Name", - content: TextInputField( - controller: controller, - hintText: 'Set a player name', - ), - actions: [ - CustomDialogAction( - onPressed: () => Navigator.of(context).pop(true), - text: "Confirm", - ), - CustomDialogAction( - onPressed: () => Navigator.of(context).pop(false), - buttonType: ButtonType.secondary, - text: loc.cancel, - ), - ], + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) { + return CustomAlertDialog( + title: "Change Name", + content: TextInputField( + controller: nameController, + hintText: 'Set a player name', + onChanged: (_) => setDialogState(() {}), + ), + actions: [ + CustomDialogAction( + onPressed: isConfirmButtonEnabled() + ? () => Navigator.of(context).pop(true) + : null, + text: "Confirm", + ), + CustomDialogAction( + onPressed: () => Navigator.of(context).pop(false), + buttonType: ButtonType.secondary, + text: loc.cancel, + ), + ], + ); + }, ), ).then((confirmed) async { if (confirmed! && context.mounted) { - if (controller.text != _player.name) { + final newName = nameController.text.trim(); + + if (newName != _player.name) { + final fetchedPlayerNameCount = await db.playerDao + .getNameCount(name: newName); await db.playerDao.updatePlayerName( playerId: _player.id, - name: controller.text, + name: newName, ); widget.callback.call(); setState(() { _player = Player( - name: controller.text, + name: newName, createdAt: _player.createdAt, id: _player.id, nameCount: _player.nameCount, description: _player.description, ); - playerNameCount = getNameCountText(_player); + playerNameCount = fetchedPlayerNameCount != null + ? ' #${fetchedPlayerNameCount + 1}' + : ''; }); } } @@ -330,4 +368,8 @@ class _PlayerDetailViewState extends State { ), ); } + + bool isConfirmButtonEnabled() { + return nameController.text.trim().isNotEmpty; + } } diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart index 8a21320..abdfb8d 100644 --- a/lib/presentation/widgets/app_skeleton.dart +++ b/lib/presentation/widgets/app_skeleton.dart @@ -6,11 +6,13 @@ class AppSkeleton extends StatefulWidget { /// - [child]: The widget tree to apply the skeleton effect to. /// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. + /// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center]. const AppSkeleton({ super.key, required this.child, this.enabled = true, this.fixLayoutBuilder = false, + this.alignment = Alignment.center, }); /// The widget tree to apply the skeleton effect to. @@ -22,6 +24,9 @@ class AppSkeleton extends StatefulWidget { /// A boolean to fix the layout builder for AnimatedSwitcher. final bool fixLayoutBuilder; + /// The alignment used for the custom layout builder and optional Align wrapper + final Alignment alignment; + @override State createState() => _AppSkeletonState(); } @@ -45,13 +50,14 @@ class _AppSkeletonState extends State { layoutBuilder: !widget.fixLayoutBuilder ? AnimatedSwitcher.defaultLayoutBuilder : (Widget? currentChild, List previousChildren) { - return Stack( - alignment: Alignment.topCenter, - children: [...previousChildren, ?currentChild], - ); + final children = [...previousChildren]; + if (currentChild != null) children.add(currentChild); + return Stack(alignment: widget.alignment, children: children); }, ), - child: widget.child, + child: widget.fixLayoutBuilder + ? Align(alignment: widget.alignment, child: widget.child) + : widget.child, ); } } diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 8c8765e..62960ff 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget { const AnimatedDialogButton({ super.key, required this.buttonText, - required this.onPressed, + this.onPressed, this.buttonConstraints, this.buttonType = ButtonType.primary, this.isDescructive = false, @@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget { final String buttonText; - final VoidCallback onPressed; + final VoidCallback? onPressed; final BoxConstraints? buttonConstraints; @@ -38,28 +38,38 @@ class _AnimatedDialogButtonState extends State { Widget build(BuildContext context) { final textStyling = _getTextStyling(); final buttonDecoration = _getButtonDecoration(); + bool _isDisabled = widget.onPressed == null; - return GestureDetector( - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - onTap: widget.onPressed, - child: AnimatedScale( - scale: _isPressed ? 0.95 : 1.0, - duration: const Duration(milliseconds: 100), - child: AnimatedOpacity( - opacity: _isPressed ? 0.6 : 1.0, - duration: const Duration(milliseconds: 100), - child: Center( - child: Container( - constraints: widget.buttonConstraints, - decoration: buttonDecoration, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - margin: const EdgeInsets.symmetric(vertical: 8), - child: Text( - widget.buttonText, - style: textStyling, - textAlign: TextAlign.center, + return IgnorePointer( + ignoring: _isDisabled, + child: Opacity( + opacity: _isDisabled ? 0.5 : 1.0, + child: GestureDetector( + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onPressed, + child: AnimatedScale( + scale: _isPressed ? 0.95 : 1.0, + duration: const Duration(milliseconds: 100), + child: AnimatedOpacity( + opacity: _isPressed ? 0.6 : 1.0, + duration: const Duration(milliseconds: 100), + child: Center( + child: Container( + constraints: widget.buttonConstraints, + decoration: buttonDecoration, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + margin: const EdgeInsets.symmetric(vertical: 8), + child: Text( + widget.buttonText, + style: textStyling, + textAlign: TextAlign.center, + ), + ), ), ), ), diff --git a/lib/presentation/widgets/dialog/custom_alert_dialog.dart b/lib/presentation/widgets/dialog/custom_alert_dialog.dart index 606fc49..7dfba98 100644 --- a/lib/presentation/widgets/dialog/custom_alert_dialog.dart +++ b/lib/presentation/widgets/dialog/custom_alert_dialog.dart @@ -19,7 +19,6 @@ class CustomAlertDialog extends StatelessWidget { final String title; final Widget content; final List actions; - @override Widget build(BuildContext context) { return AlertDialog( diff --git a/lib/presentation/widgets/dialog/custom_dialog_action.dart b/lib/presentation/widgets/dialog/custom_dialog_action.dart index 26dc40d..0c0b2e0 100644 --- a/lib/presentation/widgets/dialog/custom_dialog_action.dart +++ b/lib/presentation/widgets/dialog/custom_dialog_action.dart @@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget { /// - [onPressed]: Callback function that is triggered when the button is pressed. const CustomDialogAction({ super.key, - required this.onPressed, + this.onPressed, required this.text, this.buttonType = ButtonType.primary, this.isDestructive = false, @@ -20,17 +20,18 @@ class CustomDialogAction extends StatelessWidget { final ButtonType buttonType; - final VoidCallback onPressed; + final VoidCallback? onPressed; final bool isDestructive; - @override Widget build(BuildContext context) { return AnimatedDialogButton( - onPressed: () async { - await HapticFeedback.selectionClick(); - onPressed.call(); - }, + onPressed: onPressed != null + ? () async { + await HapticFeedback.selectionClick(); + onPressed?.call(); + } + : null, buttonText: text, buttonType: buttonType, isDescructive: isDestructive, From 9909d959b051bad8089dbba58113b3d421b6c8ec Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 10:33:16 +0200 Subject: [PATCH 05/34] implement missing localization --- lib/l10n/arb/app_de.arb | 26 ++- lib/l10n/arb/app_en.arb | 29 ++-- lib/l10n/generated/app_localizations.dart | 162 ++++++++++++------ lib/l10n/generated/app_localizations_de.dart | 72 +++++--- lib/l10n/generated/app_localizations_en.dart | 72 +++++--- .../views/main_menu/player_detail_view.dart | 22 +-- pubspec.yaml | 1 + 7 files changed, 262 insertions(+), 122 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index f9093a2..a419b90 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -19,6 +19,7 @@ "color_red": "Rot", "color_teal": "Türkis", "color_yellow": "Gelb", + "confirm": "Bestätigen", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", @@ -44,11 +45,14 @@ }, "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", - "drag_to_set_placement": "Ziehen um Platzierung zu setzen", + "delete_player": "Spieler:in löschen", "description": "Beschreibung", + "drag_to_set_placement": "Ziehen um Platzierung zu setzen", "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", + "edit_name": "Name ändern", + "edit_player": "Spieler bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", @@ -66,6 +70,8 @@ "group_name": "Gruppenname", "group_profile": "Gruppenprofil", "groups": "Gruppen", + "groups_part_of": "Gruppen Teil von", + "highest_score": "Höchste Punkte", "home": "Startseite", "import_canceled": "Import abgebrochen", "import_data": "Daten importieren", @@ -76,17 +82,23 @@ "legal_notice": "Impressum", "licenses": "Lizenzen", "live_edit_mode": "Live-Bearbeitungsmodus", + "loser": "Verlierer:in", + "lowest_score": "Niedrigste Punkte", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", "matches": "Spiele", + "matches_part_of": "Spiele Teil von", + "matches_played": "Spiele gespielt", + "matches_won": "Spiele gewonnen", "members": "Mitglieder", "most_points": "Höchste Punkte", + "multiple_winners": "Mehrere Gewinner:innen", "no_data_available": "Keine Daten verfügbar", "no_games_created_yet": "Noch keine Spielvorlagen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt", - "no_licenses_found": "Keine Lizenzen gefunden", "no_license_text_available": "Kein Lizenztext verfügbar", + "no_licenses_found": "Keine Lizenzen gefunden", "no_matches_created_yet": "Noch keine Spiele erstellt", "no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", @@ -98,10 +110,11 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", - "placement": "Platzierung", "place": "Platz", + "placement": "Platzierung", "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", + "player_profile": "Spieler:in-Profil", "players": "Spieler:innen", "point": "Punkt", "points": "Punkte", @@ -119,17 +132,14 @@ "save_changes": "Änderungen speichern", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", + "select_loser": "Verlierer:in wählen", "select_winner": "Gewinner:in wählen", "select_winners": "Gewinner:innen wählen", - "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", + "set_name": "Name setzen", "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", - "highest_score": "Höchste Punkte", - "loser": "Verlierer:in", - "lowest_score": "Niedrigste Punkte", - "multiple_winners": "Mehrere Gewinner:innen", "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b7da7f2..983ef31 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,6 +1,5 @@ { "@@locale": "en", - "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -20,13 +19,14 @@ "color_red": "Red", "color_teal": "Teal", "color_yellow": "Yellow", + "confirm": "Confirm", "could_not_add_player": "Could not add player", "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", - "created_on": "Created on", "create_new_match": "Create new match", + "created_on": "Created on", "data": "Data", "data_successfully_deleted": "Data successfully deleted", "data_successfully_exported": "Data successfully exported", @@ -45,11 +45,14 @@ }, "delete_group": "Delete Group", "delete_match": "Delete Match", - "drag_to_set_placement": "Drag to set placement", + "delete_player": "Delete player?", "description": "Description", + "drag_to_set_placement": "Drag to set placement", "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", + "edit_name": "Edit name", + "edit_player": "Edit player", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", @@ -67,6 +70,8 @@ "group_name": "Group name", "group_profile": "Group Profile", "groups": "Groups", + "groups_part_of": "Groups part of", + "highest_score": "Highest Score", "home": "Home", "import_canceled": "Import canceled", "import_data": "Import data", @@ -77,17 +82,23 @@ "legal_notice": "Legal Notice", "licenses": "Licenses", "live_edit_mode": "Live Edit Mode", + "loser": "Loser", + "lowest_score": "Lowest Score", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", "matches": "Matches", + "matches_part_of": "Matches part of", + "matches_played": "Matches played", + "matches_won": "Matches won", "members": "Members", "most_points": "Most Points", + "multiple_winners": "Multiple Winners", "no_data_available": "No data available", "no_games_created_yet": "No games created yet", "no_groups_created_yet": "No groups created yet", - "no_licenses_found": "No licenses found", "no_license_text_available": "No license text available", + "no_licenses_found": "No licenses found", "no_matches_created_yet": "No matches created yet", "no_players_created_yet": "No players created yet", "no_players_found_with_that_name": "No players found with that name", @@ -99,10 +110,11 @@ "none": "None", "none_group": "None", "not_available": "Not available", - "placement": "Placement", "place": "place", + "placement": "Placement", "played_matches": "Played Matches", "player_name": "Player name", + "player_profile": "Player Profile", "players": "Players", "point": "Point", "points": "Points", @@ -119,17 +131,14 @@ "save_changes": "Save Changes", "search_for_groups": "Search for groups", "search_for_players": "Search for players", + "select_loser": "Select Loser", "select_winner": "Select Winner", "select_winners": "Select Winners", - "select_loser": "Select Loser", "selected_players": "Selected players", + "set_name": "Set name", "settings": "Settings", "single_loser": "Single Loser", "single_winner": "Single Winner", - "highest_score": "Highest Score", - "loser": "Loser", - "lowest_score": "Lowest Score", - "multiple_winners": "Multiple Winners", "statistics": "Statistics", "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 1bff731..c7fe019 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -212,6 +212,12 @@ abstract class AppLocalizations { /// **'Yellow'** String get color_yellow; + /// No description provided for @confirm. + /// + /// In en, this message translates to: + /// **'Confirm'** + String get confirm; + /// No description provided for @could_not_add_player. /// /// In en, this message translates to: @@ -242,18 +248,18 @@ abstract class AppLocalizations { /// **'Create new group'** String get create_new_group; - /// No description provided for @created_on. - /// - /// In en, this message translates to: - /// **'Created on'** - String get created_on; - /// No description provided for @create_new_match. /// /// In en, this message translates to: /// **'Create new match'** String get create_new_match; + /// No description provided for @created_on. + /// + /// In en, this message translates to: + /// **'Created on'** + String get created_on; + /// No description provided for @data. /// /// In en, this message translates to: @@ -320,11 +326,11 @@ abstract class AppLocalizations { /// **'Delete Match'** String get delete_match; - /// No description provided for @drag_to_set_placement. + /// No description provided for @delete_player. /// /// In en, this message translates to: - /// **'Drag to set placement'** - String get drag_to_set_placement; + /// **'Delete player?'** + String get delete_player; /// No description provided for @description. /// @@ -332,6 +338,12 @@ abstract class AppLocalizations { /// **'Description'** String get description; + /// No description provided for @drag_to_set_placement. + /// + /// In en, this message translates to: + /// **'Drag to set placement'** + String get drag_to_set_placement; + /// No description provided for @edit_game. /// /// In en, this message translates to: @@ -350,6 +362,18 @@ abstract class AppLocalizations { /// **'Edit Match'** String get edit_match; + /// No description provided for @edit_name. + /// + /// In en, this message translates to: + /// **'Edit name'** + String get edit_name; + + /// No description provided for @edit_player. + /// + /// In en, this message translates to: + /// **'Edit player'** + String get edit_player; + /// No description provided for @enter_points. /// /// In en, this message translates to: @@ -452,6 +476,18 @@ abstract class AppLocalizations { /// **'Groups'** String get groups; + /// No description provided for @groups_part_of. + /// + /// In en, this message translates to: + /// **'Groups part of'** + String get groups_part_of; + + /// No description provided for @highest_score. + /// + /// In en, this message translates to: + /// **'Highest Score'** + String get highest_score; + /// No description provided for @home. /// /// In en, this message translates to: @@ -512,6 +548,18 @@ abstract class AppLocalizations { /// **'Live Edit Mode'** String get live_edit_mode; + /// No description provided for @loser. + /// + /// In en, this message translates to: + /// **'Loser'** + String get loser; + + /// No description provided for @lowest_score. + /// + /// In en, this message translates to: + /// **'Lowest Score'** + String get lowest_score; + /// No description provided for @match_in_progress. /// /// In en, this message translates to: @@ -536,6 +584,24 @@ abstract class AppLocalizations { /// **'Matches'** String get matches; + /// No description provided for @matches_part_of. + /// + /// In en, this message translates to: + /// **'Matches part of'** + String get matches_part_of; + + /// No description provided for @matches_played. + /// + /// In en, this message translates to: + /// **'Matches played'** + String get matches_played; + + /// No description provided for @matches_won. + /// + /// In en, this message translates to: + /// **'Matches won'** + String get matches_won; + /// No description provided for @members. /// /// In en, this message translates to: @@ -548,6 +614,12 @@ abstract class AppLocalizations { /// **'Most Points'** String get most_points; + /// No description provided for @multiple_winners. + /// + /// In en, this message translates to: + /// **'Multiple Winners'** + String get multiple_winners; + /// No description provided for @no_data_available. /// /// In en, this message translates to: @@ -566,18 +638,18 @@ abstract class AppLocalizations { /// **'No groups created yet'** String get no_groups_created_yet; - /// No description provided for @no_licenses_found. - /// - /// In en, this message translates to: - /// **'No licenses found'** - String get no_licenses_found; - /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; + /// No description provided for @no_licenses_found. + /// + /// In en, this message translates to: + /// **'No licenses found'** + String get no_licenses_found; + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: @@ -644,18 +716,18 @@ abstract class AppLocalizations { /// **'Not available'** String get not_available; - /// No description provided for @placement. - /// - /// In en, this message translates to: - /// **'Placement'** - String get placement; - /// No description provided for @place. /// /// In en, this message translates to: /// **'place'** String get place; + /// No description provided for @placement. + /// + /// In en, this message translates to: + /// **'Placement'** + String get placement; + /// No description provided for @played_matches. /// /// In en, this message translates to: @@ -668,6 +740,12 @@ abstract class AppLocalizations { /// **'Player name'** String get player_name; + /// No description provided for @player_profile. + /// + /// In en, this message translates to: + /// **'Player Profile'** + String get player_profile; + /// No description provided for @players. /// /// In en, this message translates to: @@ -764,6 +842,12 @@ abstract class AppLocalizations { /// **'Search for players'** String get search_for_players; + /// No description provided for @select_loser. + /// + /// In en, this message translates to: + /// **'Select Loser'** + String get select_loser; + /// No description provided for @select_winner. /// /// In en, this message translates to: @@ -776,18 +860,18 @@ abstract class AppLocalizations { /// **'Select Winners'** String get select_winners; - /// No description provided for @select_loser. - /// - /// In en, this message translates to: - /// **'Select Loser'** - String get select_loser; - /// No description provided for @selected_players. /// /// In en, this message translates to: /// **'Selected players'** String get selected_players; + /// No description provided for @set_name. + /// + /// In en, this message translates to: + /// **'Set name'** + String get set_name; + /// No description provided for @settings. /// /// In en, this message translates to: @@ -806,30 +890,6 @@ abstract class AppLocalizations { /// **'Single Winner'** String get single_winner; - /// No description provided for @highest_score. - /// - /// In en, this message translates to: - /// **'Highest Score'** - String get highest_score; - - /// No description provided for @loser. - /// - /// In en, this message translates to: - /// **'Loser'** - String get loser; - - /// No description provided for @lowest_score. - /// - /// In en, this message translates to: - /// **'Lowest Score'** - String get lowest_score; - - /// No description provided for @multiple_winners. - /// - /// In en, this message translates to: - /// **'Multiple Winners'** - String get multiple_winners; - /// No description provided for @statistics. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index ea8e1f2..cbace38 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -65,6 +65,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get color_yellow => 'Gelb'; + @override + String get confirm => 'Bestätigen'; + @override String could_not_add_player(Object playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; @@ -83,10 +86,10 @@ class AppLocalizationsDe extends AppLocalizations { String get create_new_group => 'Neue Gruppe erstellen'; @override - String get created_on => 'Erstellt am'; + String get create_new_match => 'Neues Spiel erstellen'; @override - String get create_new_match => 'Neues Spiel erstellen'; + String get created_on => 'Erstellt am'; @override String get data => 'Daten'; @@ -132,11 +135,14 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_match => 'Spiel löschen'; @override - String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; + String get delete_player => 'Spieler:in löschen'; @override String get description => 'Beschreibung'; + @override + String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; + @override String get edit_game => 'Spielvorlage bearbeiten'; @@ -146,6 +152,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get edit_match => 'Gruppe bearbeiten'; + @override + String get edit_name => 'Name ändern'; + + @override + String get edit_player => 'Spieler bearbeiten'; + @override String get enter_points => 'Punkte eingeben'; @@ -201,6 +213,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get groups => 'Gruppen'; + @override + String get groups_part_of => 'Gruppen Teil von'; + + @override + String get highest_score => 'Höchste Punkte'; + @override String get home => 'Startseite'; @@ -231,6 +249,12 @@ class AppLocalizationsDe extends AppLocalizations { @override String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override + String get loser => 'Verlierer:in'; + + @override + String get lowest_score => 'Niedrigste Punkte'; + @override String get match_in_progress => 'Spiel läuft...'; @@ -243,12 +267,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get matches => 'Spiele'; + @override + String get matches_part_of => 'Spiele Teil von'; + + @override + String get matches_played => 'Spiele gespielt'; + + @override + String get matches_won => 'Spiele gewonnen'; + @override String get members => 'Mitglieder'; @override String get most_points => 'Höchste Punkte'; + @override + String get multiple_winners => 'Mehrere Gewinner:innen'; + @override String get no_data_available => 'Keine Daten verfügbar'; @@ -259,10 +295,10 @@ class AppLocalizationsDe extends AppLocalizations { String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; @override - String get no_licenses_found => 'Keine Lizenzen gefunden'; + String get no_license_text_available => 'Kein Lizenztext verfügbar'; @override - String get no_license_text_available => 'Kein Lizenztext verfügbar'; + String get no_licenses_found => 'Keine Lizenzen gefunden'; @override String get no_matches_created_yet => 'Noch keine Spiele erstellt'; @@ -299,10 +335,10 @@ class AppLocalizationsDe extends AppLocalizations { String get not_available => 'Nicht verfügbar'; @override - String get placement => 'Platzierung'; + String get place => 'Platz'; @override - String get place => 'Platz'; + String get placement => 'Platzierung'; @override String get played_matches => 'Gespielte Spiele'; @@ -310,6 +346,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get player_name => 'Spieler:innenname'; + @override + String get player_profile => 'Spieler:in-Profil'; + @override String get players => 'Spieler:innen'; @@ -363,6 +402,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get search_for_players => 'Nach Spieler:innen suchen'; + @override + String get select_loser => 'Verlierer:in wählen'; + @override String get select_winner => 'Gewinner:in wählen'; @@ -370,10 +412,10 @@ class AppLocalizationsDe extends AppLocalizations { String get select_winners => 'Gewinner:innen wählen'; @override - String get select_loser => 'Verlierer:in wählen'; + String get selected_players => 'Ausgewählte Spieler:innen'; @override - String get selected_players => 'Ausgewählte Spieler:innen'; + String get set_name => 'Name setzen'; @override String get settings => 'Einstellungen'; @@ -384,18 +426,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get single_winner => 'Ein:e Gewinner:in'; - @override - String get highest_score => 'Höchste Punkte'; - - @override - String get loser => 'Verlierer:in'; - - @override - String get lowest_score => 'Niedrigste Punkte'; - - @override - String get multiple_winners => 'Mehrere Gewinner:innen'; - @override String get statistics => 'Statistiken'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 48f054b..1083d3b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -65,6 +65,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get color_yellow => 'Yellow'; + @override + String get confirm => 'Confirm'; + @override String could_not_add_player(Object playerName) { return 'Could not add player'; @@ -83,10 +86,10 @@ class AppLocalizationsEn extends AppLocalizations { String get create_new_group => 'Create new group'; @override - String get created_on => 'Created on'; + String get create_new_match => 'Create new match'; @override - String get create_new_match => 'Create new match'; + String get created_on => 'Created on'; @override String get data => 'Data'; @@ -132,11 +135,14 @@ class AppLocalizationsEn extends AppLocalizations { String get delete_match => 'Delete Match'; @override - String get drag_to_set_placement => 'Drag to set placement'; + String get delete_player => 'Delete player?'; @override String get description => 'Description'; + @override + String get drag_to_set_placement => 'Drag to set placement'; + @override String get edit_game => 'Edit Game'; @@ -146,6 +152,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get edit_match => 'Edit Match'; + @override + String get edit_name => 'Edit name'; + + @override + String get edit_player => 'Edit player'; + @override String get enter_points => 'Enter points'; @@ -201,6 +213,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groups => 'Groups'; + @override + String get groups_part_of => 'Groups part of'; + + @override + String get highest_score => 'Highest Score'; + @override String get home => 'Home'; @@ -231,6 +249,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get live_edit_mode => 'Live Edit Mode'; + @override + String get loser => 'Loser'; + + @override + String get lowest_score => 'Lowest Score'; + @override String get match_in_progress => 'Match in progress...'; @@ -243,12 +267,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get matches => 'Matches'; + @override + String get matches_part_of => 'Matches part of'; + + @override + String get matches_played => 'Matches played'; + + @override + String get matches_won => 'Matches won'; + @override String get members => 'Members'; @override String get most_points => 'Most Points'; + @override + String get multiple_winners => 'Multiple Winners'; + @override String get no_data_available => 'No data available'; @@ -259,10 +295,10 @@ class AppLocalizationsEn extends AppLocalizations { String get no_groups_created_yet => 'No groups created yet'; @override - String get no_licenses_found => 'No licenses found'; + String get no_license_text_available => 'No license text available'; @override - String get no_license_text_available => 'No license text available'; + String get no_licenses_found => 'No licenses found'; @override String get no_matches_created_yet => 'No matches created yet'; @@ -299,10 +335,10 @@ class AppLocalizationsEn extends AppLocalizations { String get not_available => 'Not available'; @override - String get placement => 'Placement'; + String get place => 'place'; @override - String get place => 'place'; + String get placement => 'Placement'; @override String get played_matches => 'Played Matches'; @@ -310,6 +346,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get player_name => 'Player name'; + @override + String get player_profile => 'Player Profile'; + @override String get players => 'Players'; @@ -363,6 +402,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get search_for_players => 'Search for players'; + @override + String get select_loser => 'Select Loser'; + @override String get select_winner => 'Select Winner'; @@ -370,10 +412,10 @@ class AppLocalizationsEn extends AppLocalizations { String get select_winners => 'Select Winners'; @override - String get select_loser => 'Select Loser'; + String get selected_players => 'Selected players'; @override - String get selected_players => 'Selected players'; + String get set_name => 'Set name'; @override String get settings => 'Settings'; @@ -384,18 +426,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get single_winner => 'Single Winner'; - @override - String get highest_score => 'Highest Score'; - - @override - String get loser => 'Loser'; - - @override - String get lowest_score => 'Lowest Score'; - - @override - String get multiple_winners => 'Multiple Winners'; - @override String get statistics => 'Statistics'; diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index 512bdfd..187751d 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -90,7 +90,7 @@ class _PlayerDetailViewState extends State { return Scaffold( appBar: AppBar( - title: const Text('Player Profile'), + title: Text(loc.player_profile), actions: [ HapticIconButton( icon: const Icon(Icons.delete), @@ -98,7 +98,7 @@ class _PlayerDetailViewState extends State { showDialog( context: context, builder: (context) => CustomAlertDialog( - title: 'Delete player?', + title: loc.delete_player, content: Text(loc.this_cannot_be_undone), actions: [ CustomDialogAction( @@ -178,7 +178,7 @@ class _PlayerDetailViewState extends State { ), const SizedBox(height: 20), InfoTile( - title: "Matches part of (${totalMatches})", + title: "${loc.matches_part_of} (${totalMatches})", icon: Icons.sports_esports, horizontalAlignment: CrossAxisAlignment.start, content: AppSkeleton( @@ -201,7 +201,7 @@ class _PlayerDetailViewState extends State { ), const SizedBox(height: 15), InfoTile( - title: "Groups part of (${totalGroups})", + title: "${loc.groups_part_of} (${totalGroups})", icon: Icons.people, horizontalAlignment: CrossAxisAlignment.start, content: AppSkeleton( @@ -232,12 +232,12 @@ class _PlayerDetailViewState extends State { child: Column( children: [ _buildStatRow( - "Matches played", + loc.matches_played, totalMatches.toString(), ), - _buildStatRow("Matches won", matchesWon.toString()), + _buildStatRow(loc.matches_won, matchesWon.toString()), _buildStatRow( - "Winrate", + loc.winrate, '${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%', ), ], @@ -249,7 +249,7 @@ class _PlayerDetailViewState extends State { Positioned( bottom: MediaQuery.paddingOf(context).bottom, child: MainMenuButton( - text: "Edit player", + text: loc.edit_player, icon: Icons.edit, onPressed: () async { nameController.text = _player.name; @@ -258,10 +258,10 @@ class _PlayerDetailViewState extends State { builder: (context) => StatefulBuilder( builder: (context, setDialogState) { return CustomAlertDialog( - title: "Change Name", + title: loc.edit_name, content: TextInputField( controller: nameController, - hintText: 'Set a player name', + hintText: loc.set_name, onChanged: (_) => setDialogState(() {}), ), actions: [ @@ -269,7 +269,7 @@ class _PlayerDetailViewState extends State { onPressed: isConfirmButtonEnabled() ? () => Navigator.of(context).pop(true) : null, - text: "Confirm", + text: loc.confirm, ), CustomDialogAction( onPressed: () => Navigator.of(context).pop(false), diff --git a/pubspec.yaml b/pubspec.yaml index cb0bb83..7d171b3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dev_dependencies: dart_pubspec_licenses: ^3.0.14 drift_dev: ^2.27.0 flutter_lints: ^6.0.0 + arb_utils: ^0.11.0 flutter: uses-material-design: true From 2a38462c57e518998a7cdf6b18806c5405f1b96d Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 10:34:01 +0200 Subject: [PATCH 06/34] fix linter issues --- lib/presentation/views/main_menu/player_detail_view.dart | 6 +++--- .../widgets/buttons/animated_dialog_button.dart | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index 187751d..d314a97 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -54,7 +54,7 @@ class _PlayerDetailViewState extends State { /// Full list of groups this player belongs to List playerGroups = List.filled( 4, - Group(name: "Skeleton group", members: []), + Group(name: 'Skeleton group', members: []), ); /// Full list of matches this player played in @@ -178,7 +178,7 @@ class _PlayerDetailViewState extends State { ), const SizedBox(height: 20), InfoTile( - title: "${loc.matches_part_of} (${totalMatches})", + title: '${loc.matches_part_of} ($totalMatches)', icon: Icons.sports_esports, horizontalAlignment: CrossAxisAlignment.start, content: AppSkeleton( @@ -201,7 +201,7 @@ class _PlayerDetailViewState extends State { ), const SizedBox(height: 15), InfoTile( - title: "${loc.groups_part_of} (${totalGroups})", + title: '${loc.groups_part_of} ($totalGroups)', icon: Icons.people, horizontalAlignment: CrossAxisAlignment.start, content: AppSkeleton( diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 62960ff..5062b23 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -38,12 +38,12 @@ class _AnimatedDialogButtonState extends State { Widget build(BuildContext context) { final textStyling = _getTextStyling(); final buttonDecoration = _getButtonDecoration(); - bool _isDisabled = widget.onPressed == null; + bool isDisabled = widget.onPressed == null; return IgnorePointer( - ignoring: _isDisabled, + ignoring: isDisabled, child: Opacity( - opacity: _isDisabled ? 0.5 : 1.0, + opacity: isDisabled ? 0.5 : 1.0, child: GestureDetector( onTapDown: (_) => setState(() => _isPressed = true), onTapUp: (_) => setState(() => _isPressed = false), From 82095ab41affe9a744741df5e0dc2aa8bda3cae1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 15:33:26 +0200 Subject: [PATCH 07/34] fix: name count --- lib/data/dao/player_dao.dart | 54 ++++++++++++------- .../views/main_menu/player_detail_view.dart | 9 ++-- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 3f13410..132c715 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// the new one. Future addPlayer({required Player player}) async { if (!await playerExists(playerId: player.id)) { - final int nameCount = await calculateNameCount(name: player.name); + final int nameCount = await processNameCount(name: player.name); await into(playerTable).insert( PlayerTableCompanion.insert( @@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { final playersWithName = entry.value; // Get the current nameCount - var nameCount = await calculateNameCount(name: name); + var nameCount = await processNameCount(name: name); // One player with the same name if (playersWithName.length == 1) { @@ -169,19 +169,19 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { .map((row) => row.name) .getSingleOrNull() ?? ''; - final previousNameCount = (await getNameCount(name: previousPlayerName))!; + final previousNameCount = await getNameCount(name: previousPlayerName); // Update name count for the new name - final newNameCount = await calculateNameCount(name: name); - // Updating player name + final newNameCount = await processNameCount(name: name); + + // Update name and nameCount final rowsAffected = await (update(playerTable)..where((p) => p.id.equals(playerId))).write( - PlayerTableCompanion(name: Value(name)), + PlayerTableCompanion( + name: Value(name), + nameCount: Value(newNameCount), + ), ); - // Updating the name count for the new name - await (update(playerTable)..where((p) => p.name.equals(name))).write( - PlayerTableCompanion(nameCount: Value(newNameCount)), - ); if (previousNameCount > 0) { // Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount @@ -225,10 +225,12 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /* Name count management */ /// Retrieves the count of players with the given [name]. - Future getNameCount({required String name}) async { + /// Returns the highest name count if players with the same name exist, + /// otherwise `null`. + Future getNameCount({required String name}) async { final query = select(playerTable)..where((p) => p.name.equals(name)); final result = await query.get(); - return result.isEmpty ? null : result.length; + return result.length; } /// Updates the nameCount for the player with the given [playerId] to [nameCount]. @@ -264,17 +266,33 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { } @visibleForTesting + /// Processes the name count for a new player with the given [name]. + ///- 0 Player: returning 0 + ///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1 + ///- Other: returning the existing count + 1 + Future processNameCount({required String name}) async { + final nameCount = await calculateNameCount(name: name); + if (nameCount == 2) { + // If one other player exists with the same name, initialize the nameCount + await initializeNameCount(name: name); + } + return nameCount; + } + + @visibleForTesting + /// Calculates the name count for a new player with the given [name]. + /// - 0 Players: Name count is 0 + /// - 1 Player: Name count is 2 (since the existing player will be 1) + /// - Other: Name count is the existing count + 1 Future calculateNameCount({required String name}) async { final count = await getNameCount(name: name); final int nameCount; - if (count == null) { - // If no other players exist with the same name, set nameCount to 0 + if (count == 0) { + // If no other players exist with the same name, the returned nameCount is 0 nameCount = 0; - } else if (count == 0) { - // If one other player exists with the same name, initialize the nameCount - await initializeNameCount(name: name); - // And for the new player, set nameCount to 2 + } else if (count == 1) { + // If one other player with the name count exists, the returned name count is 2 nameCount = 2; } else { // If more than one player exists with the same name, just increment diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index d314a97..27700c9 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -300,9 +300,12 @@ class _PlayerDetailViewState extends State { nameCount: _player.nameCount, description: _player.description, ); - playerNameCount = fetchedPlayerNameCount != null - ? ' #${fetchedPlayerNameCount + 1}' - : ''; + + // If there is already a player with the same name, + // the count of that player is 0, so we start counting from 2 to get the correct count for this player. If there are no players with the same name, we just show the name without a count. + playerNameCount = fetchedPlayerNameCount == 0 + ? '' + : ' #${fetchedPlayerNameCount + 1}'; }); } } From ccb0d32c5493834064cac934dde7e3761ccc2602 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 15:45:29 +0200 Subject: [PATCH 08/34] fix: tests for name count --- lib/data/dao/player_dao.dart | 9 +- test/db_tests/entities/player_test.dart | 104 +++++++++++++++--------- 2 files changed, 71 insertions(+), 42 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 132c715..2f8ef6e 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// the new one. Future addPlayer({required Player player}) async { if (!await playerExists(playerId: player.id)) { - final int nameCount = await processNameCount(name: player.name); + final int nameCount = await _processNameCount(name: player.name); await into(playerTable).insert( PlayerTableCompanion.insert( @@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { final playersWithName = entry.value; // Get the current nameCount - var nameCount = await processNameCount(name: name); + var nameCount = await _processNameCount(name: name); // One player with the same name if (playersWithName.length == 1) { @@ -172,7 +172,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { final previousNameCount = await getNameCount(name: previousPlayerName); // Update name count for the new name - final newNameCount = await processNameCount(name: name); + final newNameCount = await _processNameCount(name: name); // Update name and nameCount final rowsAffected = @@ -265,12 +265,11 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return null; } - @visibleForTesting /// Processes the name count for a new player with the given [name]. ///- 0 Player: returning 0 ///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1 ///- Other: returning the existing count + 1 - Future processNameCount({required String name}) async { + Future _processNameCount({required String name}) async { final nameCount = await calculateNameCount(name: name); if (nameCount == 2) { // If one other player exists with the same name, initialize the nameCount diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index bfcced4..d25f320 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -372,14 +372,22 @@ void main() { final player1 = Player(name: testPlayer1.name, description: ''); await database.playerDao.addPlayer(player: player1); + final player2 = Player(name: testPlayer1.name, description: ''); + await database.playerDao.addPlayer(player: player2); + var players = await database.playerDao.getAllPlayers(); - expect(players.length, 2); + expect(players.length, 3); players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); for (int i = 0; i < players.length - 1; i++) { expect(players[i].nameCount, i + 1); } + + // ids are correct in the right order + expect(players[0].id, testPlayer1.id); + expect(players[1].id, player1.id); + expect(players[2].id, player2.id); }, ); @@ -404,24 +412,62 @@ void main() { for (int i = 0; i < players.length - 1; i++) { expect(players[i].nameCount, i + 1); } + + // ids are correct in the right order + expect(players[0].id, testPlayer1.id); + expect(players[1].id, player1.id); + expect(players[2].id, player2.id); + expect(players[3].id, player3.id); }, ); - test('getNameCount works correctly', () async { + test('getNameCount works correctly', () async { + final player1 = Player(name: testPlayer1.name); final player2 = Player(name: testPlayer1.name); - final player3 = Player(name: testPlayer1.name); - await database.playerDao.addPlayersAsList( - players: [testPlayer1, player2, player3], + await database.playerDao.addPlayer(player: testPlayer1); + + var nameCount = await database.playerDao.getNameCount( + name: testPlayer1.name, ); - final nameCount = await database.playerDao.getNameCount( + expect(nameCount, 1); + + await database.playerDao.addPlayersAsList(players: [player1, player2]); + + nameCount = await database.playerDao.getNameCount( name: testPlayer1.name, ); expect(nameCount, 3); }); + test('calculateNameCount works correctly', () async { + final player1 = Player(name: testPlayer1.name); + final player2 = Player(name: testPlayer1.name); + + // Case 1: No existing players with the name + var nameCount = await database.playerDao.calculateNameCount( + name: testPlayer1.name, + ); + expect(nameCount, 0); + + // Case 2: One existing player with the name. Should return 2 for + // the new player + await database.playerDao.addPlayer(player: testPlayer1); + nameCount = await database.playerDao.calculateNameCount( + name: testPlayer1.name, + ); + expect(nameCount, 2); + + // Case 3: Multiple existing players with the name. Should return count + 1 + await database.playerDao.addPlayersAsList(players: [player1, player2]); + nameCount = await database.playerDao.calculateNameCount( + name: testPlayer1.name, + ); + expect(nameCount, 4); + }); + test('updateNameCount works correctly', () async { await database.playerDao.addPlayer(player: testPlayer1); @@ -441,14 +487,24 @@ void main() { final player2 = Player(name: testPlayer1.name, description: ''); final player3 = Player(name: testPlayer1.name, description: ''); - await database.playerDao.addPlayersAsList( - players: [testPlayer1, player2, player3], - ); - - final player = await database.playerDao.getPlayerWithHighestNameCount( + await database.playerDao.addPlayer(player: testPlayer1); + var player = await database.playerDao.getPlayerWithHighestNameCount( name: testPlayer1.name, ); + expect(player, isNotNull); + expect(player!.nameCount, 0); + await database.playerDao.addPlayer(player: player2); + player = await database.playerDao.getPlayerWithHighestNameCount( + name: testPlayer1.name, + ); + expect(player, isNotNull); + expect(player!.nameCount, 2); + + await database.playerDao.addPlayer(player: player3); + player = await database.playerDao.getPlayerWithHighestNameCount( + name: testPlayer1.name, + ); expect(player, isNotNull); expect(player!.nameCount, 3); }); @@ -460,32 +516,6 @@ void main() { expect(player, isNull); }); - test('calculateNameCount works correctly', () async { - // Case 1: No existing players with the name - var count = await database.playerDao.calculateNameCount( - name: testPlayer1.name, - ); - expect(count, 0); - - // Case 2: One existing player with the name. Should update that - // player's nameCount to 1 and return 2 for the new player - await database.playerDao.addPlayer(player: testPlayer1); - - count = await database.playerDao.calculateNameCount( - name: testPlayer1.name, - ); - expect(count, 2); - - // Case 3: Multiple existing players with the name. - final player2 = Player(name: testPlayer1.name, nameCount: count); - await database.playerDao.addPlayer(player: player2); - - count = await database.playerDao.calculateNameCount( - name: testPlayer1.name, - ); - expect(count, 3); - }); - test('getPlayerWithHighestNameCount with non existing player', () async { await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.initializeNameCount(name: testPlayer1.name); From bf2cd2bf586b445ecead508e6c04307c65b99c29 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 16:08:59 +0200 Subject: [PATCH 09/34] feat: add player creation callbacks to update member and match lists when group/match creation is canceled but player created --- .../views/main_menu/group_view/create_group_view.dart | 3 ++- lib/presentation/views/main_menu/group_view/group_view.dart | 5 +---- .../main_menu/match_view/create_match/create_match_view.dart | 1 + lib/presentation/widgets/player_selection.dart | 5 +++++ 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 84efbe1..72e4c69 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -89,6 +89,7 @@ class _CreateGroupViewState extends State { Expanded( child: PlayerSelection( initialSelectedPlayers: initialSelectedPlayers, + onPlayerCreated: () => widget.onMembersChanged?.call(), onChanged: (value) { setState(() { selectedPlayers = [...value]; @@ -134,6 +135,7 @@ class _CreateGroupViewState extends State { if (!mounted) return; if (success) { + widget.onMembersChanged?.call(); await HapticFeedback.successNotification(); if (mounted) { Navigator.pop(context, updatedGroup); @@ -157,7 +159,6 @@ class _CreateGroupViewState extends State { final success = await db.groupDao.addGroup( group: Group(name: groupName, members: selectedPlayers), ); - return success; } diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index 50bc6e8..e53b661 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -107,13 +107,10 @@ class _GroupViewState extends State { context, adaptivePageRoute( builder: (context) { - return const CreateGroupView(); + return CreateGroupView(onMembersChanged: loadGroups); }, ), ); - setState(() { - loadGroups(); - }); }, ), ), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 85bb936..c8790be 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -196,6 +196,7 @@ class _CreateMatchViewState extends State { child: PlayerSelection( key: ValueKey(selectedGroup?.id ?? 'no_group'), initialSelectedPlayers: selectedPlayers, + onPlayerCreated: () => widget.onMatchesUpdated?.call(), onChanged: (value) { setState(() { selectedPlayers = value; diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 00d6c11..6a62c95 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -26,6 +26,7 @@ class PlayerSelection extends StatefulWidget { this.availablePlayers, this.initialSelectedPlayers, required this.onChanged, + this.onPlayerCreated, }); /// An optional list of players to choose from. If null, all players from the database are used. @@ -37,6 +38,9 @@ class PlayerSelection extends StatefulWidget { /// A callback function that is invoked whenever the selection changes, final Function(List value) onChanged; + /// A callback function that is invoked when a player was created in this widget + final VoidCallback? onPlayerCreated; + @override State createState() => _PlayerSelectionState(); } @@ -323,6 +327,7 @@ class _PlayerSelectionState extends State { /// Updates the state after successfully adding a new player. void _handleSuccessfulPlayerCreation(Player player) { + widget.onPlayerCreated?.call(); selectedPlayers.insert(0, player); widget.onChanged([...selectedPlayers]); allPlayers.add(player); From 78c59a9b52824cbc031ee4bf287b5db5d2e83676 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 16:20:48 +0200 Subject: [PATCH 10/34] feat: add localization for no matches played and not part of any group --- lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 2 + lib/l10n/generated/app_localizations.dart | 12 ++++ lib/l10n/generated/app_localizations_de.dart | 6 ++ lib/l10n/generated/app_localizations_en.dart | 6 ++ .../views/main_menu/player_detail_view.dart | 64 ++++++++++++------- 6 files changed, 68 insertions(+), 24 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index a419b90..610d2c9 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -100,6 +100,7 @@ "no_license_text_available": "Kein Lizenztext verfügbar", "no_licenses_found": "Keine Lizenzen gefunden", "no_matches_created_yet": "Noch keine Spiele erstellt", + "no_matches_played_yet": "Noch kein Spiel gespielt", "no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_selected": "Keine Spieler:innen ausgewählt", @@ -110,6 +111,7 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", + "not_part_of_any_group": "Noch keiner Gruppe hinzugefügt", "place": "Platz", "placement": "Platzierung", "played_matches": "Gespielte Spiele", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 983ef31..a8b0634 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -100,6 +100,7 @@ "no_license_text_available": "No license text available", "no_licenses_found": "No licenses found", "no_matches_created_yet": "No matches created yet", + "no_matches_played_yet": "No games played yet", "no_players_created_yet": "No players created yet", "no_players_found_with_that_name": "No players found with that name", "no_players_selected": "No players selected", @@ -110,6 +111,7 @@ "none": "None", "none_group": "None", "not_available": "Not available", + "not_part_of_any_group": "Not part of any group yet", "place": "place", "placement": "Placement", "played_matches": "Played Matches", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index c7fe019..4cee263 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -656,6 +656,12 @@ abstract class AppLocalizations { /// **'No matches created yet'** String get no_matches_created_yet; + /// No description provided for @no_matches_played_yet. + /// + /// In en, this message translates to: + /// **'No games played yet'** + String get no_matches_played_yet; + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: @@ -716,6 +722,12 @@ abstract class AppLocalizations { /// **'Not available'** String get not_available; + /// No description provided for @not_part_of_any_group. + /// + /// In en, this message translates to: + /// **'Not part of any group yet'** + String get not_part_of_any_group; + /// No description provided for @place. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index cbace38..66dca88 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -303,6 +303,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_matches_created_yet => 'Noch keine Spiele erstellt'; + @override + String get no_matches_played_yet => 'Noch kein Spiel gespielt'; + @override String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; @@ -334,6 +337,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get not_available => 'Nicht verfügbar'; + @override + String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt'; + @override String get place => 'Platz'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 1083d3b..b9e467a 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -303,6 +303,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_matches_created_yet => 'No matches created yet'; + @override + String get no_matches_played_yet => 'No games played yet'; + @override String get no_players_created_yet => 'No players created yet'; @@ -334,6 +337,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get not_available => 'Not available'; + @override + String get not_part_of_any_group => 'Not part of any group yet'; + @override String get place => 'place'; diff --git a/lib/presentation/views/main_menu/player_detail_view.dart b/lib/presentation/views/main_menu/player_detail_view.dart index 27700c9..e26c327 100644 --- a/lib/presentation/views/main_menu/player_detail_view.dart +++ b/lib/presentation/views/main_menu/player_detail_view.dart @@ -185,18 +185,26 @@ class _PlayerDetailViewState extends State { enabled: isLoading, fixLayoutBuilder: true, alignment: Alignment.topLeft, - child: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: playerMatches.map((match) { - return TextIconTile( - text: match.name, - iconEnabled: false, - ); - }).toList(), - ), + child: playerMatches.isNotEmpty + ? Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerMatches.map((match) { + return TextIconTile( + text: match.name, + iconEnabled: false, + ); + }).toList(), + ) + : Text( + loc.no_matches_played_yet, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.textColor, + ), + ), ), ), const SizedBox(height: 15), @@ -208,18 +216,26 @@ class _PlayerDetailViewState extends State { enabled: isLoading, fixLayoutBuilder: true, alignment: Alignment.topLeft, - child: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: playerGroups.map((group) { - return TextIconTile( - text: group.name, - iconEnabled: false, - ); - }).toList(), - ), + child: playerGroups.isNotEmpty + ? Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: playerGroups.map((group) { + return TextIconTile( + text: group.name, + iconEnabled: false, + ); + }).toList(), + ) + : Text( + loc.not_part_of_any_group, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.textColor, + ), + ), ), ), const SizedBox(height: 15), From 9adcc29cda75b65b55255ec82f0993f522ac6672 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 23:57:59 +0200 Subject: [PATCH 11/34] fix: updatePlayerName corrects the name count after renaming to different name --- lib/data/dao/player_dao.dart | 20 +++++++++----- test/db_tests/entities/player_test.dart | 36 +++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 7 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 2f8ef6e..8b60aa4 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -169,7 +169,6 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { .map((row) => row.name) .getSingleOrNull() ?? ''; - final previousNameCount = await getNameCount(name: previousPlayerName); // Update name count for the new name final newNameCount = await _processNameCount(name: name); @@ -183,16 +182,23 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ), ); + // Updating name count for the previous name + final previousNameCount = await getNameCount(name: previousPlayerName); if (previousNameCount > 0) { - // Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount + // At least one more player with the previous name + + // Get the player with the highest count final player = await getPlayerWithHighestNameCount( name: previousPlayerName, ); - if (player != null) { - await updateNameCount( - playerId: player.id, - nameCount: previousNameCount, - ); + + if (previousNameCount > 1) { + // Multiple players + final nameCount = await getNameCount(name: previousPlayerName); + await updateNameCount(playerId: player!.id, nameCount: nameCount - 1); + } else { + // Only one player + await updateNameCount(playerId: player!.id, nameCount: 0); } } return rowsAffected > 0; diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index d25f320..1061eba 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -233,6 +233,42 @@ void main() { expect(allPlayers, isEmpty); }); + test('updatePlayerName() updates the nameCount correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); + + final newName = testPlayer1.name; + await database.playerDao.updatePlayerName( + playerId: testPlayer2.id, + name: newName, + ); + + var player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 1); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer2.id, + ); + expect(player.nameCount, 2); + + await database.playerDao.updatePlayerName( + playerId: testPlayer1.id, + name: 'different name', + ); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 0); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer2.id, + ); + expect(player.nameCount, 0); + }); + test('updatePlayerDescription() works correctly', () async { await database.playerDao.addPlayer(player: testPlayer1); From 4dcd4f0f71f54eacc4cfc9d72f8fb138f57f56dd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 20:00:28 +0200 Subject: [PATCH 12/34] fix: name count 3 player issue --- lib/data/dao/player_dao.dart | 2 +- test/db_tests/entities/player_test.dart | 55 ++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 8b60aa4..40c842a 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -195,7 +195,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { if (previousNameCount > 1) { // Multiple players final nameCount = await getNameCount(name: previousPlayerName); - await updateNameCount(playerId: player!.id, nameCount: nameCount - 1); + await updateNameCount(playerId: player!.id, nameCount: nameCount); } else { // Only one player await updateNameCount(playerId: player!.id, nameCount: 0); diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 1061eba..4105df1 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -233,7 +233,7 @@ void main() { expect(allPlayers, isEmpty); }); - test('updatePlayerName() updates the nameCount correctly', () async { + test('updatePlayerName() sets correct nameCount with 2 player', () async { await database.playerDao.addPlayer(player: testPlayer1); await database.playerDao.addPlayer(player: testPlayer2); @@ -269,6 +269,59 @@ void main() { expect(player.nameCount, 0); }); + test('updatePlayerName() sets correct nameCount with 3 player', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3], + ); + + // Changing both names to player 1's name + final newName = testPlayer1.name; + await database.playerDao.updatePlayerName( + playerId: testPlayer2.id, + name: newName, + ); + await database.playerDao.updatePlayerName( + playerId: testPlayer3.id, + name: newName, + ); + + var player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 1); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer2.id, + ); + expect(player.nameCount, 2); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer3.id, + ); + expect(player.nameCount, 3); + + // Changing the middle players name + await database.playerDao.updatePlayerName( + playerId: testPlayer2.id, + name: 'different name', + ); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 1); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer2.id, + ); + expect(player.nameCount, 0); + + player = await database.playerDao.getPlayerById( + playerId: testPlayer3.id, + ); + expect(player.nameCount, 2); + }); + test('updatePlayerDescription() works correctly', () async { await database.playerDao.addPlayer(player: testPlayer1); From 5a652a5f2c5920d4c8908384299f728fc847bb91 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 20:06:27 +0200 Subject: [PATCH 13/34] feat: updatePlayerName keeps created order in nameCount --- lib/data/dao/player_dao.dart | 80 +++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 40c842a..a6fd1c5 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -159,49 +159,63 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /* Update */ /// Updates the name of the player with the given [playerId] to [name]. + /// + /// Keeps the `nameCount` values of the affected name groups consistent: + /// - The renamed player gets a fresh `nameCount` for the new name group. + /// - All players in the previous name group whose `nameCount` was greater + /// than the removed one get decremented by 1, so the numbering stays + /// contiguous (1..N) in `createdAt` order. + /// - If only one player remains in the previous name group, their + /// `nameCount` is reset to 0. Future updatePlayerName({ required String playerId, required String name, }) async { - // Get previous name and name count for the player before updating - final previousPlayerName = - await (select(playerTable)..where((p) => p.id.equals(playerId))) - .map((row) => row.name) - .getSingleOrNull() ?? - ''; + return transaction(() async { + final previousPlayer = await (select( + playerTable, + )..where((p) => p.id.equals(playerId))).getSingleOrNull(); + if (previousPlayer == null) return false; - // Update name count for the new name - final newNameCount = await _processNameCount(name: name); + final previousName = previousPlayer.name; + final previousCount = previousPlayer.nameCount; - // Update name and nameCount - final rowsAffected = - await (update(playerTable)..where((p) => p.id.equals(playerId))).write( - PlayerTableCompanion( - name: Value(name), - nameCount: Value(newNameCount), - ), - ); + // Determine the nameCount for the renamed player in the new group. + final newNameCount = await _processNameCount(name: name); - // Updating name count for the previous name - final previousNameCount = await getNameCount(name: previousPlayerName); - if (previousNameCount > 0) { - // At least one more player with the previous name + final rowsAffected = + await (update( + playerTable, + )..where((p) => p.id.equals(playerId))).write( + PlayerTableCompanion( + name: Value(name), + nameCount: Value(newNameCount), + ), + ); - // Get the player with the highest count - final player = await getPlayerWithHighestNameCount( - name: previousPlayerName, - ); + // Consolidate the previous name group. + final remainingCount = await getNameCount(name: previousName); - if (previousNameCount > 1) { - // Multiple players - final nameCount = await getNameCount(name: previousPlayerName); - await updateNameCount(playerId: player!.id, nameCount: nameCount); - } else { - // Only one player - await updateNameCount(playerId: player!.id, nameCount: 0); + if (remainingCount == 1) { + // Only one player left + await (update(playerTable)..where((p) => p.name.equals(previousName))) + .write(const PlayerTableCompanion(nameCount: Value(0))); + } else if (remainingCount > 1 && previousCount > 0) { + // Shift every player above the gap down by one to keep numbering in order. + await (update(playerTable)..where( + (p) => + p.name.equals(previousName) & + p.nameCount.isBiggerThanValue(previousCount), + )) + .write( + PlayerTableCompanion.custom( + nameCount: playerTable.nameCount - const Constant(1), + ), + ); } - } - return rowsAffected > 0; + + return rowsAffected > 0; + }); } /// Updates the description of the player with the given [playerId] to From 9ad50c9f9c28fd4ca0e4f000675a854b2e56c7b6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 23 May 2026 21:52:32 +0200 Subject: [PATCH 14/34] Removed bg color --- .../main_menu/match_view/create_match/create_game_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 998f4e1..a5729be 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -117,7 +117,6 @@ class _CreateGameViewState extends State { return ScaffoldMessenger( child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( title: Text(isEditing ? loc.edit_game : loc.create_game), actions: [ From 134f77c5a3160eb5197c9906d9de8e638c9570d5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 01:26:08 +0200 Subject: [PATCH 15/34] feat: create statistics view --- lib/core/enums.dart | 28 + lib/data/models/statistic.dart | 19 + lib/l10n/arb/app_de.arb | 35 + lib/l10n/arb/app_en.arb | 35 + lib/l10n/generated/app_localizations.dart | 210 ++++++ lib/l10n/generated/app_localizations_de.dart | 110 +++ lib/l10n/generated/app_localizations_en.dart | 108 +++ .../main_menu/custom_navigation_bar.dart | 2 +- .../create_statistic_view.dart | 635 ++++++++++++++++++ .../statistics_view.dart | 169 +++-- pubspec.lock | 16 + pubspec.yaml | 2 + 12 files changed, 1296 insertions(+), 73 deletions(-) create mode 100644 lib/data/models/statistic.dart create mode 100644 lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart rename lib/presentation/views/main_menu/{ => statistics_view}/statistics_view.dart (62%) diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 99141e4..5d46a3f 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -44,3 +44,31 @@ enum Ruleset { /// Different colors for highlighting games enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } + +enum StatisticType { + totalMatches, + totalWins, + totalScore, + totalLosses, + averageScore, + bestScore, + worstScore, + winrate, +} + +enum StatisticScope { + allPlayers, + //selectedPlayer, + selectedGroups, + selectedGames, + timeframe, +} + +enum Timeframe { + last7Days, + last30Days, + last90Days, + last180Days, + lastYear, + allTime, +} diff --git a/lib/data/models/statistic.dart b/lib/data/models/statistic.dart new file mode 100644 index 0000000..4b5df07 --- /dev/null +++ b/lib/data/models/statistic.dart @@ -0,0 +1,19 @@ +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; + +class Statistic { + final StatisticType type; + final List scopes; + final Timeframe? timeframe; + final List? selectedGroups; + final List? selectedGames; + + Statistic({ + required this.type, + required this.scopes, + this.timeframe, + this.selectedGroups, + this.selectedGames, + }); +} diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 610d2c9..8f92cb0 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -26,6 +26,17 @@ "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", "create_new_match": "Neues Spiel erstellen", + "create_statistic": "Statistik erstellen", + "create_statistic_classifier_subtitle": "Wähle die anzuzeigende Hauptmetrik aus", + "create_statistic_classifier_title": "Klassifikator", + "create_statistic_games_subtitle": "Wähle die gefilterten Spielvorlagen", + "create_statistic_games_title": "Spielvorlagen", + "create_statistic_groups_subtitle": "Wähle die gefilterten Gruppen", + "create_statistic_groups_title": "Gruppen", + "create_statistic_scope_subtitle": "Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.", + "create_statistic_scope_title": "Bereich", + "create_statistic_timeframe_subtitle": "Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.", + "create_statistic_timeframe_title": "Zeitraum", "created_on": "Erstellt am", "data": "Daten", "data_successfully_deleted": "Daten erfolgreich gelöscht", @@ -82,6 +93,7 @@ "legal_notice": "Impressum", "licenses": "Lizenzen", "live_edit_mode": "Live-Bearbeitungsmodus", + "loading": "Lädt...", "loser": "Verlierer:in", "lowest_score": "Niedrigste Punkte", "match_in_progress": "Spiel läuft...", @@ -134,6 +146,11 @@ "save_changes": "Änderungen speichern", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", + "select_a_classifier": "Klassifikator auswählen", + "select_a_game": "Spielvorlage auswählen", + "select_a_group": "Gruppe auswählen", + "select_a_scope": "Bereich auswählen", + "select_a_timeframe": "Zeitraum auswählen", "select_loser": "Verlierer:in wählen", "select_winner": "Gewinner:in wählen", "select_winners": "Gewinner:innen wählen", @@ -142,6 +159,18 @@ "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", + "statistic_scope_all_players": "Alle Spieler:innen", + "statistic_scope_selected_games": "Ausgewählte Spielvorlagen", + "statistic_scope_selected_groups": "Ausgewählte Gruppen", + "statistic_scope_timeframe": "Zeitraum", + "statistic_type_average_score": "Durchschnittliche Punktzahl", + "statistic_type_best_score": "Beste Punktzahl", + "statistic_type_total_losses": "Niederlagen insgesamt", + "statistic_type_total_matches": "Spiele insgesamt", + "statistic_type_total_score": "Punktzahl insgesamt", + "statistic_type_total_wins": "Siege insgesamt", + "statistic_type_winrate": "Siegquote", + "statistic_type_worst_score": "Schlechteste Punktzahl", "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", @@ -149,6 +178,12 @@ "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", + "timeframe_all_time": "Gesamter Zeitraum", + "timeframe_last_180_days": "Letzte 180 Tage", + "timeframe_last_30_days": "Letzte 30 Tage", + "timeframe_last_7_days": "Letzte 7 Tage", + "timeframe_last_90_days": "Letzte 90 Tage", + "timeframe_last_year": "Letztes Jahr", "today_at": "Heute um", "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a8b0634..b7548bd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -26,6 +26,17 @@ "create_match": "Create match", "create_new_group": "Create new group", "create_new_match": "Create new match", + "create_statistic": "Create statistic", + "create_statistic_classifier_subtitle": "Select which key metric you want to display", + "create_statistic_classifier_title": "Classifier", + "create_statistic_games_subtitle": "Select the filtered games", + "create_statistic_games_title": "Games", + "create_statistic_groups_subtitle": "Select the filtered groups", + "create_statistic_groups_title": "Groups", + "create_statistic_scope_subtitle": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", + "create_statistic_scope_title": "Scope", + "create_statistic_timeframe_subtitle": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", + "create_statistic_timeframe_title": "Timeframe", "created_on": "Created on", "data": "Data", "data_successfully_deleted": "Data successfully deleted", @@ -82,6 +93,7 @@ "legal_notice": "Legal Notice", "licenses": "Licenses", "live_edit_mode": "Live Edit Mode", + "loading": "Loading...", "loser": "Loser", "lowest_score": "Lowest Score", "match_in_progress": "Match in progress...", @@ -139,10 +151,27 @@ "selected_players": "Selected players", "set_name": "Set name", "settings": "Settings", + "select_a_classifier": "Select a classifier", + "select_a_game": "Select a game", + "select_a_group": "Select a group", + "select_a_scope": "Select a scope", + "select_a_timeframe": "Select a timeframe", "single_loser": "Single Loser", "single_winner": "Single Winner", "statistics": "Statistics", "stats": "Stats", + "statistic_scope_all_players": "All players", + "statistic_scope_selected_games": "Selected games", + "statistic_scope_selected_groups": "Selected groups", + "statistic_scope_timeframe": "Timeframe", + "statistic_type_average_score": "Average score", + "statistic_type_best_score": "Best score", + "statistic_type_total_losses": "Total losses", + "statistic_type_total_matches": "Total matches", + "statistic_type_total_score": "Total score", + "statistic_type_total_wins": "Total wins", + "statistic_type_winrate": "Winrate", + "statistic_type_worst_score": "Worst score", "successfully_added_player": "Successfully added player {playerName}", "@successfully_added_player": { "description": "Success message when adding a player", @@ -157,6 +186,12 @@ "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", + "timeframe_all_time": "All time", + "timeframe_last_180_days": "Last 180 days", + "timeframe_last_30_days": "Last 30 days", + "timeframe_last_7_days": "Last 7 days", + "timeframe_last_90_days": "Last 90 days", + "timeframe_last_year": "Last year", "today_at": "Today at", "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 4cee263..e8af2e8 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -254,6 +254,72 @@ abstract class AppLocalizations { /// **'Create new match'** String get create_new_match; + /// No description provided for @create_statistic. + /// + /// In en, this message translates to: + /// **'Create statistic'** + String get create_statistic; + + /// No description provided for @create_statistic_classifier_subtitle. + /// + /// In en, this message translates to: + /// **'Select which key metric you want to display'** + String get create_statistic_classifier_subtitle; + + /// No description provided for @create_statistic_classifier_title. + /// + /// In en, this message translates to: + /// **'Classifier'** + String get create_statistic_classifier_title; + + /// No description provided for @create_statistic_games_subtitle. + /// + /// In en, this message translates to: + /// **'Select the filtered games'** + String get create_statistic_games_subtitle; + + /// No description provided for @create_statistic_games_title. + /// + /// In en, this message translates to: + /// **'Games'** + String get create_statistic_games_title; + + /// No description provided for @create_statistic_groups_subtitle. + /// + /// In en, this message translates to: + /// **'Select the filtered groups'** + String get create_statistic_groups_subtitle; + + /// No description provided for @create_statistic_groups_title. + /// + /// In en, this message translates to: + /// **'Groups'** + String get create_statistic_groups_title; + + /// No description provided for @create_statistic_scope_subtitle. + /// + /// In en, this message translates to: + /// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'** + String get create_statistic_scope_subtitle; + + /// No description provided for @create_statistic_scope_title. + /// + /// In en, this message translates to: + /// **'Scope'** + String get create_statistic_scope_title; + + /// No description provided for @create_statistic_timeframe_subtitle. + /// + /// In en, this message translates to: + /// **'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'** + String get create_statistic_timeframe_subtitle; + + /// No description provided for @create_statistic_timeframe_title. + /// + /// In en, this message translates to: + /// **'Timeframe'** + String get create_statistic_timeframe_title; + /// No description provided for @created_on. /// /// In en, this message translates to: @@ -548,6 +614,12 @@ abstract class AppLocalizations { /// **'Live Edit Mode'** String get live_edit_mode; + /// No description provided for @loading. + /// + /// In en, this message translates to: + /// **'Loading...'** + String get loading; + /// No description provided for @loser. /// /// In en, this message translates to: @@ -890,6 +962,36 @@ abstract class AppLocalizations { /// **'Settings'** String get settings; + /// No description provided for @select_a_classifier. + /// + /// In en, this message translates to: + /// **'Select a classifier'** + String get select_a_classifier; + + /// No description provided for @select_a_game. + /// + /// In en, this message translates to: + /// **'Select a game'** + String get select_a_game; + + /// No description provided for @select_a_group. + /// + /// In en, this message translates to: + /// **'Select a group'** + String get select_a_group; + + /// No description provided for @select_a_scope. + /// + /// In en, this message translates to: + /// **'Select a scope'** + String get select_a_scope; + + /// No description provided for @select_a_timeframe. + /// + /// In en, this message translates to: + /// **'Select a timeframe'** + String get select_a_timeframe; + /// No description provided for @single_loser. /// /// In en, this message translates to: @@ -914,6 +1016,78 @@ abstract class AppLocalizations { /// **'Stats'** String get stats; + /// No description provided for @statistic_scope_all_players. + /// + /// In en, this message translates to: + /// **'All players'** + String get statistic_scope_all_players; + + /// No description provided for @statistic_scope_selected_games. + /// + /// In en, this message translates to: + /// **'Selected games'** + String get statistic_scope_selected_games; + + /// No description provided for @statistic_scope_selected_groups. + /// + /// In en, this message translates to: + /// **'Selected groups'** + String get statistic_scope_selected_groups; + + /// No description provided for @statistic_scope_timeframe. + /// + /// In en, this message translates to: + /// **'Timeframe'** + String get statistic_scope_timeframe; + + /// No description provided for @statistic_type_average_score. + /// + /// In en, this message translates to: + /// **'Average score'** + String get statistic_type_average_score; + + /// No description provided for @statistic_type_best_score. + /// + /// In en, this message translates to: + /// **'Best score'** + String get statistic_type_best_score; + + /// No description provided for @statistic_type_total_losses. + /// + /// In en, this message translates to: + /// **'Total losses'** + String get statistic_type_total_losses; + + /// No description provided for @statistic_type_total_matches. + /// + /// In en, this message translates to: + /// **'Total matches'** + String get statistic_type_total_matches; + + /// No description provided for @statistic_type_total_score. + /// + /// In en, this message translates to: + /// **'Total score'** + String get statistic_type_total_score; + + /// No description provided for @statistic_type_total_wins. + /// + /// In en, this message translates to: + /// **'Total wins'** + String get statistic_type_total_wins; + + /// No description provided for @statistic_type_winrate. + /// + /// In en, this message translates to: + /// **'Winrate'** + String get statistic_type_winrate; + + /// No description provided for @statistic_type_worst_score. + /// + /// In en, this message translates to: + /// **'Worst score'** + String get statistic_type_worst_score; + /// Success message when adding a player /// /// In en, this message translates to: @@ -944,6 +1118,42 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; + /// No description provided for @timeframe_all_time. + /// + /// In en, this message translates to: + /// **'All time'** + String get timeframe_all_time; + + /// No description provided for @timeframe_last_180_days. + /// + /// In en, this message translates to: + /// **'Last 180 days'** + String get timeframe_last_180_days; + + /// No description provided for @timeframe_last_30_days. + /// + /// In en, this message translates to: + /// **'Last 30 days'** + String get timeframe_last_30_days; + + /// No description provided for @timeframe_last_7_days. + /// + /// In en, this message translates to: + /// **'Last 7 days'** + String get timeframe_last_7_days; + + /// No description provided for @timeframe_last_90_days. + /// + /// In en, this message translates to: + /// **'Last 90 days'** + String get timeframe_last_90_days; + + /// No description provided for @timeframe_last_year. + /// + /// In en, this message translates to: + /// **'Last year'** + String get timeframe_last_year; + /// No description provided for @today_at. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 66dca88..4ff81bb 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -88,6 +88,44 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_match => 'Neues Spiel erstellen'; + @override + String get create_statistic => 'Statistik erstellen'; + + @override + String get create_statistic_classifier_subtitle => + 'Wähle die anzuzeigende Hauptmetrik aus'; + + @override + String get create_statistic_classifier_title => 'Klassifikator'; + + @override + String get create_statistic_games_subtitle => + 'Wähle die gefilterten Spielvorlagen'; + + @override + String get create_statistic_games_title => 'Spielvorlagen'; + + @override + String get create_statistic_groups_subtitle => + 'Wähle die gefilterten Gruppen'; + + @override + String get create_statistic_groups_title => 'Gruppen'; + + @override + String get create_statistic_scope_subtitle => + 'Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.'; + + @override + String get create_statistic_scope_title => 'Bereich'; + + @override + String get create_statistic_timeframe_subtitle => + 'Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.'; + + @override + String get create_statistic_timeframe_title => 'Zeitraum'; + @override String get created_on => 'Erstellt am'; @@ -249,6 +287,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override + String get loading => 'Lädt...'; + @override String get loser => 'Verlierer:in'; @@ -426,6 +467,21 @@ class AppLocalizationsDe extends AppLocalizations { @override String get settings => 'Einstellungen'; + @override + String get select_a_classifier => 'Klassifikator auswählen'; + + @override + String get select_a_game => 'Spielvorlage auswählen'; + + @override + String get select_a_group => 'Gruppe auswählen'; + + @override + String get select_a_scope => 'Bereich auswählen'; + + @override + String get select_a_timeframe => 'Zeitraum auswählen'; + @override String get single_loser => 'Ein:e Verlierer:in'; @@ -438,6 +494,42 @@ class AppLocalizationsDe extends AppLocalizations { @override String get stats => 'Statistiken'; + @override + String get statistic_scope_all_players => 'Alle Spieler:innen'; + + @override + String get statistic_scope_selected_games => 'Ausgewählte Spielvorlagen'; + + @override + String get statistic_scope_selected_groups => 'Ausgewählte Gruppen'; + + @override + String get statistic_scope_timeframe => 'Zeitraum'; + + @override + String get statistic_type_average_score => 'Durchschnittliche Punktzahl'; + + @override + String get statistic_type_best_score => 'Beste Punktzahl'; + + @override + String get statistic_type_total_losses => 'Niederlagen insgesamt'; + + @override + String get statistic_type_total_matches => 'Spiele insgesamt'; + + @override + String get statistic_type_total_score => 'Punktzahl insgesamt'; + + @override + String get statistic_type_total_wins => 'Siege insgesamt'; + + @override + String get statistic_type_winrate => 'Siegquote'; + + @override + String get statistic_type_worst_score => 'Schlechteste Punktzahl'; + @override String successfully_added_player(String playerName) { return 'Spieler:in $playerName erfolgreich hinzugefügt'; @@ -458,6 +550,24 @@ class AppLocalizationsDe extends AppLocalizations { @override String get tie => 'Unentschieden'; + @override + String get timeframe_all_time => 'Gesamter Zeitraum'; + + @override + String get timeframe_last_180_days => 'Letzte 180 Tage'; + + @override + String get timeframe_last_30_days => 'Letzte 30 Tage'; + + @override + String get timeframe_last_7_days => 'Letzte 7 Tage'; + + @override + String get timeframe_last_90_days => 'Letzte 90 Tage'; + + @override + String get timeframe_last_year => 'Letztes Jahr'; + @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index b9e467a..4bc1dd7 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -88,6 +88,42 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_match => 'Create new match'; + @override + String get create_statistic => 'Create statistic'; + + @override + String get create_statistic_classifier_subtitle => + 'Select which key metric you want to display'; + + @override + String get create_statistic_classifier_title => 'Classifier'; + + @override + String get create_statistic_games_subtitle => 'Select the filtered games'; + + @override + String get create_statistic_games_title => 'Games'; + + @override + String get create_statistic_groups_subtitle => 'Select the filtered groups'; + + @override + String get create_statistic_groups_title => 'Groups'; + + @override + String get create_statistic_scope_subtitle => + 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; + + @override + String get create_statistic_scope_title => 'Scope'; + + @override + String get create_statistic_timeframe_subtitle => + 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; + + @override + String get create_statistic_timeframe_title => 'Timeframe'; + @override String get created_on => 'Created on'; @@ -249,6 +285,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get live_edit_mode => 'Live Edit Mode'; + @override + String get loading => 'Loading...'; + @override String get loser => 'Loser'; @@ -426,6 +465,21 @@ class AppLocalizationsEn extends AppLocalizations { @override String get settings => 'Settings'; + @override + String get select_a_classifier => 'Select a classifier'; + + @override + String get select_a_game => 'Select a game'; + + @override + String get select_a_group => 'Select a group'; + + @override + String get select_a_scope => 'Select a scope'; + + @override + String get select_a_timeframe => 'Select a timeframe'; + @override String get single_loser => 'Single Loser'; @@ -438,6 +492,42 @@ class AppLocalizationsEn extends AppLocalizations { @override String get stats => 'Stats'; + @override + String get statistic_scope_all_players => 'All players'; + + @override + String get statistic_scope_selected_games => 'Selected games'; + + @override + String get statistic_scope_selected_groups => 'Selected groups'; + + @override + String get statistic_scope_timeframe => 'Timeframe'; + + @override + String get statistic_type_average_score => 'Average score'; + + @override + String get statistic_type_best_score => 'Best score'; + + @override + String get statistic_type_total_losses => 'Total losses'; + + @override + String get statistic_type_total_matches => 'Total matches'; + + @override + String get statistic_type_total_score => 'Total score'; + + @override + String get statistic_type_total_wins => 'Total wins'; + + @override + String get statistic_type_winrate => 'Winrate'; + + @override + String get statistic_type_worst_score => 'Worst score'; + @override String successfully_added_player(String playerName) { return 'Successfully added player $playerName'; @@ -457,6 +547,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get tie => 'Tie'; + @override + String get timeframe_all_time => 'All time'; + + @override + String get timeframe_last_180_days => 'Last 180 days'; + + @override + String get timeframe_last_30_days => 'Last 30 days'; + + @override + String get timeframe_last_7_days => 'Last 7 days'; + + @override + String get timeframe_last_90_days => 'Last 90 days'; + + @override + String get timeframe_last_year => 'Last year'; + @override String get today_at => 'Today at'; diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 7e5434b..07d66b4 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -6,7 +6,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; -import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/statistics_view.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart'; diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart new file mode 100644 index 0000000..17706a1 --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -0,0 +1,635 @@ +import 'package:animated_custom_dropdown/custom_dropdown.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; + +class CreateStatisticView extends StatefulWidget { + const CreateStatisticView({super.key, required this.onStatisticCreated}); + + final void Function() onStatisticCreated; + + @override + State createState() => _CreateStatisticViewState(); +} + +class _CreateStatisticViewState extends State { + bool isLoading = false; + + /* Data loaded from the database */ + List players = []; + List games = []; + List groups = []; + + /* User selections */ + StatisticType? selectedType; + List selectedScope = []; + List selectedGames = []; + List selectedPlayers = []; + List selectedGroups = []; + Timeframe? selectedTimeframe; + + @override + void initState() { + loadAllData(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + var loc = AppLocalizations.of(context); + + return ScaffoldMessenger( + child: Scaffold( + appBar: AppBar(title: Text(loc.create_statistic)), + body: Stack( + alignment: AlignmentDirectional.center, + children: [ + SingleChildScrollView( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).padding.bottom + 80, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Classifier title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_classifier_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_classifier_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + softWrap: true, + ), + ], + ), + ), + + // Classifier selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown( + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + listItemBuilder: + (context, item, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translateStatisticTypeToString(item, context), + style: itemStyle, + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerBuilder: (context, selectedType, enabled) => Text( + translateStatisticTypeToString(selectedType, context), + style: headerStyle, + ), + hintText: loc.select_a_classifier, + items: StatisticType.values, + decoration: decoration, + onChanged: (value) { + setState(() { + selectedType = value; + }); + }, + ), + ), + + const SizedBox(height: 10), + + // Scope title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_scope_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_scope_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // Scope selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown.multiSelect( + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: loc.select_a_scope, + items: StatisticScope.values, + decoration: decoration, + listItemBuilder: + (context, scope, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + translateScopeToString(scope, context), + style: itemStyle, + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerListBuilder: (context, selectedItems, enabled) => + Text( + selectedItems + .map((s) => translateScopeToString(s, context)) + .join(', '), + style: headerStyle, + overflow: TextOverflow.ellipsis, + ), + onListChanged: (List values) { + setState(() { + selectedScope = values; + }); + }, + ), + ), + + if (selectedScope.contains(StatisticScope.selectedGames)) ...[ + const SizedBox(height: 10), + + // games title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_games_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_games_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // game selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown.multiSelect( + enabled: !isLoading, + disabledDecoration: disabledDecoration, + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: isLoading ? loc.loading : loc.select_a_game, + items: games, + decoration: decoration, + listItemBuilder: + (context, item, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + // Name + Text(item.name, style: itemStyle), + const SizedBox(width: 12), + + // Ruleset + Text( + translateRulesetToString( + item.ruleset, + context, + ), + style: hintStyle.copyWith(fontSize: 12), + ), + ], + ), + + // Check icon + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerListBuilder: (context, selectedItems, enabled) => + Text( + selectedItems.map((g) => g.name).join(', '), + style: const TextStyle( + color: CustomTheme.textColor, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + onListChanged: (List values) { + setState(() { + selectedGames = values; + }); + }, + ), + ), + ], + + if (selectedScope.contains( + StatisticScope.selectedGroups, + )) ...[ + const SizedBox(height: 10), + + // groups title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_groups_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_groups_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // groups selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown.multiSelect( + enabled: !isLoading, + disabledDecoration: disabledDecoration, + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: isLoading ? loc.loading : loc.select_a_group, + items: groups, + decoration: decoration, + listItemBuilder: + (context, item, isSelected, onItemSelect) => Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Name + Text(item.name, style: itemStyle), + const SizedBox(width: 12), + + // Ruleset + Text( + ' ${item.members.length.toString()} ${loc.members}', + style: hintStyle.copyWith(fontSize: 12), + ), + ], + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerListBuilder: (context, selectedItems, enabled) => + Text( + selectedItems.map((g) => g.name).join(', '), + style: headerStyle, + overflow: TextOverflow.ellipsis, + ), + onListChanged: (List groups) { + setState(() { + selectedGroups = groups; + }); + }, + ), + ), + ], + + if (selectedScope.contains(StatisticScope.timeframe)) ...[ + const SizedBox(height: 10), + + // timeframe title + Padding( + padding: const EdgeInsetsGeometry.only(left: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.create_statistic_timeframe_title, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + Text( + loc.create_statistic_timeframe_subtitle, + textAlign: TextAlign.start, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 12, + ), + ), + ], + ), + ), + + // groups selection + Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: CustomDropdown( + enabled: !isLoading, + excludeSelected: false, + disabledDecoration: disabledDecoration, + closedHeaderPadding: const EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, + ), + hintText: isLoading + ? loc.loading + : loc.select_a_timeframe, + items: Timeframe.values, + decoration: decoration, + listItemBuilder: + (context, timeframe, isSelected, onItemSelect) => + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text( + translateTimeframeToString( + timeframe, + context, + ), + style: itemStyle, + ), + if (isSelected) + const Icon( + Icons.check, + color: CustomTheme.textColor, + ), + ], + ), + headerBuilder: (context, selectedTimeframe, enabled) => + Text( + translateTimeframeToString( + selectedTimeframe, + context, + ), + style: headerStyle, + overflow: TextOverflow.ellipsis, + ), + onChanged: (Timeframe? timeframe) { + setState(() { + selectedTimeframe = timeframe; + }); + }, + ), + ), + ], + ], + ), + ), + + // Create statistic button + Positioned( + bottom: MediaQuery.of(context).padding.bottom, + child: AnimatedDialogButton( + buttonConstraints: const BoxConstraints(minWidth: 350), + buttonText: loc.create_statistic, + onPressed: selectedType != null && selectedScope.isNotEmpty + ? () => submitStatistic() + : null, + ), + ), + ], + ), + ), + ); + } + + CustomDropdownDecoration get decoration => CustomDropdownDecoration( + listItemDecoration: const ListItemDecoration( + selectedIconBorder: BorderSide(color: CustomTheme.primaryColor, width: 1), + selectedIconColor: CustomTheme.primaryColor, + highlightColor: CustomTheme.secondaryColor, + splashColor: Colors.transparent, + selectedColor: CustomTheme.onBoxColor, + ), + listItemStyle: itemStyle, + headerStyle: headerStyle, + hintStyle: hintStyle, + closedFillColor: CustomTheme.boxColor, + closedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1), + expandedFillColor: CustomTheme.boxColor, + expandedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1), + ); + + CustomDropdownDisabledDecoration get disabledDecoration => + CustomDropdownDisabledDecoration( + fillColor: CustomTheme.boxColor.withAlpha(125), + border: Border.all( + color: CustomTheme.boxBorderColor.withAlpha(125), + width: 1, + ), + headerStyle: disabledHeaderStyle, + hintStyle: disabledHintStyle, + ); + + TextStyle get headerStyle => const TextStyle( + color: CustomTheme.textColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ); + + TextStyle get itemStyle => + const TextStyle(color: CustomTheme.textColor, fontSize: 14); + + TextStyle get hintStyle => + const TextStyle(color: CustomTheme.hintColor, fontSize: 14); + + TextStyle get disabledHeaderStyle => const TextStyle( + color: CustomTheme.hintColor, + fontSize: 14, + fontWeight: FontWeight.bold, + ); + + TextStyle get disabledHintStyle => + const TextStyle(color: CustomTheme.hintColor, fontSize: 14); + + Future loadAllData() async { + isLoading = true; + final db = Provider.of(context, listen: false); + + Future.wait([ + db.playerDao.getAllPlayers(), + db.groupDao.getAllGroups(), + db.gameDao.getAllGames(), + Future.delayed(Constants.MINIMUM_SKELETON_DURATION), + ]) + .then((results) async { + players = results[0]; + groups = results[1]; + games = results[2]; + isLoading = false; + }) + .catchError((error) { + print('Error loading data: $error'); + }); + } + + void submitStatistic() { + final newStatistic = Statistic( + type: selectedType!, + scopes: selectedScope, + timeframe: selectedTimeframe, + selectedGroups: selectedGroups, + selectedGames: selectedGames, + ); + // final db = Provider.of(context, listen: false); + // db.statisticDao.addStatistic(newStatistic); + Navigator.of(context).pop(newStatistic); + } +} + +String translateTimeframeToString(Timeframe timeframe, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (timeframe) { + case Timeframe.last7Days: + return loc.timeframe_last_7_days; + case Timeframe.last30Days: + return loc.timeframe_last_30_days; + case Timeframe.last90Days: + return loc.timeframe_last_90_days; + case Timeframe.last180Days: + return loc.timeframe_last_180_days; + case Timeframe.lastYear: + return loc.timeframe_last_year; + case Timeframe.allTime: + return loc.timeframe_all_time; + } +} + +String translateScopeToString(StatisticScope scope, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (scope) { + case StatisticScope.allPlayers: + return loc.statistic_scope_all_players; + case StatisticScope.selectedGroups: + return loc.statistic_scope_selected_groups; + case StatisticScope.selectedGames: + return loc.statistic_scope_selected_games; + case StatisticScope.timeframe: + return loc.statistic_scope_timeframe; + } +} + +String translateStatisticTypeToString( + StatisticType type, + BuildContext context, +) { + final loc = AppLocalizations.of(context); + switch (type) { + case StatisticType.totalMatches: + return loc.statistic_type_total_matches; + case StatisticType.totalWins: + return loc.statistic_type_total_wins; + case StatisticType.totalScore: + return loc.statistic_type_total_score; + case StatisticType.totalLosses: + return loc.statistic_type_total_losses; + case StatisticType.averageScore: + return loc.statistic_type_average_score; + case StatisticType.bestScore: + return loc.statistic_type_best_score; + case StatisticType.worstScore: + return loc.statistic_type_worst_score; + case StatisticType.winrate: + return loc.statistic_type_winrate; + } +} diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart similarity index 62% rename from lib/presentation/views/main_menu/statistics_view.dart rename to lib/presentation/views/main_menu/statistics_view/statistics_view.dart index 8659a2e..3470a12 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -1,11 +1,14 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart'; @@ -47,85 +50,107 @@ class _StatisticsViewState extends State { final loc = AppLocalizations.of(context); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { - return SingleChildScrollView( - child: AppSkeleton( - enabled: isLoading, - fixLayoutBuilder: true, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + return Stack( + alignment: AlignmentDirectional.center, + children: [ + SingleChildScrollView( + child: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.matches, - icon: Icons.groups_rounded, - value: matchCount, + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.13, + title: loc.matches, + icon: Icons.groups_rounded, + value: matchCount, + ), + SizedBox(width: constraints.maxWidth * 0.05), + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.13, + title: loc.groups, + icon: Icons.groups_rounded, + value: groupCount, + ), + ], ), - SizedBox(width: constraints.maxWidth * 0.05), - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.groups, - icon: Icons.groups_rounded, - value: groupCount, + SizedBox(height: constraints.maxHeight * 0.02), + Visibility( + visible: + winCounts.isEmpty && + matchCounts.isEmpty && + winRates.isEmpty, + replacement: Column( + children: [ + StatisticsTile( + icon: Icons.sports_score, + title: loc.wins, + width: constraints.maxWidth * 0.95, + values: winCounts, + itemCount: 3, + barColor: Colors.green, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.percent, + title: loc.winrate, + width: constraints.maxWidth * 0.95, + values: winRates, + itemCount: 5, + barColor: Colors.orange[700]!, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: loc.amount_of_matches, + width: constraints.maxWidth * 0.95, + values: matchCounts, + itemCount: 10, + barColor: Colors.blue, + ), + ], + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( + context, + ).no_statistics_available, + ), ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), ], ), - SizedBox(height: constraints.maxHeight * 0.02), - Visibility( - visible: - winCounts.isEmpty && - matchCounts.isEmpty && - winRates.isEmpty, - replacement: Column( - children: [ - StatisticsTile( - icon: Icons.sports_score, - title: loc.wins, - width: constraints.maxWidth * 0.95, - values: winCounts, - itemCount: 3, - barColor: Colors.green, - ), - SizedBox(height: constraints.maxHeight * 0.02), - StatisticsTile( - icon: Icons.percent, - title: loc.winrate, - width: constraints.maxWidth * 0.95, - values: winRates, - itemCount: 5, - barColor: Colors.orange[700]!, - ), - SizedBox(height: constraints.maxHeight * 0.02), - StatisticsTile( - icon: Icons.casino, - title: loc.amount_of_matches, - width: constraints.maxWidth * 0.95, - values: matchCounts, - itemCount: 10, - barColor: Colors.blue, - ), - ], - ), - child: TopCenteredMessage( - icon: Icons.info, - title: loc.info, - message: AppLocalizations.of( - context, - ).no_statistics_available, - ), - ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), - ], + ), ), ), - ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: MainMenuButton( + text: loc.create_statistic, + icon: Icons.bar_chart, + onPressed: () { + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => CreateStatisticView( + onStatisticCreated: loadStatisticData, + ), + ), + ); + }, + ), + ), + ], ); }, ); diff --git a/pubspec.lock b/pubspec.lock index 96b2cc5..fc30e57 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "10.0.1" + animated_custom_dropdown: + dependency: "direct main" + description: + name: animated_custom_dropdown + sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486" + url: "https://pub.dev" + source: hosted + version: "3.1.1" arb_utils: dependency: "direct dev" description: @@ -353,6 +361,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.8" + dropdown_flutter: + dependency: "direct main" + description: + name: dropdown_flutter + sha256: "5ae3d05d768d0bb6030ff735e6b4b93f7b29be3cf3bec7c86cd4f444c8f067ff" + url: "https://pub.dev" + source: hosted + version: "1.0.3" equatable: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3d8d99f..8ca8550 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,11 +7,13 @@ environment: sdk: ^3.8.1 dependencies: + animated_custom_dropdown: ^3.1.1 clock: ^1.1.2 collection: ^1.19.1 cupertino_icons: ^1.0.6 drift: ^2.27.0 drift_flutter: ^0.2.4 + dropdown_flutter: ^1.0.3 file_picker: ^11.0.2 file_saver: ^0.3.1 flutter: From d389b93cc5e9801bf83632cd9a197eb57a700399 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 12:16:36 +0200 Subject: [PATCH 16/34] Updated method with join --- lib/data/dao/player_group_dao.dart | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index 48c5653..5139eea 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -39,18 +39,25 @@ class PlayerGroupDao extends DatabaseAccessor /// Retrieves all players belonging to a specific group by [groupId]. Future> getPlayersOfGroup({required String groupId}) async { - final query = select(playerGroupTable) - ..where((pG) => pG.groupId.equals(groupId)); - final result = await query.get(); + final query = select(playerGroupTable).join([ + innerJoin( + playerTable, + playerTable.id.equalsExp(playerGroupTable.playerId), + ), + ])..where(playerGroupTable.groupId.equals(groupId)); - List groupMembers = List.empty(growable: true); - - for (var entry in result) { - final player = await db.playerDao.getPlayerById(playerId: entry.playerId); - groupMembers.add(player); - } - - return groupMembers; + final results = await query.map((row) => row.readTable(playerTable)).get(); + return results + .map( + (result) => Player( + id: result.id, + createdAt: result.createdAt, + name: result.name, + nameCount: result.nameCount, + description: result.description, + ), + ) + .toList(); } /// Checks if a player with [playerId] is in the group with [groupId]. From 37031d66c98aedc5ef297c1be20119b11d85bcf5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 12:16:44 +0200 Subject: [PATCH 17/34] Updated attribute order --- lib/data/models/group.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/data/models/group.dart b/lib/data/models/group.dart index 5c1515c..8b1fe92 100644 --- a/lib/data/models/group.dart +++ b/lib/data/models/group.dart @@ -5,17 +5,17 @@ import 'package:uuid/uuid.dart'; class Group { final String id; - final String name; - final String description; final DateTime createdAt; + final String name; final List members; + final String description; Group({ + required this.name, + required this.members, String? id, DateTime? createdAt, - required this.name, String? description, - required this.members, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), description = description ?? ''; From 807ae61df7e6c4d7bf91fd854d42fbd903bd7cf1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 13:52:27 +0200 Subject: [PATCH 18/34] feat: basic database functionality --- lib/data/dao/statistic_dao.dart | 112 + lib/data/dao/statistic_dao.g.dart | 19 + lib/data/dao/statistic_game_dao.dart | 60 + lib/data/dao/statistic_game_dao.g.dart | 29 + lib/data/dao/statistic_group_dao.dart | 66 + lib/data/dao/statistic_group_dao.g.dart | 29 + lib/data/dao/statistic_scope_dao.dart | 55 + lib/data/dao/statistic_scope_dao.g.dart | 26 + lib/data/db/database.dart | 16 + lib/data/db/database.g.dart | 2842 ++++++++++++++++- lib/data/db/tables/statistic_game_table.dart | 13 + lib/data/db/tables/statistic_group_table.dart | 13 + lib/data/db/tables/statistic_scope_table.dart | 11 + lib/data/db/tables/statistic_table.dart | 10 + lib/data/models/statistic.dart | 10 +- .../create_statistic_view.dart | 4 +- .../statistics_view/statistics_view.dart | 127 +- pubspec.yaml | 2 +- test/db_tests/statistics/statistic_test.dart | 124 + 19 files changed, 3457 insertions(+), 111 deletions(-) create mode 100644 lib/data/dao/statistic_dao.dart create mode 100644 lib/data/dao/statistic_dao.g.dart create mode 100644 lib/data/dao/statistic_game_dao.dart create mode 100644 lib/data/dao/statistic_game_dao.g.dart create mode 100644 lib/data/dao/statistic_group_dao.dart create mode 100644 lib/data/dao/statistic_group_dao.g.dart create mode 100644 lib/data/dao/statistic_scope_dao.dart create mode 100644 lib/data/dao/statistic_scope_dao.g.dart create mode 100644 lib/data/db/tables/statistic_game_table.dart create mode 100644 lib/data/db/tables/statistic_group_table.dart create mode 100644 lib/data/db/tables/statistic_scope_table.dart create mode 100644 lib/data/db/tables/statistic_table.dart create mode 100644 test/db_tests/statistics/statistic_test.dart diff --git a/lib/data/dao/statistic_dao.dart b/lib/data/dao/statistic_dao.dart new file mode 100644 index 0000000..39904fc --- /dev/null +++ b/lib/data/dao/statistic_dao.dart @@ -0,0 +1,112 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/statistic_table.dart'; +import 'package:tallee/data/models/statistic.dart'; + +part 'statistic_dao.g.dart'; + +@DriftAccessor(tables: [StatisticTable]) +class StatisticDao extends DatabaseAccessor + with _$StatisticDaoMixin { + StatisticDao(super.db); + + /* Create */ + + Future addStatistic({required Statistic statistic}) async { + await into(statisticTable).insert( + StatisticTableCompanion.insert( + id: statistic.id, + type: statistic.type.name, + timeframe: Value(statistic.timeframe?.name), + ), + mode: InsertMode.insertOrReplace, + ); + + await db.statisticScopeDao.addStatisticScopes( + statisticId: statistic.id, + scopes: statistic.scopes, + ); + + if (statistic.selectedGroups != null) { + await db.statisticGroupDao.addStatisticGroups( + statisticId: statistic.id, + groups: statistic.selectedGroups!, + ); + } + + if (statistic.selectedGames != null) { + await db.statisticGameDao.addStatisticGames( + statisticId: statistic.id, + games: statistic.selectedGames!, + ); + } + + return true; + } + + /* Read */ + + Future getStatisticById(String statisticId) async { + final query = select(statisticTable); + final row = await query.getSingleOrNull(); + if (row != null) { + final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id); + final games = await db.statisticGameDao.getGamesForStatistic(row.id); + final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id); + + return Statistic( + type: StatisticType.values.firstWhere((type) => type.name == row.type), + scopes: scopes, + id: row.id, + timeframe: Timeframe.values.firstWhereOrNull( + (t) => t.name == row.timeframe, + ), + selectedGroups: groups, + selectedGames: games, + ); + } + return null; + } + + /// Retrieves all statistics from the database, including their associated groups and games. + Future> getAllStatistics() async { + final query = select(statisticTable); + final rows = await query.get(); + return Future.wait( + rows.map((row) async { + final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id); + final games = await db.statisticGameDao.getGamesForStatistic(row.id); + + return Statistic( + type: StatisticType.values.firstWhere( + (type) => type.name == row.type, + ), + scopes: [], + id: row.id, + timeframe: Timeframe.values.firstWhereOrNull( + (t) => t.name == row.timeframe, + ), + selectedGroups: groups, + selectedGames: games, + ); + }), + ); + } + + /* Delete */ + + Future deleteStatistic(String statisticId) async { + final rowsDeleted = await (delete( + statisticTable, + )..where((tbl) => tbl.id.equals(statisticId))).go(); + + return rowsDeleted > 0; + } + + Future deleteAllStatistics() async { + final rowsDeleted = await delete(statisticTable).go(); + return rowsDeleted > 0; + } +} diff --git a/lib/data/dao/statistic_dao.g.dart b/lib/data/dao/statistic_dao.g.dart new file mode 100644 index 0000000..0ce36e1 --- /dev/null +++ b/lib/data/dao/statistic_dao.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'statistic_dao.dart'; + +// ignore_for_file: type=lint +mixin _$StatisticDaoMixin on DatabaseAccessor { + $StatisticTableTable get statisticTable => attachedDatabase.statisticTable; + StatisticDaoManager get managers => StatisticDaoManager(this); +} + +class StatisticDaoManager { + final _$StatisticDaoMixin _db; + StatisticDaoManager(this._db); + $$StatisticTableTableTableManager get statisticTable => + $$StatisticTableTableTableManager( + _db.attachedDatabase, + _db.statisticTable, + ); +} diff --git a/lib/data/dao/statistic_game_dao.dart b/lib/data/dao/statistic_game_dao.dart new file mode 100644 index 0000000..ea7260f --- /dev/null +++ b/lib/data/dao/statistic_game_dao.dart @@ -0,0 +1,60 @@ +import 'package:drift/drift.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/statistic_game_table.dart'; +import 'package:tallee/data/models/game.dart'; + +part 'statistic_game_dao.g.dart'; + +@DriftAccessor(tables: [StatisticGameTable]) +class StatisticGameDao extends DatabaseAccessor + with _$StatisticGameDaoMixin { + StatisticGameDao(super.db); + + /// Retrieves a list of games associated with a specific statistic. + Future> getGamesForStatistic(String statisticId) async { + final query = select(statisticGameTable).join([ + innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)), + ])..where(statisticGameTable.statisticId.equals(statisticId)); + + final results = await query.map((row) => row.readTable(gameTable)).get(); + return results + .map( + (result) => Game( + id: result.id, + name: result.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), + description: result.description, + color: GameColor.values.firstWhere((e) => e.name == result.color), + icon: result.icon, + createdAt: result.createdAt, + ), + ) + .toList(); + } + + Future addStatisticGames({ + required String statisticId, + required List games, + }) { + final entries = games + .map( + (game) => StatisticGameTableCompanion.insert( + statisticId: statisticId, + gameId: game.id, + ), + ) + .toList(); + + return batch((batch) { + batch.insertAll( + statisticGameTable, + entries, + mode: InsertMode.insertOrReplace, + ); + }).then((_) => true).catchError((error) { + print('Error adding statistic games: $error'); + return false; + }); + } +} diff --git a/lib/data/dao/statistic_game_dao.g.dart b/lib/data/dao/statistic_game_dao.g.dart new file mode 100644 index 0000000..d6ee984 --- /dev/null +++ b/lib/data/dao/statistic_game_dao.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'statistic_game_dao.dart'; + +// ignore_for_file: type=lint +mixin _$StatisticGameDaoMixin on DatabaseAccessor { + $StatisticTableTable get statisticTable => attachedDatabase.statisticTable; + $GameTableTable get gameTable => attachedDatabase.gameTable; + $StatisticGameTableTable get statisticGameTable => + attachedDatabase.statisticGameTable; + StatisticGameDaoManager get managers => StatisticGameDaoManager(this); +} + +class StatisticGameDaoManager { + final _$StatisticGameDaoMixin _db; + StatisticGameDaoManager(this._db); + $$StatisticTableTableTableManager get statisticTable => + $$StatisticTableTableTableManager( + _db.attachedDatabase, + _db.statisticTable, + ); + $$GameTableTableTableManager get gameTable => + $$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable); + $$StatisticGameTableTableTableManager get statisticGameTable => + $$StatisticGameTableTableTableManager( + _db.attachedDatabase, + _db.statisticGameTable, + ); +} diff --git a/lib/data/dao/statistic_group_dao.dart b/lib/data/dao/statistic_group_dao.dart new file mode 100644 index 0000000..9eb9397 --- /dev/null +++ b/lib/data/dao/statistic_group_dao.dart @@ -0,0 +1,66 @@ +import 'package:drift/drift.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/statistic_group_table.dart'; +import 'package:tallee/data/models/group.dart'; + +part 'statistic_group_dao.g.dart'; + +@DriftAccessor(tables: [StatisticGroupTable, GroupTable]) +class StatisticGroupDao extends DatabaseAccessor + with _$StatisticGroupDaoMixin { + StatisticGroupDao(super.db); + + /// Retrieves a list of groups associated with a specific statistic. + Future> getGroupsForStatistic(String statisticId) async { + final query = select(statisticGroupTable).join([ + innerJoin( + groupTable, + groupTable.id.equalsExp(statisticGroupTable.groupId), + ), + ])..where(statisticGroupTable.statisticId.equals(statisticId)); + + final results = await query.map((row) => row.readTable(groupTable)).get(); + final groups = await Future.wait( + results.map((result) async { + final groupMembers = await db.playerGroupDao.getPlayersOfGroup( + groupId: result.id, + ); + return Group( + id: result.id, + createdAt: result.createdAt, + name: result.name, + description: result.description, + members: groupMembers, + ); + }), + ); + + return groups; + } + + Future addStatisticGroups({ + required String statisticId, + required List groups, + }) async { + final entries = groups + .map( + (group) => StatisticGroupTableCompanion.insert( + statisticId: statisticId, + groupId: group.id, + ), + ) + .toList(); + + return batch((batch) { + batch.insertAll( + statisticGroupTable, + entries, + mode: InsertMode.insertOrReplace, + ); + }).then((_) => true).catchError((error) { + print('Error adding statistic groups: $error'); + return false; + }); + } +} diff --git a/lib/data/dao/statistic_group_dao.g.dart b/lib/data/dao/statistic_group_dao.g.dart new file mode 100644 index 0000000..57a83c5 --- /dev/null +++ b/lib/data/dao/statistic_group_dao.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'statistic_group_dao.dart'; + +// ignore_for_file: type=lint +mixin _$StatisticGroupDaoMixin on DatabaseAccessor { + $StatisticTableTable get statisticTable => attachedDatabase.statisticTable; + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $StatisticGroupTableTable get statisticGroupTable => + attachedDatabase.statisticGroupTable; + StatisticGroupDaoManager get managers => StatisticGroupDaoManager(this); +} + +class StatisticGroupDaoManager { + final _$StatisticGroupDaoMixin _db; + StatisticGroupDaoManager(this._db); + $$StatisticTableTableTableManager get statisticTable => + $$StatisticTableTableTableManager( + _db.attachedDatabase, + _db.statisticTable, + ); + $$GroupTableTableTableManager get groupTable => + $$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable); + $$StatisticGroupTableTableTableManager get statisticGroupTable => + $$StatisticGroupTableTableTableManager( + _db.attachedDatabase, + _db.statisticGroupTable, + ); +} diff --git a/lib/data/dao/statistic_scope_dao.dart b/lib/data/dao/statistic_scope_dao.dart new file mode 100644 index 0000000..2027acd --- /dev/null +++ b/lib/data/dao/statistic_scope_dao.dart @@ -0,0 +1,55 @@ +import 'package:drift/drift.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/db/tables/statistic_scope_table.dart'; + +part 'statistic_scope_dao.g.dart'; + +@DriftAccessor(tables: [StatisticScopeTable]) +class StatisticScopeDao extends DatabaseAccessor + with _$StatisticScopeDaoMixin { + StatisticScopeDao(super.db); + + /// Retrieves a list of statistic scopes associated with a specific statistic ID. + Future> getScopeForStatistic(String statisticId) async { + final query = select(statisticScopeTable) + ..where((tbl) => tbl.statisticId.equals(statisticId)); + + final results = await query.get(); + return results + .map( + (result) => StatisticScope.values.firstWhere( + (e) => e.name == result.scope, + orElse: () => throw Exception( + 'Invalid scope value: ${result.scope} for statistic ID: $statisticId', + ), + ), + ) + .toList(); + } + + Future addStatisticScopes({ + required String statisticId, + required List scopes, + }) async { + final entries = scopes + .map( + (scope) => StatisticScopeTableCompanion.insert( + statisticId: statisticId, + scope: scope.name, + ), + ) + .toList(); + + return batch((batch) { + batch.insertAll( + statisticScopeTable, + entries, + mode: InsertMode.insertOrReplace, + ); + }).then((_) => true).catchError((error) { + print('Error adding statistic scopes: $error'); + return false; + }); + } +} diff --git a/lib/data/dao/statistic_scope_dao.g.dart b/lib/data/dao/statistic_scope_dao.g.dart new file mode 100644 index 0000000..adaa171 --- /dev/null +++ b/lib/data/dao/statistic_scope_dao.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'statistic_scope_dao.dart'; + +// ignore_for_file: type=lint +mixin _$StatisticScopeDaoMixin on DatabaseAccessor { + $StatisticTableTable get statisticTable => attachedDatabase.statisticTable; + $StatisticScopeTableTable get statisticScopeTable => + attachedDatabase.statisticScopeTable; + StatisticScopeDaoManager get managers => StatisticScopeDaoManager(this); +} + +class StatisticScopeDaoManager { + final _$StatisticScopeDaoMixin _db; + StatisticScopeDaoManager(this._db); + $$StatisticTableTableTableManager get statisticTable => + $$StatisticTableTableTableManager( + _db.attachedDatabase, + _db.statisticTable, + ); + $$StatisticScopeTableTableTableManager get statisticScopeTable => + $$StatisticScopeTableTableTableManager( + _db.attachedDatabase, + _db.statisticScopeTable, + ); +} diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart index a7e9c1d..792da0e 100644 --- a/lib/data/db/database.dart +++ b/lib/data/db/database.dart @@ -8,6 +8,10 @@ import 'package:tallee/data/dao/player_dao.dart'; import 'package:tallee/data/dao/player_group_dao.dart'; import 'package:tallee/data/dao/player_match_dao.dart'; import 'package:tallee/data/dao/score_entry_dao.dart'; +import 'package:tallee/data/dao/statistic_dao.dart'; +import 'package:tallee/data/dao/statistic_game_dao.dart'; +import 'package:tallee/data/dao/statistic_group_dao.dart'; +import 'package:tallee/data/dao/statistic_scope_dao.dart'; import 'package:tallee/data/dao/team_dao.dart'; import 'package:tallee/data/db/tables/game_table.dart'; import 'package:tallee/data/db/tables/group_table.dart'; @@ -16,6 +20,10 @@ import 'package:tallee/data/db/tables/player_group_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/db/tables/score_entry_table.dart'; +import 'package:tallee/data/db/tables/statistic_game_table.dart'; +import 'package:tallee/data/db/tables/statistic_group_table.dart'; +import 'package:tallee/data/db/tables/statistic_scope_table.dart'; +import 'package:tallee/data/db/tables/statistic_table.dart'; import 'package:tallee/data/db/tables/team_table.dart'; part 'database.g.dart'; @@ -30,6 +38,10 @@ part 'database.g.dart'; GameTable, TeamTable, ScoreEntryTable, + StatisticTable, + StatisticScopeTable, + StatisticGameTable, + StatisticGroupTable, ], daos: [ PlayerDao, @@ -40,6 +52,10 @@ part 'database.g.dart'; GameDao, ScoreEntryDao, TeamDao, + StatisticDao, + StatisticScopeDao, + StatisticGameDao, + StatisticGroupDao, ], ) class AppDatabase extends _$AppDatabase { diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index c8d0faa..489b890 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -2732,6 +2732,971 @@ class ScoreEntryTableCompanion extends UpdateCompanion { } } +class $StatisticTableTable extends StatisticTable + with TableInfo<$StatisticTableTable, StatisticTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $StatisticTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _typeMeta = const VerificationMeta('type'); + @override + late final GeneratedColumn type = GeneratedColumn( + 'type', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _timeframeMeta = const VerificationMeta( + 'timeframe', + ); + @override + late final GeneratedColumn timeframe = GeneratedColumn( + 'timeframe', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + @override + List get $columns => [id, type, timeframe]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'statistic_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('type')) { + context.handle( + _typeMeta, + type.isAcceptableOrUnknown(data['type']!, _typeMeta), + ); + } else if (isInserting) { + context.missing(_typeMeta); + } + if (data.containsKey('timeframe')) { + context.handle( + _timeframeMeta, + timeframe.isAcceptableOrUnknown(data['timeframe']!, _timeframeMeta), + ); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + StatisticTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StatisticTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + type: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}type'], + )!, + timeframe: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}timeframe'], + ), + ); + } + + @override + $StatisticTableTable createAlias(String alias) { + return $StatisticTableTable(attachedDatabase, alias); + } +} + +class StatisticTableData extends DataClass + implements Insertable { + final String id; + final String type; + final String? timeframe; + const StatisticTableData({ + required this.id, + required this.type, + this.timeframe, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['type'] = Variable(type); + if (!nullToAbsent || timeframe != null) { + map['timeframe'] = Variable(timeframe); + } + return map; + } + + StatisticTableCompanion toCompanion(bool nullToAbsent) { + return StatisticTableCompanion( + id: Value(id), + type: Value(type), + timeframe: timeframe == null && nullToAbsent + ? const Value.absent() + : Value(timeframe), + ); + } + + factory StatisticTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StatisticTableData( + id: serializer.fromJson(json['id']), + type: serializer.fromJson(json['type']), + timeframe: serializer.fromJson(json['timeframe']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'type': serializer.toJson(type), + 'timeframe': serializer.toJson(timeframe), + }; + } + + StatisticTableData copyWith({ + String? id, + String? type, + Value timeframe = const Value.absent(), + }) => StatisticTableData( + id: id ?? this.id, + type: type ?? this.type, + timeframe: timeframe.present ? timeframe.value : this.timeframe, + ); + StatisticTableData copyWithCompanion(StatisticTableCompanion data) { + return StatisticTableData( + id: data.id.present ? data.id.value : this.id, + type: data.type.present ? data.type.value : this.type, + timeframe: data.timeframe.present ? data.timeframe.value : this.timeframe, + ); + } + + @override + String toString() { + return (StringBuffer('StatisticTableData(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('timeframe: $timeframe') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, type, timeframe); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StatisticTableData && + other.id == this.id && + other.type == this.type && + other.timeframe == this.timeframe); +} + +class StatisticTableCompanion extends UpdateCompanion { + final Value id; + final Value type; + final Value timeframe; + final Value rowid; + const StatisticTableCompanion({ + this.id = const Value.absent(), + this.type = const Value.absent(), + this.timeframe = const Value.absent(), + this.rowid = const Value.absent(), + }); + StatisticTableCompanion.insert({ + required String id, + required String type, + this.timeframe = const Value.absent(), + this.rowid = const Value.absent(), + }) : id = Value(id), + type = Value(type); + static Insertable custom({ + Expression? id, + Expression? type, + Expression? timeframe, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (type != null) 'type': type, + if (timeframe != null) 'timeframe': timeframe, + if (rowid != null) 'rowid': rowid, + }); + } + + StatisticTableCompanion copyWith({ + Value? id, + Value? type, + Value? timeframe, + Value? rowid, + }) { + return StatisticTableCompanion( + id: id ?? this.id, + type: type ?? this.type, + timeframe: timeframe ?? this.timeframe, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (type.present) { + map['type'] = Variable(type.value); + } + if (timeframe.present) { + map['timeframe'] = Variable(timeframe.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StatisticTableCompanion(') + ..write('id: $id, ') + ..write('type: $type, ') + ..write('timeframe: $timeframe, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $StatisticScopeTableTable extends StatisticScopeTable + with TableInfo<$StatisticScopeTableTable, StatisticScopeTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $StatisticScopeTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _statisticIdMeta = const VerificationMeta( + 'statisticId', + ); + @override + late final GeneratedColumn statisticId = GeneratedColumn( + 'statistic_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES statistic_table (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _scopeMeta = const VerificationMeta('scope'); + @override + late final GeneratedColumn scope = GeneratedColumn( + 'scope', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + @override + List get $columns => [statisticId, scope]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'statistic_scope_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('statistic_id')) { + context.handle( + _statisticIdMeta, + statisticId.isAcceptableOrUnknown( + data['statistic_id']!, + _statisticIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_statisticIdMeta); + } + if (data.containsKey('scope')) { + context.handle( + _scopeMeta, + scope.isAcceptableOrUnknown(data['scope']!, _scopeMeta), + ); + } else if (isInserting) { + context.missing(_scopeMeta); + } + return context; + } + + @override + Set get $primaryKey => {statisticId, scope}; + @override + StatisticScopeTableData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StatisticScopeTableData( + statisticId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}statistic_id'], + )!, + scope: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}scope'], + )!, + ); + } + + @override + $StatisticScopeTableTable createAlias(String alias) { + return $StatisticScopeTableTable(attachedDatabase, alias); + } +} + +class StatisticScopeTableData extends DataClass + implements Insertable { + final String statisticId; + final String scope; + const StatisticScopeTableData({ + required this.statisticId, + required this.scope, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['statistic_id'] = Variable(statisticId); + map['scope'] = Variable(scope); + return map; + } + + StatisticScopeTableCompanion toCompanion(bool nullToAbsent) { + return StatisticScopeTableCompanion( + statisticId: Value(statisticId), + scope: Value(scope), + ); + } + + factory StatisticScopeTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StatisticScopeTableData( + statisticId: serializer.fromJson(json['statisticId']), + scope: serializer.fromJson(json['scope']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'statisticId': serializer.toJson(statisticId), + 'scope': serializer.toJson(scope), + }; + } + + StatisticScopeTableData copyWith({String? statisticId, String? scope}) => + StatisticScopeTableData( + statisticId: statisticId ?? this.statisticId, + scope: scope ?? this.scope, + ); + StatisticScopeTableData copyWithCompanion(StatisticScopeTableCompanion data) { + return StatisticScopeTableData( + statisticId: data.statisticId.present + ? data.statisticId.value + : this.statisticId, + scope: data.scope.present ? data.scope.value : this.scope, + ); + } + + @override + String toString() { + return (StringBuffer('StatisticScopeTableData(') + ..write('statisticId: $statisticId, ') + ..write('scope: $scope') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(statisticId, scope); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StatisticScopeTableData && + other.statisticId == this.statisticId && + other.scope == this.scope); +} + +class StatisticScopeTableCompanion + extends UpdateCompanion { + final Value statisticId; + final Value scope; + final Value rowid; + const StatisticScopeTableCompanion({ + this.statisticId = const Value.absent(), + this.scope = const Value.absent(), + this.rowid = const Value.absent(), + }); + StatisticScopeTableCompanion.insert({ + required String statisticId, + required String scope, + this.rowid = const Value.absent(), + }) : statisticId = Value(statisticId), + scope = Value(scope); + static Insertable custom({ + Expression? statisticId, + Expression? scope, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (statisticId != null) 'statistic_id': statisticId, + if (scope != null) 'scope': scope, + if (rowid != null) 'rowid': rowid, + }); + } + + StatisticScopeTableCompanion copyWith({ + Value? statisticId, + Value? scope, + Value? rowid, + }) { + return StatisticScopeTableCompanion( + statisticId: statisticId ?? this.statisticId, + scope: scope ?? this.scope, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (statisticId.present) { + map['statistic_id'] = Variable(statisticId.value); + } + if (scope.present) { + map['scope'] = Variable(scope.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StatisticScopeTableCompanion(') + ..write('statisticId: $statisticId, ') + ..write('scope: $scope, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $StatisticGameTableTable extends StatisticGameTable + with TableInfo<$StatisticGameTableTable, StatisticGameTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $StatisticGameTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _statisticIdMeta = const VerificationMeta( + 'statisticId', + ); + @override + late final GeneratedColumn statisticId = GeneratedColumn( + 'statistic_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES statistic_table (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _gameIdMeta = const VerificationMeta('gameId'); + @override + late final GeneratedColumn gameId = GeneratedColumn( + 'game_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES game_table (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [statisticId, gameId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'statistic_game_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('statistic_id')) { + context.handle( + _statisticIdMeta, + statisticId.isAcceptableOrUnknown( + data['statistic_id']!, + _statisticIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_statisticIdMeta); + } + if (data.containsKey('game_id')) { + context.handle( + _gameIdMeta, + gameId.isAcceptableOrUnknown(data['game_id']!, _gameIdMeta), + ); + } else if (isInserting) { + context.missing(_gameIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {statisticId, gameId}; + @override + StatisticGameTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StatisticGameTableData( + statisticId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}statistic_id'], + )!, + gameId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}game_id'], + )!, + ); + } + + @override + $StatisticGameTableTable createAlias(String alias) { + return $StatisticGameTableTable(attachedDatabase, alias); + } +} + +class StatisticGameTableData extends DataClass + implements Insertable { + final String statisticId; + final String gameId; + const StatisticGameTableData({ + required this.statisticId, + required this.gameId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['statistic_id'] = Variable(statisticId); + map['game_id'] = Variable(gameId); + return map; + } + + StatisticGameTableCompanion toCompanion(bool nullToAbsent) { + return StatisticGameTableCompanion( + statisticId: Value(statisticId), + gameId: Value(gameId), + ); + } + + factory StatisticGameTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StatisticGameTableData( + statisticId: serializer.fromJson(json['statisticId']), + gameId: serializer.fromJson(json['gameId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'statisticId': serializer.toJson(statisticId), + 'gameId': serializer.toJson(gameId), + }; + } + + StatisticGameTableData copyWith({String? statisticId, String? gameId}) => + StatisticGameTableData( + statisticId: statisticId ?? this.statisticId, + gameId: gameId ?? this.gameId, + ); + StatisticGameTableData copyWithCompanion(StatisticGameTableCompanion data) { + return StatisticGameTableData( + statisticId: data.statisticId.present + ? data.statisticId.value + : this.statisticId, + gameId: data.gameId.present ? data.gameId.value : this.gameId, + ); + } + + @override + String toString() { + return (StringBuffer('StatisticGameTableData(') + ..write('statisticId: $statisticId, ') + ..write('gameId: $gameId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(statisticId, gameId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StatisticGameTableData && + other.statisticId == this.statisticId && + other.gameId == this.gameId); +} + +class StatisticGameTableCompanion + extends UpdateCompanion { + final Value statisticId; + final Value gameId; + final Value rowid; + const StatisticGameTableCompanion({ + this.statisticId = const Value.absent(), + this.gameId = const Value.absent(), + this.rowid = const Value.absent(), + }); + StatisticGameTableCompanion.insert({ + required String statisticId, + required String gameId, + this.rowid = const Value.absent(), + }) : statisticId = Value(statisticId), + gameId = Value(gameId); + static Insertable custom({ + Expression? statisticId, + Expression? gameId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (statisticId != null) 'statistic_id': statisticId, + if (gameId != null) 'game_id': gameId, + if (rowid != null) 'rowid': rowid, + }); + } + + StatisticGameTableCompanion copyWith({ + Value? statisticId, + Value? gameId, + Value? rowid, + }) { + return StatisticGameTableCompanion( + statisticId: statisticId ?? this.statisticId, + gameId: gameId ?? this.gameId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (statisticId.present) { + map['statistic_id'] = Variable(statisticId.value); + } + if (gameId.present) { + map['game_id'] = Variable(gameId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StatisticGameTableCompanion(') + ..write('statisticId: $statisticId, ') + ..write('gameId: $gameId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $StatisticGroupTableTable extends StatisticGroupTable + with TableInfo<$StatisticGroupTableTable, StatisticGroupTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $StatisticGroupTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _statisticIdMeta = const VerificationMeta( + 'statisticId', + ); + @override + late final GeneratedColumn statisticId = GeneratedColumn( + 'statistic_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES statistic_table (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _groupIdMeta = const VerificationMeta( + 'groupId', + ); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES group_table (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [statisticId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'statistic_group_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('statistic_id')) { + context.handle( + _statisticIdMeta, + statisticId.isAcceptableOrUnknown( + data['statistic_id']!, + _statisticIdMeta, + ), + ); + } else if (isInserting) { + context.missing(_statisticIdMeta); + } + if (data.containsKey('group_id')) { + context.handle( + _groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta), + ); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {statisticId, groupId}; + @override + StatisticGroupTableData map( + Map data, { + String? tablePrefix, + }) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return StatisticGroupTableData( + statisticId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}statistic_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + $StatisticGroupTableTable createAlias(String alias) { + return $StatisticGroupTableTable(attachedDatabase, alias); + } +} + +class StatisticGroupTableData extends DataClass + implements Insertable { + final String statisticId; + final String groupId; + const StatisticGroupTableData({ + required this.statisticId, + required this.groupId, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['statistic_id'] = Variable(statisticId); + map['group_id'] = Variable(groupId); + return map; + } + + StatisticGroupTableCompanion toCompanion(bool nullToAbsent) { + return StatisticGroupTableCompanion( + statisticId: Value(statisticId), + groupId: Value(groupId), + ); + } + + factory StatisticGroupTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return StatisticGroupTableData( + statisticId: serializer.fromJson(json['statisticId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'statisticId': serializer.toJson(statisticId), + 'groupId': serializer.toJson(groupId), + }; + } + + StatisticGroupTableData copyWith({String? statisticId, String? groupId}) => + StatisticGroupTableData( + statisticId: statisticId ?? this.statisticId, + groupId: groupId ?? this.groupId, + ); + StatisticGroupTableData copyWithCompanion(StatisticGroupTableCompanion data) { + return StatisticGroupTableData( + statisticId: data.statisticId.present + ? data.statisticId.value + : this.statisticId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('StatisticGroupTableData(') + ..write('statisticId: $statisticId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(statisticId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is StatisticGroupTableData && + other.statisticId == this.statisticId && + other.groupId == this.groupId); +} + +class StatisticGroupTableCompanion + extends UpdateCompanion { + final Value statisticId; + final Value groupId; + final Value rowid; + const StatisticGroupTableCompanion({ + this.statisticId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + StatisticGroupTableCompanion.insert({ + required String statisticId, + required String groupId, + this.rowid = const Value.absent(), + }) : statisticId = Value(statisticId), + groupId = Value(groupId); + static Insertable custom({ + Expression? statisticId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (statisticId != null) 'statistic_id': statisticId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + StatisticGroupTableCompanion copyWith({ + Value? statisticId, + Value? groupId, + Value? rowid, + }) { + return StatisticGroupTableCompanion( + statisticId: statisticId ?? this.statisticId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (statisticId.present) { + map['statistic_id'] = Variable(statisticId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('StatisticGroupTableCompanion(') + ..write('statisticId: $statisticId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + abstract class _$AppDatabase extends GeneratedDatabase { _$AppDatabase(QueryExecutor e) : super(e); $AppDatabaseManager get managers => $AppDatabaseManager(this); @@ -2749,6 +3714,13 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final $ScoreEntryTableTable scoreEntryTable = $ScoreEntryTableTable( this, ); + late final $StatisticTableTable statisticTable = $StatisticTableTable(this); + late final $StatisticScopeTableTable statisticScopeTable = + $StatisticScopeTableTable(this); + late final $StatisticGameTableTable statisticGameTable = + $StatisticGameTableTable(this); + late final $StatisticGroupTableTable statisticGroupTable = + $StatisticGroupTableTable(this); late final PlayerDao playerDao = PlayerDao(this as AppDatabase); late final GroupDao groupDao = GroupDao(this as AppDatabase); late final MatchDao matchDao = MatchDao(this as AppDatabase); @@ -2761,6 +3733,16 @@ abstract class _$AppDatabase extends GeneratedDatabase { late final GameDao gameDao = GameDao(this as AppDatabase); late final ScoreEntryDao scoreEntryDao = ScoreEntryDao(this as AppDatabase); late final TeamDao teamDao = TeamDao(this as AppDatabase); + late final StatisticDao statisticDao = StatisticDao(this as AppDatabase); + late final StatisticScopeDao statisticScopeDao = StatisticScopeDao( + this as AppDatabase, + ); + late final StatisticGameDao statisticGameDao = StatisticGameDao( + this as AppDatabase, + ); + late final StatisticGroupDao statisticGroupDao = StatisticGroupDao( + this as AppDatabase, + ); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -2774,6 +3756,10 @@ abstract class _$AppDatabase extends GeneratedDatabase { teamTable, playerMatchTable, scoreEntryTable, + statisticTable, + statisticScopeTable, + statisticGameTable, + statisticGroupTable, ]; @override StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ @@ -2840,6 +3826,41 @@ abstract class _$AppDatabase extends GeneratedDatabase { ), result: [TableUpdate('score_entry_table', kind: UpdateKind.delete)], ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'statistic_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('statistic_scope_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'statistic_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('statistic_game_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'game_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('statistic_game_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'statistic_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('statistic_group_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'group_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('statistic_group_table', kind: UpdateKind.delete)], + ), ]); } @@ -3419,6 +4440,33 @@ final class $$GroupTableTableReferences manager.$state.copyWith(prefetchedData: cache), ); } + + static MultiTypedResultKey< + $StatisticGroupTableTable, + List + > + _statisticGroupTableRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.statisticGroupTable, + aliasName: $_aliasNameGenerator( + db.groupTable.id, + db.statisticGroupTable.groupId, + ), + ); + + $$StatisticGroupTableTableProcessedTableManager get statisticGroupTableRefs { + final manager = $$StatisticGroupTableTableTableManager( + $_db, + $_db.statisticGroupTable, + ).filter((f) => f.groupId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _statisticGroupTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } } class $$GroupTableTableFilterComposer @@ -3499,6 +4547,31 @@ class $$GroupTableTableFilterComposer ); return f(composer); } + + Expression statisticGroupTableRefs( + Expression Function($$StatisticGroupTableTableFilterComposer f) f, + ) { + final $$StatisticGroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGroupTable, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGroupTableTableFilterComposer( + $db: $db, + $table: $db.statisticGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } } class $$GroupTableTableOrderingComposer @@ -3603,6 +4676,32 @@ class $$GroupTableTableAnnotationComposer ); return f(composer); } + + Expression statisticGroupTableRefs( + Expression Function($$StatisticGroupTableTableAnnotationComposer a) f, + ) { + final $$StatisticGroupTableTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGroupTable, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGroupTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } } class $$GroupTableTableTableManager @@ -3621,6 +4720,7 @@ class $$GroupTableTableTableManager PrefetchHooks Function({ bool matchTableRefs, bool playerGroupTableRefs, + bool statisticGroupTableRefs, }) > { $$GroupTableTableTableManager(_$AppDatabase db, $GroupTableTable table) @@ -3671,12 +4771,17 @@ class $$GroupTableTableTableManager ) .toList(), prefetchHooksCallback: - ({matchTableRefs = false, playerGroupTableRefs = false}) { + ({ + matchTableRefs = false, + playerGroupTableRefs = false, + statisticGroupTableRefs = false, + }) { return PrefetchHooks( db: db, explicitlyWatchedTables: [ if (matchTableRefs) db.matchTable, if (playerGroupTableRefs) db.playerGroupTable, + if (statisticGroupTableRefs) db.statisticGroupTable, ], addJoins: null, getPrefetchedDataCallback: (items) async { @@ -3723,6 +4828,27 @@ class $$GroupTableTableTableManager ), typedResults: items, ), + if (statisticGroupTableRefs) + await $_getPrefetchedData< + GroupTableData, + $GroupTableTable, + StatisticGroupTableData + >( + currentTable: table, + referencedTable: $$GroupTableTableReferences + ._statisticGroupTableRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupTableTableReferences( + db, + table, + p0, + ).statisticGroupTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.groupId == item.id, + ), + typedResults: items, + ), ]; }, ); @@ -3743,7 +4869,11 @@ typedef $$GroupTableTableProcessedTableManager = $$GroupTableTableUpdateCompanionBuilder, (GroupTableData, $$GroupTableTableReferences), GroupTableData, - PrefetchHooks Function({bool matchTableRefs, bool playerGroupTableRefs}) + PrefetchHooks Function({ + bool matchTableRefs, + bool playerGroupTableRefs, + bool statisticGroupTableRefs, + }) >; typedef $$GameTableTableCreateCompanionBuilder = GameTableCompanion Function({ @@ -3789,6 +4919,33 @@ final class $$GameTableTableReferences manager.$state.copyWith(prefetchedData: cache), ); } + + static MultiTypedResultKey< + $StatisticGameTableTable, + List + > + _statisticGameTableRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.statisticGameTable, + aliasName: $_aliasNameGenerator( + db.gameTable.id, + db.statisticGameTable.gameId, + ), + ); + + $$StatisticGameTableTableProcessedTableManager get statisticGameTableRefs { + final manager = $$StatisticGameTableTableTableManager( + $_db, + $_db.statisticGameTable, + ).filter((f) => f.gameId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _statisticGameTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } } class $$GameTableTableFilterComposer @@ -3859,6 +5016,31 @@ class $$GameTableTableFilterComposer ); return f(composer); } + + Expression statisticGameTableRefs( + Expression Function($$StatisticGameTableTableFilterComposer f) f, + ) { + final $$StatisticGameTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGameTable, + getReferencedColumn: (t) => t.gameId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGameTableTableFilterComposer( + $db: $db, + $table: $db.statisticGameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } } class $$GameTableTableOrderingComposer @@ -3962,6 +5144,32 @@ class $$GameTableTableAnnotationComposer ); return f(composer); } + + Expression statisticGameTableRefs( + Expression Function($$StatisticGameTableTableAnnotationComposer a) f, + ) { + final $$StatisticGameTableTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGameTable, + getReferencedColumn: (t) => t.gameId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGameTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticGameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } } class $$GameTableTableTableManager @@ -3977,7 +5185,10 @@ class $$GameTableTableTableManager $$GameTableTableUpdateCompanionBuilder, (GameTableData, $$GameTableTableReferences), GameTableData, - PrefetchHooks Function({bool matchTableRefs}) + PrefetchHooks Function({ + bool matchTableRefs, + bool statisticGameTableRefs, + }) > { $$GameTableTableTableManager(_$AppDatabase db, $GameTableTable table) : super( @@ -4038,36 +5249,63 @@ class $$GameTableTableTableManager ), ) .toList(), - prefetchHooksCallback: ({matchTableRefs = false}) { - return PrefetchHooks( - db: db, - explicitlyWatchedTables: [if (matchTableRefs) db.matchTable], - addJoins: null, - getPrefetchedDataCallback: (items) async { - return [ - if (matchTableRefs) - await $_getPrefetchedData< - GameTableData, - $GameTableTable, - MatchTableData - >( - currentTable: table, - referencedTable: $$GameTableTableReferences - ._matchTableRefsTable(db), - managerFromTypedResult: (p0) => - $$GameTableTableReferences( - db, - table, - p0, - ).matchTableRefs, - referencedItemsForCurrentItem: (item, referencedItems) => - referencedItems.where((e) => e.gameId == item.id), - typedResults: items, - ), - ]; + prefetchHooksCallback: + ({matchTableRefs = false, statisticGameTableRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (matchTableRefs) db.matchTable, + if (statisticGameTableRefs) db.statisticGameTable, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (matchTableRefs) + await $_getPrefetchedData< + GameTableData, + $GameTableTable, + MatchTableData + >( + currentTable: table, + referencedTable: $$GameTableTableReferences + ._matchTableRefsTable(db), + managerFromTypedResult: (p0) => + $$GameTableTableReferences( + db, + table, + p0, + ).matchTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.gameId == item.id, + ), + typedResults: items, + ), + if (statisticGameTableRefs) + await $_getPrefetchedData< + GameTableData, + $GameTableTable, + StatisticGameTableData + >( + currentTable: table, + referencedTable: $$GameTableTableReferences + ._statisticGameTableRefsTable(db), + managerFromTypedResult: (p0) => + $$GameTableTableReferences( + db, + table, + p0, + ).statisticGameTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.gameId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); }, - ); - }, ), ); } @@ -4084,7 +5322,7 @@ typedef $$GameTableTableProcessedTableManager = $$GameTableTableUpdateCompanionBuilder, (GameTableData, $$GameTableTableReferences), GameTableData, - PrefetchHooks Function({bool matchTableRefs}) + PrefetchHooks Function({bool matchTableRefs, bool statisticGameTableRefs}) >; typedef $$MatchTableTableCreateCompanionBuilder = MatchTableCompanion Function({ @@ -6273,6 +7511,1536 @@ typedef $$ScoreEntryTableTableProcessedTableManager = ScoreEntryTableData, PrefetchHooks Function({bool playerId, bool matchId}) >; +typedef $$StatisticTableTableCreateCompanionBuilder = + StatisticTableCompanion Function({ + required String id, + required String type, + Value timeframe, + Value rowid, + }); +typedef $$StatisticTableTableUpdateCompanionBuilder = + StatisticTableCompanion Function({ + Value id, + Value type, + Value timeframe, + Value rowid, + }); + +final class $$StatisticTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $StatisticTableTable, + StatisticTableData + > { + $$StatisticTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static MultiTypedResultKey< + $StatisticScopeTableTable, + List + > + _statisticScopeTableRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.statisticScopeTable, + aliasName: $_aliasNameGenerator( + db.statisticTable.id, + db.statisticScopeTable.statisticId, + ), + ); + + $$StatisticScopeTableTableProcessedTableManager get statisticScopeTableRefs { + final manager = $$StatisticScopeTableTableTableManager( + $_db, + $_db.statisticScopeTable, + ).filter((f) => f.statisticId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _statisticScopeTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $StatisticGameTableTable, + List + > + _statisticGameTableRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.statisticGameTable, + aliasName: $_aliasNameGenerator( + db.statisticTable.id, + db.statisticGameTable.statisticId, + ), + ); + + $$StatisticGameTableTableProcessedTableManager get statisticGameTableRefs { + final manager = $$StatisticGameTableTableTableManager( + $_db, + $_db.statisticGameTable, + ).filter((f) => f.statisticId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _statisticGameTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey< + $StatisticGroupTableTable, + List + > + _statisticGroupTableRefsTable(_$AppDatabase db) => + MultiTypedResultKey.fromTable( + db.statisticGroupTable, + aliasName: $_aliasNameGenerator( + db.statisticTable.id, + db.statisticGroupTable.statisticId, + ), + ); + + $$StatisticGroupTableTableProcessedTableManager get statisticGroupTableRefs { + final manager = $$StatisticGroupTableTableTableManager( + $_db, + $_db.statisticGroupTable, + ).filter((f) => f.statisticId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _statisticGroupTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$StatisticTableTableFilterComposer + extends Composer<_$AppDatabase, $StatisticTableTable> { + $$StatisticTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get timeframe => $composableBuilder( + column: $table.timeframe, + builder: (column) => ColumnFilters(column), + ); + + Expression statisticScopeTableRefs( + Expression Function($$StatisticScopeTableTableFilterComposer f) f, + ) { + final $$StatisticScopeTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticScopeTable, + getReferencedColumn: (t) => t.statisticId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticScopeTableTableFilterComposer( + $db: $db, + $table: $db.statisticScopeTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression statisticGameTableRefs( + Expression Function($$StatisticGameTableTableFilterComposer f) f, + ) { + final $$StatisticGameTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGameTable, + getReferencedColumn: (t) => t.statisticId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGameTableTableFilterComposer( + $db: $db, + $table: $db.statisticGameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression statisticGroupTableRefs( + Expression Function($$StatisticGroupTableTableFilterComposer f) f, + ) { + final $$StatisticGroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGroupTable, + getReferencedColumn: (t) => t.statisticId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGroupTableTableFilterComposer( + $db: $db, + $table: $db.statisticGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$StatisticTableTableOrderingComposer + extends Composer<_$AppDatabase, $StatisticTableTable> { + $$StatisticTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get type => $composableBuilder( + column: $table.type, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get timeframe => $composableBuilder( + column: $table.timeframe, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$StatisticTableTableAnnotationComposer + extends Composer<_$AppDatabase, $StatisticTableTable> { + $$StatisticTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get type => + $composableBuilder(column: $table.type, builder: (column) => column); + + GeneratedColumn get timeframe => + $composableBuilder(column: $table.timeframe, builder: (column) => column); + + Expression statisticScopeTableRefs( + Expression Function($$StatisticScopeTableTableAnnotationComposer a) f, + ) { + final $$StatisticScopeTableTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticScopeTable, + getReferencedColumn: (t) => t.statisticId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticScopeTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticScopeTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression statisticGameTableRefs( + Expression Function($$StatisticGameTableTableAnnotationComposer a) f, + ) { + final $$StatisticGameTableTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGameTable, + getReferencedColumn: (t) => t.statisticId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGameTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticGameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression statisticGroupTableRefs( + Expression Function($$StatisticGroupTableTableAnnotationComposer a) f, + ) { + final $$StatisticGroupTableTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.statisticGroupTable, + getReferencedColumn: (t) => t.statisticId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticGroupTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$StatisticTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $StatisticTableTable, + StatisticTableData, + $$StatisticTableTableFilterComposer, + $$StatisticTableTableOrderingComposer, + $$StatisticTableTableAnnotationComposer, + $$StatisticTableTableCreateCompanionBuilder, + $$StatisticTableTableUpdateCompanionBuilder, + (StatisticTableData, $$StatisticTableTableReferences), + StatisticTableData, + PrefetchHooks Function({ + bool statisticScopeTableRefs, + bool statisticGameTableRefs, + bool statisticGroupTableRefs, + }) + > { + $$StatisticTableTableTableManager( + _$AppDatabase db, + $StatisticTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$StatisticTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$StatisticTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$StatisticTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value type = const Value.absent(), + Value timeframe = const Value.absent(), + Value rowid = const Value.absent(), + }) => StatisticTableCompanion( + id: id, + type: type, + timeframe: timeframe, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String type, + Value timeframe = const Value.absent(), + Value rowid = const Value.absent(), + }) => StatisticTableCompanion.insert( + id: id, + type: type, + timeframe: timeframe, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$StatisticTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({ + statisticScopeTableRefs = false, + statisticGameTableRefs = false, + statisticGroupTableRefs = false, + }) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (statisticScopeTableRefs) db.statisticScopeTable, + if (statisticGameTableRefs) db.statisticGameTable, + if (statisticGroupTableRefs) db.statisticGroupTable, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (statisticScopeTableRefs) + await $_getPrefetchedData< + StatisticTableData, + $StatisticTableTable, + StatisticScopeTableData + >( + currentTable: table, + referencedTable: $$StatisticTableTableReferences + ._statisticScopeTableRefsTable(db), + managerFromTypedResult: (p0) => + $$StatisticTableTableReferences( + db, + table, + p0, + ).statisticScopeTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.statisticId == item.id, + ), + typedResults: items, + ), + if (statisticGameTableRefs) + await $_getPrefetchedData< + StatisticTableData, + $StatisticTableTable, + StatisticGameTableData + >( + currentTable: table, + referencedTable: $$StatisticTableTableReferences + ._statisticGameTableRefsTable(db), + managerFromTypedResult: (p0) => + $$StatisticTableTableReferences( + db, + table, + p0, + ).statisticGameTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.statisticId == item.id, + ), + typedResults: items, + ), + if (statisticGroupTableRefs) + await $_getPrefetchedData< + StatisticTableData, + $StatisticTableTable, + StatisticGroupTableData + >( + currentTable: table, + referencedTable: $$StatisticTableTableReferences + ._statisticGroupTableRefsTable(db), + managerFromTypedResult: (p0) => + $$StatisticTableTableReferences( + db, + table, + p0, + ).statisticGroupTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.statisticId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$StatisticTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $StatisticTableTable, + StatisticTableData, + $$StatisticTableTableFilterComposer, + $$StatisticTableTableOrderingComposer, + $$StatisticTableTableAnnotationComposer, + $$StatisticTableTableCreateCompanionBuilder, + $$StatisticTableTableUpdateCompanionBuilder, + (StatisticTableData, $$StatisticTableTableReferences), + StatisticTableData, + PrefetchHooks Function({ + bool statisticScopeTableRefs, + bool statisticGameTableRefs, + bool statisticGroupTableRefs, + }) + >; +typedef $$StatisticScopeTableTableCreateCompanionBuilder = + StatisticScopeTableCompanion Function({ + required String statisticId, + required String scope, + Value rowid, + }); +typedef $$StatisticScopeTableTableUpdateCompanionBuilder = + StatisticScopeTableCompanion Function({ + Value statisticId, + Value scope, + Value rowid, + }); + +final class $$StatisticScopeTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $StatisticScopeTableTable, + StatisticScopeTableData + > { + $$StatisticScopeTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $StatisticTableTable _statisticIdTable(_$AppDatabase db) => + db.statisticTable.createAlias( + $_aliasNameGenerator( + db.statisticScopeTable.statisticId, + db.statisticTable.id, + ), + ); + + $$StatisticTableTableProcessedTableManager get statisticId { + final $_column = $_itemColumn('statistic_id')!; + + final manager = $$StatisticTableTableTableManager( + $_db, + $_db.statisticTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_statisticIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$StatisticScopeTableTableFilterComposer + extends Composer<_$AppDatabase, $StatisticScopeTableTable> { + $$StatisticScopeTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get scope => $composableBuilder( + column: $table.scope, + builder: (column) => ColumnFilters(column), + ); + + $$StatisticTableTableFilterComposer get statisticId { + final $$StatisticTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableFilterComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticScopeTableTableOrderingComposer + extends Composer<_$AppDatabase, $StatisticScopeTableTable> { + $$StatisticScopeTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get scope => $composableBuilder( + column: $table.scope, + builder: (column) => ColumnOrderings(column), + ); + + $$StatisticTableTableOrderingComposer get statisticId { + final $$StatisticTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableOrderingComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticScopeTableTableAnnotationComposer + extends Composer<_$AppDatabase, $StatisticScopeTableTable> { + $$StatisticScopeTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get scope => + $composableBuilder(column: $table.scope, builder: (column) => column); + + $$StatisticTableTableAnnotationComposer get statisticId { + final $$StatisticTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticScopeTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $StatisticScopeTableTable, + StatisticScopeTableData, + $$StatisticScopeTableTableFilterComposer, + $$StatisticScopeTableTableOrderingComposer, + $$StatisticScopeTableTableAnnotationComposer, + $$StatisticScopeTableTableCreateCompanionBuilder, + $$StatisticScopeTableTableUpdateCompanionBuilder, + (StatisticScopeTableData, $$StatisticScopeTableTableReferences), + StatisticScopeTableData, + PrefetchHooks Function({bool statisticId}) + > { + $$StatisticScopeTableTableTableManager( + _$AppDatabase db, + $StatisticScopeTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$StatisticScopeTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$StatisticScopeTableTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$StatisticScopeTableTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value statisticId = const Value.absent(), + Value scope = const Value.absent(), + Value rowid = const Value.absent(), + }) => StatisticScopeTableCompanion( + statisticId: statisticId, + scope: scope, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String statisticId, + required String scope, + Value rowid = const Value.absent(), + }) => StatisticScopeTableCompanion.insert( + statisticId: statisticId, + scope: scope, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$StatisticScopeTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({statisticId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (statisticId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.statisticId, + referencedTable: + $$StatisticScopeTableTableReferences + ._statisticIdTable(db), + referencedColumn: + $$StatisticScopeTableTableReferences + ._statisticIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$StatisticScopeTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $StatisticScopeTableTable, + StatisticScopeTableData, + $$StatisticScopeTableTableFilterComposer, + $$StatisticScopeTableTableOrderingComposer, + $$StatisticScopeTableTableAnnotationComposer, + $$StatisticScopeTableTableCreateCompanionBuilder, + $$StatisticScopeTableTableUpdateCompanionBuilder, + (StatisticScopeTableData, $$StatisticScopeTableTableReferences), + StatisticScopeTableData, + PrefetchHooks Function({bool statisticId}) + >; +typedef $$StatisticGameTableTableCreateCompanionBuilder = + StatisticGameTableCompanion Function({ + required String statisticId, + required String gameId, + Value rowid, + }); +typedef $$StatisticGameTableTableUpdateCompanionBuilder = + StatisticGameTableCompanion Function({ + Value statisticId, + Value gameId, + Value rowid, + }); + +final class $$StatisticGameTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $StatisticGameTableTable, + StatisticGameTableData + > { + $$StatisticGameTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $StatisticTableTable _statisticIdTable(_$AppDatabase db) => + db.statisticTable.createAlias( + $_aliasNameGenerator( + db.statisticGameTable.statisticId, + db.statisticTable.id, + ), + ); + + $$StatisticTableTableProcessedTableManager get statisticId { + final $_column = $_itemColumn('statistic_id')!; + + final manager = $$StatisticTableTableTableManager( + $_db, + $_db.statisticTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_statisticIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $GameTableTable _gameIdTable(_$AppDatabase db) => + db.gameTable.createAlias( + $_aliasNameGenerator(db.statisticGameTable.gameId, db.gameTable.id), + ); + + $$GameTableTableProcessedTableManager get gameId { + final $_column = $_itemColumn('game_id')!; + + final manager = $$GameTableTableTableManager( + $_db, + $_db.gameTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_gameIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$StatisticGameTableTableFilterComposer + extends Composer<_$AppDatabase, $StatisticGameTableTable> { + $$StatisticGameTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$StatisticTableTableFilterComposer get statisticId { + final $$StatisticTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableFilterComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GameTableTableFilterComposer get gameId { + final $$GameTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.gameId, + referencedTable: $db.gameTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GameTableTableFilterComposer( + $db: $db, + $table: $db.gameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticGameTableTableOrderingComposer + extends Composer<_$AppDatabase, $StatisticGameTableTable> { + $$StatisticGameTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$StatisticTableTableOrderingComposer get statisticId { + final $$StatisticTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableOrderingComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GameTableTableOrderingComposer get gameId { + final $$GameTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.gameId, + referencedTable: $db.gameTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GameTableTableOrderingComposer( + $db: $db, + $table: $db.gameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticGameTableTableAnnotationComposer + extends Composer<_$AppDatabase, $StatisticGameTableTable> { + $$StatisticGameTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$StatisticTableTableAnnotationComposer get statisticId { + final $$StatisticTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GameTableTableAnnotationComposer get gameId { + final $$GameTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.gameId, + referencedTable: $db.gameTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GameTableTableAnnotationComposer( + $db: $db, + $table: $db.gameTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticGameTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $StatisticGameTableTable, + StatisticGameTableData, + $$StatisticGameTableTableFilterComposer, + $$StatisticGameTableTableOrderingComposer, + $$StatisticGameTableTableAnnotationComposer, + $$StatisticGameTableTableCreateCompanionBuilder, + $$StatisticGameTableTableUpdateCompanionBuilder, + (StatisticGameTableData, $$StatisticGameTableTableReferences), + StatisticGameTableData, + PrefetchHooks Function({bool statisticId, bool gameId}) + > { + $$StatisticGameTableTableTableManager( + _$AppDatabase db, + $StatisticGameTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$StatisticGameTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$StatisticGameTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$StatisticGameTableTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value statisticId = const Value.absent(), + Value gameId = const Value.absent(), + Value rowid = const Value.absent(), + }) => StatisticGameTableCompanion( + statisticId: statisticId, + gameId: gameId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String statisticId, + required String gameId, + Value rowid = const Value.absent(), + }) => StatisticGameTableCompanion.insert( + statisticId: statisticId, + gameId: gameId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$StatisticGameTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({statisticId = false, gameId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (statisticId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.statisticId, + referencedTable: + $$StatisticGameTableTableReferences + ._statisticIdTable(db), + referencedColumn: + $$StatisticGameTableTableReferences + ._statisticIdTable(db) + .id, + ) + as T; + } + if (gameId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.gameId, + referencedTable: + $$StatisticGameTableTableReferences + ._gameIdTable(db), + referencedColumn: + $$StatisticGameTableTableReferences + ._gameIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$StatisticGameTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $StatisticGameTableTable, + StatisticGameTableData, + $$StatisticGameTableTableFilterComposer, + $$StatisticGameTableTableOrderingComposer, + $$StatisticGameTableTableAnnotationComposer, + $$StatisticGameTableTableCreateCompanionBuilder, + $$StatisticGameTableTableUpdateCompanionBuilder, + (StatisticGameTableData, $$StatisticGameTableTableReferences), + StatisticGameTableData, + PrefetchHooks Function({bool statisticId, bool gameId}) + >; +typedef $$StatisticGroupTableTableCreateCompanionBuilder = + StatisticGroupTableCompanion Function({ + required String statisticId, + required String groupId, + Value rowid, + }); +typedef $$StatisticGroupTableTableUpdateCompanionBuilder = + StatisticGroupTableCompanion Function({ + Value statisticId, + Value groupId, + Value rowid, + }); + +final class $$StatisticGroupTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $StatisticGroupTableTable, + StatisticGroupTableData + > { + $$StatisticGroupTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $StatisticTableTable _statisticIdTable(_$AppDatabase db) => + db.statisticTable.createAlias( + $_aliasNameGenerator( + db.statisticGroupTable.statisticId, + db.statisticTable.id, + ), + ); + + $$StatisticTableTableProcessedTableManager get statisticId { + final $_column = $_itemColumn('statistic_id')!; + + final manager = $$StatisticTableTableTableManager( + $_db, + $_db.statisticTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_statisticIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $GroupTableTable _groupIdTable(_$AppDatabase db) => + db.groupTable.createAlias( + $_aliasNameGenerator(db.statisticGroupTable.groupId, db.groupTable.id), + ); + + $$GroupTableTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupTableTableTableManager( + $_db, + $_db.groupTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$StatisticGroupTableTableFilterComposer + extends Composer<_$AppDatabase, $StatisticGroupTableTable> { + $$StatisticGroupTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$StatisticTableTableFilterComposer get statisticId { + final $$StatisticTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableFilterComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupTableTableFilterComposer get groupId { + final $$GroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableFilterComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticGroupTableTableOrderingComposer + extends Composer<_$AppDatabase, $StatisticGroupTableTable> { + $$StatisticGroupTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$StatisticTableTableOrderingComposer get statisticId { + final $$StatisticTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableOrderingComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupTableTableOrderingComposer get groupId { + final $$GroupTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableOrderingComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticGroupTableTableAnnotationComposer + extends Composer<_$AppDatabase, $StatisticGroupTableTable> { + $$StatisticGroupTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$StatisticTableTableAnnotationComposer get statisticId { + final $$StatisticTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.statisticId, + referencedTable: $db.statisticTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$StatisticTableTableAnnotationComposer( + $db: $db, + $table: $db.statisticTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupTableTableAnnotationComposer get groupId { + final $$GroupTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableAnnotationComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$StatisticGroupTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $StatisticGroupTableTable, + StatisticGroupTableData, + $$StatisticGroupTableTableFilterComposer, + $$StatisticGroupTableTableOrderingComposer, + $$StatisticGroupTableTableAnnotationComposer, + $$StatisticGroupTableTableCreateCompanionBuilder, + $$StatisticGroupTableTableUpdateCompanionBuilder, + (StatisticGroupTableData, $$StatisticGroupTableTableReferences), + StatisticGroupTableData, + PrefetchHooks Function({bool statisticId, bool groupId}) + > { + $$StatisticGroupTableTableTableManager( + _$AppDatabase db, + $StatisticGroupTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$StatisticGroupTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$StatisticGroupTableTableOrderingComposer( + $db: db, + $table: table, + ), + createComputedFieldComposer: () => + $$StatisticGroupTableTableAnnotationComposer( + $db: db, + $table: table, + ), + updateCompanionCallback: + ({ + Value statisticId = const Value.absent(), + Value groupId = const Value.absent(), + Value rowid = const Value.absent(), + }) => StatisticGroupTableCompanion( + statisticId: statisticId, + groupId: groupId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String statisticId, + required String groupId, + Value rowid = const Value.absent(), + }) => StatisticGroupTableCompanion.insert( + statisticId: statisticId, + groupId: groupId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$StatisticGroupTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({statisticId = false, groupId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (statisticId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.statisticId, + referencedTable: + $$StatisticGroupTableTableReferences + ._statisticIdTable(db), + referencedColumn: + $$StatisticGroupTableTableReferences + ._statisticIdTable(db) + .id, + ) + as T; + } + if (groupId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$StatisticGroupTableTableReferences + ._groupIdTable(db), + referencedColumn: + $$StatisticGroupTableTableReferences + ._groupIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$StatisticGroupTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $StatisticGroupTableTable, + StatisticGroupTableData, + $$StatisticGroupTableTableFilterComposer, + $$StatisticGroupTableTableOrderingComposer, + $$StatisticGroupTableTableAnnotationComposer, + $$StatisticGroupTableTableCreateCompanionBuilder, + $$StatisticGroupTableTableUpdateCompanionBuilder, + (StatisticGroupTableData, $$StatisticGroupTableTableReferences), + StatisticGroupTableData, + PrefetchHooks Function({bool statisticId, bool groupId}) + >; class $AppDatabaseManager { final _$AppDatabase _db; @@ -6293,4 +9061,12 @@ class $AppDatabaseManager { $$PlayerMatchTableTableTableManager(_db, _db.playerMatchTable); $$ScoreEntryTableTableTableManager get scoreEntryTable => $$ScoreEntryTableTableTableManager(_db, _db.scoreEntryTable); + $$StatisticTableTableTableManager get statisticTable => + $$StatisticTableTableTableManager(_db, _db.statisticTable); + $$StatisticScopeTableTableTableManager get statisticScopeTable => + $$StatisticScopeTableTableTableManager(_db, _db.statisticScopeTable); + $$StatisticGameTableTableTableManager get statisticGameTable => + $$StatisticGameTableTableTableManager(_db, _db.statisticGameTable); + $$StatisticGroupTableTableTableManager get statisticGroupTable => + $$StatisticGroupTableTableTableManager(_db, _db.statisticGroupTable); } diff --git a/lib/data/db/tables/statistic_game_table.dart b/lib/data/db/tables/statistic_game_table.dart new file mode 100644 index 0000000..e1cc7d4 --- /dev/null +++ b/lib/data/db/tables/statistic_game_table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:tallee/data/db/tables/game_table.dart'; +import 'package:tallee/data/db/tables/statistic_table.dart'; + +class StatisticGameTable extends Table { + TextColumn get statisticId => + text().references(StatisticTable, #id, onDelete: KeyAction.cascade)(); + TextColumn get gameId => + text().references(GameTable, #id, onDelete: KeyAction.cascade)(); + + @override + Set> get primaryKey => {statisticId, gameId}; +} diff --git a/lib/data/db/tables/statistic_group_table.dart b/lib/data/db/tables/statistic_group_table.dart new file mode 100644 index 0000000..cd642ad --- /dev/null +++ b/lib/data/db/tables/statistic_group_table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:tallee/data/db/tables/group_table.dart'; +import 'package:tallee/data/db/tables/statistic_table.dart'; + +class StatisticGroupTable extends Table { + TextColumn get statisticId => + text().references(StatisticTable, #id, onDelete: KeyAction.cascade)(); + TextColumn get groupId => + text().references(GroupTable, #id, onDelete: KeyAction.cascade)(); + + @override + Set> get primaryKey => {statisticId, groupId}; +} diff --git a/lib/data/db/tables/statistic_scope_table.dart b/lib/data/db/tables/statistic_scope_table.dart new file mode 100644 index 0000000..3a9bcdc --- /dev/null +++ b/lib/data/db/tables/statistic_scope_table.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; +import 'package:tallee/data/db/tables/statistic_table.dart'; + +class StatisticScopeTable extends Table { + TextColumn get statisticId => + text().references(StatisticTable, #id, onDelete: KeyAction.cascade)(); + TextColumn get scope => text()(); + + @override + Set> get primaryKey => {statisticId, scope}; +} diff --git a/lib/data/db/tables/statistic_table.dart b/lib/data/db/tables/statistic_table.dart new file mode 100644 index 0000000..d627894 --- /dev/null +++ b/lib/data/db/tables/statistic_table.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; + +class StatisticTable extends Table { + TextColumn get id => text()(); + TextColumn get type => text()(); + TextColumn get timeframe => text().nullable()(); + + @override + Set> get primaryKey => {id}; +} diff --git a/lib/data/models/statistic.dart b/lib/data/models/statistic.dart index 4b5df07..3d118f7 100644 --- a/lib/data/models/statistic.dart +++ b/lib/data/models/statistic.dart @@ -1,8 +1,10 @@ import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; +import 'package:uuid/uuid.dart'; class Statistic { + final String id; final StatisticType type; final List scopes; final Timeframe? timeframe; @@ -12,8 +14,14 @@ class Statistic { Statistic({ required this.type, required this.scopes, + String? id, this.timeframe, this.selectedGroups, this.selectedGames, - }); + }) : id = id ?? const Uuid().v4(); + + @override + String toString() { + return 'Statistic(id: $id, type: $type, scopes: $scopes, timeframe: $timeframe, selectedGroups: $selectedGroups, selectedGames: $selectedGames)'; + } } diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart index 17706a1..9ac03aa 100644 --- a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -571,8 +571,8 @@ class _CreateStatisticViewState extends State { selectedGroups: selectedGroups, selectedGames: selectedGames, ); - // final db = Provider.of(context, listen: false); - // db.statisticDao.addStatistic(newStatistic); + final db = Provider.of(context, listen: false); + db.statisticDao.addStatistic(statistic: newStatistic); Navigator.of(context).pop(newStatistic); } } diff --git a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart index 3470a12..f551a8b 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -5,13 +5,12 @@ import 'package:tallee/core/constants.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; -import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; -import 'package:tallee/presentation/widgets/top_centered_message.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; class StatisticsView extends StatefulWidget { /// A view that displays player statistics @@ -38,11 +37,20 @@ class _StatisticsViewState extends State { 1, )); bool isLoading = true; + List statisticTiles = List.generate( + 4, + (_) => const InfoTile( + icon: Icons.sports_score, + title: 'Skeleton Statistic', + width: double.infinity, + content: Text('Skeleton content'), + ), + ); @override void initState() { super.initState(); - loadStatisticData(); + getStatisticTiles(context); } @override @@ -51,7 +59,8 @@ class _StatisticsViewState extends State { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Stack( - alignment: AlignmentDirectional.center, + alignment: AlignmentDirectional.bottomCenter, + fit: StackFit.expand, children: [ SingleChildScrollView( child: AppSkeleton( @@ -62,73 +71,7 @@ class _StatisticsViewState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.matches, - icon: Icons.groups_rounded, - value: matchCount, - ), - SizedBox(width: constraints.maxWidth * 0.05), - QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.13, - title: loc.groups, - icon: Icons.groups_rounded, - value: groupCount, - ), - ], - ), - SizedBox(height: constraints.maxHeight * 0.02), - Visibility( - visible: - winCounts.isEmpty && - matchCounts.isEmpty && - winRates.isEmpty, - replacement: Column( - children: [ - StatisticsTile( - icon: Icons.sports_score, - title: loc.wins, - width: constraints.maxWidth * 0.95, - values: winCounts, - itemCount: 3, - barColor: Colors.green, - ), - SizedBox(height: constraints.maxHeight * 0.02), - StatisticsTile( - icon: Icons.percent, - title: loc.winrate, - width: constraints.maxWidth * 0.95, - values: winRates, - itemCount: 5, - barColor: Colors.orange[700]!, - ), - SizedBox(height: constraints.maxHeight * 0.02), - StatisticsTile( - icon: Icons.casino, - title: loc.amount_of_matches, - width: constraints.maxWidth * 0.95, - values: matchCounts, - itemCount: 10, - barColor: Colors.blue, - ), - ], - ), - child: TopCenteredMessage( - icon: Icons.info, - title: loc.info, - message: AppLocalizations.of( - context, - ).no_statistics_available, - ), - ), - SizedBox(height: MediaQuery.paddingOf(context).bottom), - ], + children: [...statisticTiles], ), ), ), @@ -138,8 +81,8 @@ class _StatisticsViewState extends State { child: MainMenuButton( text: loc.create_statistic, icon: Icons.bar_chart, - onPressed: () { - Navigator.push( + onPressed: () async { + Statistic newStatistic = await Navigator.push( context, adaptivePageRoute( builder: (context) => CreateStatisticView( @@ -147,6 +90,18 @@ class _StatisticsViewState extends State { ), ), ); + final newTile = InfoTile( + icon: Icons.sports_score, + title: newStatistic.type.name, + width: MediaQuery.sizeOf(context).width * 0.95, + content: Text( + '${newStatistic.id}\n${newStatistic.scopes}\n${newStatistic.type}\n${newStatistic.timeframe}\n${newStatistic.selectedGroups}\n${newStatistic.selectedGames}\n', + ), + ); + + setState(() { + statisticTiles.add(newTile); + }); }, ), ), @@ -196,6 +151,30 @@ class _StatisticsViewState extends State { }); } + Future getStatisticTiles(BuildContext context) async { + isLoading = true; + final db = Provider.of(context, listen: false); + final statistics = await db.statisticDao.getAllStatistics(); + + setState(() { + statisticTiles = []; + for (var statistic in statistics) { + statisticTiles.add( + InfoTile( + icon: Icons.sports_score, + title: statistic.type.name, + width: MediaQuery.sizeOf(context).width * 0.95, + content: Text(statistic.toString()), + ), + ); + statisticTiles.add( + SizedBox(height: MediaQuery.sizeOf(context).height * 0.02), + ); + } + }); + isLoading = false; + } + /// Calculates the number of wins for each player /// and returns a sorted list of tuples (playerName, winCount) List<(Player, int)> _calculateWinsForAllPlayers({ diff --git a/pubspec.yaml b/pubspec.yaml index 8ca8550..b6563cc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.33+273 +version: 0.0.33+274 environment: sdk: ^3.8.1 diff --git a/test/db_tests/statistics/statistic_test.dart b/test/db_tests/statistics/statistic_test.dart new file mode 100644 index 0000000..b969b4c --- /dev/null +++ b/test/db_tests/statistics/statistic_test.dart @@ -0,0 +1,124 @@ +import 'dart:core'; + +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' hide isNotNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; +import 'package:tallee/data/models/statistic.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; + late Group testGroup1; + late Group testGroup2; + late Game testGame; + late Match testMatch1; + late Match testMatch2; + late Match testMatchOnlyPlayers; + late Match testMatchOnlyGroup; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() async { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testGroup1 = Group( + name: 'Test Group 1', + description: '', + members: [testPlayer1, testPlayer2, testPlayer3], + ); + testGroup2 = Group( + name: 'Test Group 2', + description: '', + members: [testPlayer4, testPlayer5], + ); + testGame = Game( + name: 'Test Game', + ruleset: Ruleset.singleWinner, + description: 'A test game', + color: GameColor.blue, + icon: '', + ); + testMatch1 = Match( + name: 'First Test Match', + game: testGame, + group: testGroup1, + players: [testPlayer4, testPlayer5], + scores: {testPlayer4.id: ScoreEntry(score: 1)}, + ); + testMatch2 = Match( + name: 'Second Test Match', + game: testGame, + group: testGroup2, + players: [testPlayer1, testPlayer2, testPlayer3], + ); + testMatchOnlyPlayers = Match( + name: 'Test Match with Players', + game: testGame, + players: [testPlayer1, testPlayer2, testPlayer3], + ); + testMatchOnlyGroup = Match( + name: 'Test Match with Group', + game: testGame, + group: testGroup2, + players: testGroup2.members, + ); + }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); + await database.gameDao.addGame(game: testGame); + }); + tearDown(() async { + await database.close(); + }); + + test('Adding/fetching statistic works correclty', () async { + final statistic = Statistic( + type: StatisticType.averageScore, + scopes: [StatisticScope.allPlayers, StatisticScope.selectedGames], + timeframe: Timeframe.allTime, + selectedGames: [testGame], + selectedGroups: [testGroup1], + ); + + final added = await database.statisticDao.addStatistic( + statistic: statistic, + ); + expect(added, isTrue); + + final fetched = await database.statisticDao.getStatisticById(statistic.id); + expect(fetched, isNotNull); + expect(fetched!.type, statistic.type); + }); +} From 2e3b46253388f10bdd63e84d2c859971d19bce45 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 15:11:56 +0200 Subject: [PATCH 19/34] feat: added statistic tile factory --- .../statistic_tile_factory.dart | 318 ++++++++++++++++++ .../statistics_view/statistics_view.dart | 285 +++------------- .../widgets/tiles/statistics_tile.dart | 223 ++++++++---- 3 files changed, 529 insertions(+), 297 deletions(-) create mode 100644 lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart new file mode 100644 index 0000000..c482a7d --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart @@ -0,0 +1,318 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart' + show translateStatisticTypeToString; +import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; + +/// Build the [StatisticsTile] for a given [Statistic]. + +Widget buildStatisticTile({ + required Statistic statistic, + required List matches, + required List players, + required BuildContext context, + double? width, + int itemCount = 5, +}) { + final filteredMatches = _getFilterMatches(statistic, matches); + final filteredPlayers = _getFilteredPlayers( + statistic, + players, + filteredMatches, + ); + + print('Building tile for statistic: $statistic'); + print('Filtered matches count: ${filteredMatches.length}'); + print('Filtered players count: ${filteredPlayers.length}'); + + final values = _computeValuesForType( + type: statistic.type, + matches: filteredMatches, + players: filteredPlayers, + ); + print(values); + + return StatisticsTile( + icon: _getStatisticIcon(type: statistic.type), + title: translateStatisticTypeToString(statistic.type, context), + width: width ?? MediaQuery.sizeOf(context).width * 0.95, + values: values, + itemCount: itemCount, + barColor: _getStatisticColor(statistic), + statistic: statistic, + ); +} + +List _getFilterMatches(Statistic statistic, List matches) { + List filteredMatches = matches; + + // Filter timeframe + if (statistic.scopes.contains(StatisticScope.timeframe) && + statistic.timeframe != null) { + final minDate = _getMinimumDate(timeframe: statistic.timeframe!); + print( + 'Filtering matches by timeframe: ${statistic.timeframe}, minDate: $minDate', + ); + if (minDate != null) { + filteredMatches = matches + .where((m) => m.endedAt != null && m.endedAt!.isAfter(minDate)) + .toList(); + } + } + + // Filter games + if (statistic.scopes.contains(StatisticScope.selectedGames) && + (statistic.selectedGames?.isNotEmpty ?? false)) { + final gameIds = statistic.selectedGames!.map((g) => g.id).toSet(); + filteredMatches = filteredMatches + .where((match) => gameIds.contains(match.game.id)) + .toList(); + } + + // Filter groups + if (statistic.scopes.contains(StatisticScope.selectedGroups) && + (statistic.selectedGroups?.isNotEmpty ?? false)) { + final groupIds = statistic.selectedGroups!.map((g) => g.id).toSet(); + filteredMatches = filteredMatches + .where((m) => m.group != null && groupIds.contains(m.group!.id)) + .toList(); + } + + return filteredMatches; +} + +/// Returns a [Player] List with the selected players depending on +List _getFilteredPlayers( + Statistic statistic, + List allPlayers, + List filteredMatches, +) { + // allPlayers + if (statistic.scopes.contains(StatisticScope.allPlayers)) { + return allPlayers; + } + + // selectedGroups -> only members + if (statistic.scopes.contains(StatisticScope.selectedGroups) && + (statistic.selectedGroups?.isNotEmpty ?? false)) { + final Set ids = { + for (final g in statistic.selectedGroups!) + for (final p in g.members) p.id, + }; + return allPlayers.where((p) => ids.contains(p.id)).toList(); + } + + // Else -> all players from filtered matches + final Set ids = { + for (final m in filteredMatches) + for (final p in m.players) p.id, + }; + return allPlayers.where((p) => ids.contains(p.id)).toList(); +} + +/// Returns a [DateTime] with the minimum time and date the [timeframe] allows +DateTime? _getMinimumDate({required Timeframe timeframe}) { + final now = DateTime.now(); + switch (timeframe) { + case Timeframe.last7Days: + return now.subtract(const Duration(days: 7)); + case Timeframe.last30Days: + return now.subtract(const Duration(days: 30)); + case Timeframe.last90Days: + return now.subtract(const Duration(days: 90)); + case Timeframe.last180Days: + return now.subtract(const Duration(days: 180)); + case Timeframe.lastYear: + return now.subtract(const Duration(days: 365)); + case Timeframe.allTime: + return null; + } +} + +/// Computes the statistic values for each player based on the statistic type +/// and returns a list of (Player, value) tuples sorted descending by value. +List<(Player, num)> _computeValuesForType({ + required StatisticType type, + required List matches, + required List players, +}) { + switch (type) { + case StatisticType.totalMatches: + return _sortDesc( + players.map((p) => (p, _matchesPlayed(p, matches) as num)).toList(), + ); + + case StatisticType.totalWins: + return _sortDesc( + players.map((p) => (p, _wins(p, matches) as num)).toList(), + ); + + case StatisticType.totalLosses: + return _sortDesc( + players + .map( + (p) => + (p, (_matchesPlayed(p, matches) - _wins(p, matches)) as num), + ) + .toList(), + ); + + case StatisticType.totalScore: + return _sortDesc( + players.map((p) => (p, _totalScore(p, matches) as num)).toList(), + ); + + case StatisticType.averageScore: + return _sortDesc( + players.map((p) { + final scores = _scoresOf(p, matches); + final avg = scores.isEmpty + ? 0.0 + : double.parse( + (scores.reduce((a, b) => a + b) / scores.length) + .toStringAsFixed(2), + ); + return (p, avg as num); + }).toList(), + ); + + case StatisticType.bestScore: + return _sortDesc( + players.map((p) { + final scores = _scoresOf(p, matches); + final best = scores.isEmpty ? 0 : scores.reduce(max); + return (p, best as num); + }).toList(), + ); + + case StatisticType.worstScore: + // Ascending here is more meaningful for "worst", but keep the + // existing tile semantics (largest bar = top entry) by sorting + // descending on the inverse — i.e. show smallest score on top. + final entries = players.map((p) { + final scores = _scoresOf(p, matches); + final worst = scores.isEmpty ? 0 : scores.reduce(min); + return (p, worst as num); + }).toList(); + entries.sort((a, b) => a.$2.compareTo(b.$2)); + return entries; + + case StatisticType.winrate: + return _sortDesc( + players.map((p) { + final played = _matchesPlayed(p, matches); + final wins = _wins(p, matches); + final rate = played == 0 + ? 0.0 + : double.parse((wins / played).toStringAsFixed(2)); + return (p, rate as num); + }).toList(), + ); + } +} + +/* Helper functions for different statistic types */ + +/// Detemerines how many matches the player has played in the given list of matches. +int _matchesPlayed(Player p, List matches) => + matches.where((m) => m.players.any((mp) => mp.id == p.id)).length; + +/// Determines how many matches the player has won in the given list of matches. +int _wins(Player p, List matches) => + matches.where((m) => m.mvp.any((mp) => mp.id == p.id)).length; + +/// Determines the total score of the player in the given list of matches. +int _totalScore(Player p, List matches) { + var total = 0; + for (final m in matches) { + final s = m.scores[p.id]; + if (s != null) total += s.score; + } + return total; +} + +/// Returns a list of all scores the player has achieved in the given list of matches. +List _scoresOf(Player p, List matches) => [ + for (final m in matches) + if (m.scores[p.id] != null) m.scores[p.id]!.score, +]; + +/// Returns the list of entries sorted descending by the statistic value. +List<(Player, num)> _sortDesc(List<(Player, num)> entries) { + entries.sort((a, b) => b.$2.compareTo(a.$2)); + return entries; +} + +/* Icon and color */ + +/// Returns the icon for the given statistic type. +IconData _getStatisticIcon({required StatisticType type}) { + switch (type) { + case StatisticType.totalMatches: + return Icons.casino; + case StatisticType.totalWins: + return Icons.emoji_events; + case StatisticType.totalLosses: + return Icons.sentiment_dissatisfied; + case StatisticType.totalScore: + return Icons.scoreboard; + case StatisticType.averageScore: + return Icons.show_chart; + case StatisticType.bestScore: + return Icons.trending_up; + case StatisticType.worstScore: + return Icons.trending_down; + case StatisticType.winrate: + return Icons.percent; + } +} + +/// The bar colors for the statistic tiles +const List _palette = [ + Color(0xFF4CAF50), // green + Color(0xFF2196F3), // blue + Color(0xFFFF9800), // orange + Color(0xFFE91E63), // pink + Color(0xFF9C27B0), // purple + Color(0xFF00BCD4), // cyan + Color(0xFFFFC107), // amber + Color(0xFF3F51B5), // indigo + Color(0xFF8BC34A), // light green + Color(0xFFFF5722), // deep orange +]; + +/// Returns a color from the palette based on the statistic's ID as random seed. +Color _getStatisticColor(Statistic stat) { + final seed = stat.id.hashCode; + return _palette[seed.abs() % _palette.length]; +} + +/* Skeleton data */ + +/// A placeholder tile with mock data for the loading state. +Widget buildSkeletonStatisticTile({required BuildContext context}) { + final count = 4 + Random().nextInt(5); // 4..8 + final values = <(Player, num)>[ + for (var i = 0; i < count; i++) + (Player(name: 'Player ${i + 1}'), count - i), + ]; + + return StatisticsTile( + icon: Icons.bar_chart, + title: 'Skeleton title', + width: MediaQuery.sizeOf(context).width * 0.95, + values: values, + itemCount: count, + barColor: _palette[Random().nextInt(_palette.length)], + statistic: Statistic( + type: StatisticType.totalMatches, + scopes: [StatisticScope.allPlayers], + timeframe: Timeframe.last7Days, + ), + ); +} diff --git a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart index f551a8b..8c3ac16 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -8,9 +8,9 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; -import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; class StatisticsView extends StatefulWidget { /// A view that displays player statistics @@ -21,36 +21,19 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { - int matchCount = 0; - int groupCount = 0; - - List<(Player, int)> winCounts = List.filled(6, ( - Player(name: 'Skeleton Player'), - 1, - )); - List<(Player, int)> matchCounts = List.filled(6, ( - Player(name: 'Skeleton Player'), - 1, - )); - List<(Player, double)> winRates = List.filled(6, ( - Player(name: 'Skeleton Player'), - 1, - )); bool isLoading = true; - List statisticTiles = List.generate( - 4, - (_) => const InfoTile( - icon: Icons.sports_score, - title: 'Skeleton Statistic', - width: double.infinity, - content: Text('Skeleton content'), - ), - ); + List _allMatches = const []; + List _allPlayers = const []; + List statisticTiles = []; @override void initState() { super.initState(); - getStatisticTiles(context); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + getStatisticTiles(context); + }); } @override @@ -66,13 +49,14 @@ class _StatisticsViewState extends State { child: AppSkeleton( enabled: isLoading, fixLayoutBuilder: true, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [...statisticTiles], - ), + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...statisticTiles, + SizedBox(height: MediaQuery.paddingOf(context).bottom + 80), + ], ), ), ), @@ -86,17 +70,16 @@ class _StatisticsViewState extends State { context, adaptivePageRoute( builder: (context) => CreateStatisticView( - onStatisticCreated: loadStatisticData, + onStatisticCreated: () => getStatisticTiles(context), ), ), ); - final newTile = InfoTile( - icon: Icons.sports_score, - title: newStatistic.type.name, - width: MediaQuery.sizeOf(context).width * 0.95, - content: Text( - '${newStatistic.id}\n${newStatistic.scopes}\n${newStatistic.type}\n${newStatistic.timeframe}\n${newStatistic.selectedGroups}\n${newStatistic.selectedGames}\n', - ), + if (!context.mounted) return; + final newTile = buildStatisticTile( + statistic: newStatistic, + matches: _allMatches, + players: _allPlayers, + context: context, ); setState(() { @@ -111,205 +94,47 @@ class _StatisticsViewState extends State { ); } - /// Loads matches and players from the database - /// and calculates statistics for each player - void loadStatisticData() { + Future getStatisticTiles(BuildContext context) async { + setState(() { + isLoading = true; + statisticTiles = List.generate( + 4, + (index) => Column( + children: [ + buildSkeletonStatisticTile(context: context), + const SizedBox(height: 12), + ], + ), + ); + }); + final db = Provider.of(context, listen: false); - Future.wait([ + final results = await Future.wait([ + db.statisticDao.getAllStatistics(), db.matchDao.getAllMatches(), db.playerDao.getAllPlayers(), - db.matchDao.getMatchCount(), - db.groupDao.getGroupCount(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), - ]).then((results) async { - if (!mounted) return; + ]); - final matches = results[0] as List; - final players = results[1] as List; - matchCount = results[2] as int; - groupCount = results[3] as int; + if (!mounted) return; - winCounts = _calculateWinsForAllPlayers( - matches: matches, - players: players, - context: context, - ); - matchCounts = _calculateMatchAmountsForAllPlayers( - matches: matches, - players: players, - context: context, - ); - winRates = computeWinRatePercent( - winCounts: winCounts, - matchCounts: matchCounts, - ); - - setState(() { - isLoading = false; - }); - }); - } - - Future getStatisticTiles(BuildContext context) async { - isLoading = true; - final db = Provider.of(context, listen: false); - final statistics = await db.statisticDao.getAllStatistics(); + final statistics = results[0] as List; + _allMatches = results[1] as List; + _allPlayers = results[2] as List; setState(() { - statisticTiles = []; - for (var statistic in statistics) { - statisticTiles.add( - InfoTile( - icon: Icons.sports_score, - title: statistic.type.name, - width: MediaQuery.sizeOf(context).width * 0.95, - content: Text(statistic.toString()), + statisticTiles = [ + for (final statistic in statistics) ...[ + buildStatisticTile( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + context: context, ), - ); - statisticTiles.add( - SizedBox(height: MediaQuery.sizeOf(context).height * 0.02), - ); - } + ], + ]; + isLoading = false; }); - isLoading = false; - } - - /// Calculates the number of wins for each player - /// and returns a sorted list of tuples (playerName, winCount) - List<(Player, int)> _calculateWinsForAllPlayers({ - required List matches, - required List players, - required BuildContext context, - }) { - List<(Player, int)> winCounts = []; - final loc = AppLocalizations.of(context); - - // Getting the winners - for (var match in matches) { - final mvps = match.mvp; - for (var winner in mvps) { - final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id); - // -1 means winner not found in winCounts - 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.id == player.id); - // -1 means player not found in winCounts - if (index == -1) { - winCounts.add((player, 0)); - } - } - - // Replace player IDs with names - for (int i = 0; i < winCounts.length; i++) { - final playerId = winCounts[i].$1.id; - final player = players.firstWhere( - (p) => p.id == playerId, - orElse: () => Player(id: playerId, name: loc.not_available), - ); - winCounts[i] = (player, winCounts[i].$2); - } - - winCounts.sort((a, b) => b.$2.compareTo(a.$2)); - - return winCounts; - } - - /// Calculates the number of matches played for each player - /// and returns a sorted list of tuples (playerName, matchCount) - List<(Player, int)> _calculateMatchAmountsForAllPlayers({ - required List matches, - required List players, - required BuildContext context, - }) { - List<(Player, int)> matchCounts = []; - final loc = AppLocalizations.of(context); - - // Counting matches for each player - for (var match in matches) { - for (Player player in match.players) { - // Check if the player is already in matchCounts - final index = matchCounts.indexWhere( - (entry) => entry.$1.id == player.id, - ); - - // -1 -> not found - if (index == -1) { - // Add new entry - matchCounts.add((player, 1)); - } else { - // Update existing entry - final currentMatchAmount = matchCounts[index].$2; - matchCounts[index] = (player, currentMatchAmount + 1); - } - } - } - - // Adding all players with zero matches - for (var player in players) { - final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id); - // -1 means player not found in matchCounts - if (index == -1) { - matchCounts.add((player, 0)); - } - } - - // Replace player IDs with names - for (int i = 0; i < matchCounts.length; i++) { - final playerId = matchCounts[i].$1.id; - final player = players.firstWhere( - (p) => p.id == playerId, - orElse: () => Player(id: playerId, name: loc.not_available), - ); - matchCounts[i] = (player, matchCounts[i].$2); - } - - matchCounts.sort((a, b) => b.$2.compareTo(a.$2)); - - return matchCounts; - } - - List<(Player, double)> computeWinRatePercent({ - required List<(Player, int)> winCounts, - required List<(Player, int)> matchCounts, - }) { - final Map winsMap = {for (var e in winCounts) e.$1: e.$2}; - final Map matchesMap = {for (var e in matchCounts) e.$1: e.$2}; - - // Get all unique player names - final player = {...matchesMap.keys}; - - // Calculate win rates - final result = player.map((name) { - final int w = winsMap[name] ?? 0; - final int m = matchesMap[name] ?? 0; - // Calculate percentage and round to 2 decimal places - // Avoid division by zero - final double percent = (m > 0) - ? double.parse(((w / m)).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; } } diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index ea9cb49..b740eb5 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -1,9 +1,13 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; @@ -23,6 +27,7 @@ class StatisticsTile extends StatelessWidget { required this.values, required this.itemCount, required this.barColor, + required this.statistic, }); /// The icon displayed next to the title. @@ -43,6 +48,8 @@ class StatisticsTile extends StatelessWidget { /// The color of the bars representing the values. final Color barColor; + final Statistic statistic; + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); @@ -52,7 +59,7 @@ class StatisticsTile extends StatelessWidget { title: title, icon: icon, content: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Visibility( visible: values.isNotEmpty, replacement: Center( @@ -63,80 +70,146 @@ class StatisticsTile extends StatelessWidget { builder: (context, constraints) { final maxBarWidth = constraints.maxWidth * 0.65; return Column( - children: List.generate(min(values.length, itemCount), (index) { - /// The maximum wins among all players - final maxMatches = values.isNotEmpty ? values[0].$2 : 0; + children: [ + // Bar chart + ...List.generate(min(values.length, itemCount), (index) { + /// The maximum wins among all players + final maxMatches = values.isNotEmpty ? values[0].$2 : 0; - /// Fraction of wins - final double fraction = (maxMatches > 0) - ? (values[index].$2 / maxMatches) - : 0.0; + /// Fraction of wins + final double fraction = (maxMatches > 0) + ? (values[index].$2 / maxMatches) + : 0.0; - /// Calculated width for current the bar - final double barWidth = maxBarWidth * fraction; + /// 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: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: values[index].$1.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: getNameCountText(values[index].$1), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor.withAlpha( - 150, - ), - ), - ), - ], + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + // Bar + Container( + height: 24, + width: barWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: barColor, ), ), - ), - ], - ), - const Spacer(), - Center( - child: Text( - values[index].$2 <= 1 && values[index].$2 is double - ? values[index].$2.toStringAsFixed(2) - : values[index].$2.toString(), - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + + // Player + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: values[index].$1.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: getNameCountText( + values[index].$1, + ), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor + .withAlpha(150), + ), + ), + ], + ), + ), + ), + ], + ), + const Spacer(), + + // Value + Center( + child: Text( + values[index].$2 <= 1 && + values[index].$2 is double + ? values[index].$2.toStringAsFixed(2) + : values[index].$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ), - ), - ], + ], + ), + ); + }), + + // Group & Game info + if (statistic.selectedGames != null || + statistic.selectedGroups != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // Game + if (statistic.selectedGames != null && + statistic.selectedGames!.isNotEmpty) + Row( + spacing: 8, + children: [ + const Icon( + RpgAwesome.clovers_card, + color: CustomTheme.hintColor, + size: 20, + ), + Text( + getGameText(statistic.selectedGames!), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.hintColor, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + + // Group + if (statistic.selectedGroups != null && + statistic.selectedGroups!.isNotEmpty) + Row( + spacing: 8, + children: [ + const Icon( + Icons.groups, + color: CustomTheme.hintColor, + ), + Text( + getGroupText(statistic.selectedGroups!), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.hintColor, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), ), - ); - }), + ], ); }, ), @@ -144,4 +217,20 @@ class StatisticsTile extends StatelessWidget { ), ); } + + String getGroupText(List groups) { + var text = groups[0].name; + if (groups.length > 1) { + return '$text + ${groups.length - 1}'; + } + return text; + } + + String getGameText(List games) { + var text = games[0].name; + if (games.length > 1) { + return '$text + ${games.length - 1}'; + } + return text; + } } From 18a5dcfdd53d0484e7d6dcdcbb7bc22d936e3df1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 17:03:43 +0200 Subject: [PATCH 20/34] Changed colors --- lib/core/common.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 312e3fa..cf77e35 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -63,9 +63,9 @@ Color getColorFromGameColor(GameColor color) { case GameColor.orange: return const Color(0xFFef681f); case GameColor.pink: - return Colors.pink; + return const Color(0xFFE91E63); case GameColor.teal: - return Colors.teal; + return const Color(0xFF00BCD4); } } From 5a2cc790ddd86c8d5ef4c7f707d549995048478d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 17:03:58 +0200 Subject: [PATCH 21/34] Renamed GameColor -> AppColor --- lib/core/common.dart | 40 +++++++++---------- lib/core/enums.dart | 2 +- lib/data/dao/game_dao.dart | 6 +-- lib/data/dao/statistic_game_dao.dart | 2 +- lib/data/models/game.dart | 8 ++-- lib/data/models/match.dart | 2 +- .../create_match/create_game_view.dart | 10 ++--- .../main_menu/match_view/match_view.dart | 2 +- lib/presentation/widgets/game_label.dart | 2 +- lib/presentation/widgets/tiles/game_tile.dart | 2 +- lib/services/data_transfer_service.dart | 2 +- test/db_tests/aggregates/match_test.dart | 2 +- test/db_tests/aggregates/team_test.dart | 2 +- test/db_tests/entities/game_test.dart | 16 ++++---- .../relationships/player_match_test.dart | 2 +- test/db_tests/statistics/statistic_test.dart | 2 +- test/db_tests/values/score_entry_test.dart | 2 +- test/services/data_transfer_service_test.dart | 14 +++---- 18 files changed, 59 insertions(+), 59 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index cf77e35..6f1db7f 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -24,47 +24,47 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } } -/// Translates a [GameColor] enum value to its corresponding localized string. -String translateGameColorToString(GameColor color, BuildContext context) { +/// Translates a [AppColor] enum value to its corresponding localized string. +String translateGameColorToString(AppColor color, BuildContext context) { final loc = AppLocalizations.of(context); switch (color) { - case GameColor.red: + case AppColor.red: return loc.color_red; - case GameColor.blue: + case AppColor.blue: return loc.color_blue; - case GameColor.green: + case AppColor.green: return loc.color_green; - case GameColor.yellow: + case AppColor.yellow: return loc.color_yellow; - case GameColor.purple: + case AppColor.purple: return loc.color_purple; - case GameColor.orange: + case AppColor.orange: return loc.color_orange; - case GameColor.pink: + case AppColor.pink: return loc.color_pink; - case GameColor.teal: + case AppColor.teal: return loc.color_teal; } } -/// Returns the [Color] object corresponding to a [GameColor] enum value. -Color getColorFromGameColor(GameColor color) { +/// Returns the [Color] object corresponding to a [AppColor] enum value. +Color getColorFromGameColor(AppColor color) { switch (color) { - case GameColor.red: + case AppColor.red: return Colors.red; - case GameColor.blue: + case AppColor.blue: return Colors.blue; - case GameColor.green: + case AppColor.green: return Colors.green; - case GameColor.yellow: + case AppColor.yellow: return const Color(0xFFF7CA28); - case GameColor.purple: + case AppColor.purple: return Colors.purple; - case GameColor.orange: + case AppColor.orange: return const Color(0xFFef681f); - case GameColor.pink: + case AppColor.pink: return const Color(0xFFE91E63); - case GameColor.teal: + case AppColor.teal: return const Color(0xFF00BCD4); } } diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 5d46a3f..073fd7a 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -43,7 +43,7 @@ enum Ruleset { } /// Different colors for highlighting games -enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } +enum AppColor { red, orange, yellow, green, teal, blue, purple, pink } enum StatisticType { totalMatches, diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index a4c2300..1adfc9e 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -92,7 +92,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { name: row.name, ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), description: row.description, - color: GameColor.values.firstWhere((e) => e.name == row.color), + color: AppColor.values.firstWhere((e) => e.name == row.color), icon: row.icon, createdAt: row.createdAt, ), @@ -109,7 +109,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { name: result.name, ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), description: result.description, - color: GameColor.values.firstWhere((e) => e.name == result.color), + color: AppColor.values.firstWhere((e) => e.name == result.color), icon: result.icon, createdAt: result.createdAt, ); @@ -156,7 +156,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Updates the color of the game with the given [gameId]. Future updateGameColor({ required String gameId, - required GameColor color, + required AppColor color, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( diff --git a/lib/data/dao/statistic_game_dao.dart b/lib/data/dao/statistic_game_dao.dart index ea7260f..4ee5b84 100644 --- a/lib/data/dao/statistic_game_dao.dart +++ b/lib/data/dao/statistic_game_dao.dart @@ -25,7 +25,7 @@ class StatisticGameDao extends DatabaseAccessor name: result.name, ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), description: result.description, - color: GameColor.values.firstWhere((e) => e.name == result.color), + color: AppColor.values.firstWhere((e) => e.name == result.color), icon: result.icon, createdAt: result.createdAt, ), diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 89bbd30..ec69204 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -8,13 +8,13 @@ class Game { final String name; final Ruleset ruleset; final String description; - final GameColor color; + final AppColor color; final String icon; Game({ required this.name, required this.ruleset, - this.color = GameColor.orange, + this.color = AppColor.orange, this.description = '', this.icon = '', String? id, @@ -33,7 +33,7 @@ class Game { String? name, Ruleset? ruleset, String? description, - GameColor? color, + AppColor? color, String? icon, }) { return Game( @@ -73,7 +73,7 @@ class Game { orElse: () => Ruleset.singleWinner, ), description = json['description'], - color = GameColor.values.firstWhere((e) => e.name == json['color']), + color = AppColor.values.firstWhere((e) => e.name == json['color']), icon = json['icon']; Map toJson() => { diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 2c43fe3..601a01c 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -107,7 +107,7 @@ class Match { name: '', ruleset: Ruleset.singleWinner, description: '', - color: GameColor.blue, + color: AppColor.blue, icon: '', ), group = null, diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index a5729be..29dc7d1 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -49,10 +49,10 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - late List<(GameColor, String)> _colors; + late List<(AppColor, String)> _colors; Ruleset? selectedRuleset = Ruleset.singleWinner; - GameColor? selectedColor = GameColor.orange; + AppColor? selectedColor = AppColor.orange; /// Controller for the game name input field. final _gameNameController = TextEditingController(); @@ -87,10 +87,10 @@ class _CreateGameViewState extends State { ), ); _colors = List.generate( - GameColor.values.length, + AppColor.values.length, (index) => ( - GameColor.values[index], - translateGameColorToString(GameColor.values[index], context), + AppColor.values[index], + translateGameColorToString(AppColor.values[index], context), ), ); diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 83ff069..3c01f45 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -39,7 +39,7 @@ class _MatchViewState extends State { game: Game( name: 'Game name', ruleset: Ruleset.singleWinner, - color: GameColor.blue, + color: AppColor.blue, icon: '', ), group: Group( diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart index 553e637..3eae9b1 100644 --- a/lib/presentation/widgets/game_label.dart +++ b/lib/presentation/widgets/game_label.dart @@ -12,7 +12,7 @@ class GameLabel extends StatelessWidget { final String title; final String description; - final GameColor color; + final AppColor color; @override Widget build(BuildContext context) { diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index ee5acf0..11d96d8 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -51,7 +51,7 @@ class GameTile extends StatelessWidget { ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) : Colors.white; - final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); + final gameColor = badgeColor ?? getColorFromGameColor(AppColor.orange); return GestureDetector( onTap: () async { diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 29199f8..0f2f8fa 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -278,7 +278,7 @@ class DataTransferService { name: 'Unknown', ruleset: Ruleset.singleWinner, description: '', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); } diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 00e6e46..1144a73 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -56,7 +56,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index fefdcc5..381d22b 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -49,7 +49,7 @@ void main() { testGame = Game( name: 'Test Game', ruleset: Ruleset.highestScore, - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index 778d43b..f7e7dcd 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -28,7 +28,7 @@ void main() { name: 'Chess', ruleset: Ruleset.singleWinner, description: 'A classic strategy game', - color: GameColor.blue, + color: AppColor.blue, icon: 'chess_icon', ); testGame2 = Game( @@ -36,7 +36,7 @@ void main() { name: 'Poker', ruleset: Ruleset.multipleWinners, description: 'Card game with multiple winners', - color: GameColor.red, + color: AppColor.red, icon: 'poker_icon', ); testGame3 = Game( @@ -44,7 +44,7 @@ void main() { name: 'Monopoly', ruleset: Ruleset.highestScore, description: 'A board game about real estate', - color: GameColor.orange, + color: AppColor.orange, icon: '', ); }); @@ -124,7 +124,7 @@ void main() { name: 'Game\'s & "Special" ', ruleset: Ruleset.multipleWinners, description: 'Description with émojis 🎮🎲', - color: GameColor.purple, + color: AppColor.purple, icon: '', ); await database.gameDao.addGame(game: specialGame); @@ -280,19 +280,19 @@ void main() { await database.gameDao.updateGameColor( gameId: testGame1.id, - color: GameColor.green, + color: AppColor.green, ); final updatedGame = await database.gameDao.getGameById( gameId: testGame1.id, ); - expect(updatedGame.color, GameColor.green); + expect(updatedGame.color, AppColor.green); }); test('updateGameColor() does nothing for non-existent game', () async { final updated = await database.gameDao.updateGameColor( gameId: 'non-existent-id', - color: GameColor.green, + color: AppColor.green, ); expect(updated, isFalse); @@ -336,7 +336,7 @@ void main() { name: newName, ); - const newGameColor = GameColor.teal; + const newGameColor = AppColor.teal; await database.gameDao.updateGameColor( gameId: testGame1.id, color: newGameColor, diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 6d879c3..fa7ec21 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -42,7 +42,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/statistics/statistic_test.dart b/test/db_tests/statistics/statistic_test.dart index b969b4c..cd08615 100644 --- a/test/db_tests/statistics/statistic_test.dart +++ b/test/db_tests/statistics/statistic_test.dart @@ -59,7 +59,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/values/score_entry_test.dart b/test/db_tests/values/score_entry_test.dart index f6cc292..593d194 100644 --- a/test/db_tests/values/score_entry_test.dart +++ b/test/db_tests/values/score_entry_test.dart @@ -40,7 +40,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 586138a..94ed977 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -45,7 +45,7 @@ void main() { name: 'Chess', ruleset: Ruleset.singleWinner, description: 'Strategic board game', - color: GameColor.blue, + color: AppColor.blue, icon: 'chess_icon', ); @@ -448,19 +448,19 @@ void main() { Game( name: 'Red Game', ruleset: Ruleset.singleWinner, - color: GameColor.red, + color: AppColor.red, icon: 'icon', ), Game( name: 'Blue Game', ruleset: Ruleset.singleWinner, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), Game( name: 'Green Game', ruleset: Ruleset.singleWinner, - color: GameColor.green, + color: AppColor.green, icon: 'icon', ), ]; @@ -484,19 +484,19 @@ void main() { Game( name: 'Highest Score Game', ruleset: Ruleset.highestScore, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), Game( name: 'Lowest Score Game', ruleset: Ruleset.lowestScore, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), Game( name: 'Single Winner', ruleset: Ruleset.singleWinner, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), ]; From d82206319a69e3cd19a44499a16375394ffc37be Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 17:04:14 +0200 Subject: [PATCH 22/34] Renamed GameColor -> AppColor --- lib/core/common.dart | 2 +- .../main_menu/match_view/create_match/create_game_view.dart | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 6f1db7f..31d039a 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -25,7 +25,7 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } /// Translates a [AppColor] enum value to its corresponding localized string. -String translateGameColorToString(AppColor color, BuildContext context) { +String translateAppColorToString(AppColor color, BuildContext context) { final loc = AppLocalizations.of(context); switch (color) { case AppColor.red: diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 29dc7d1..06f15db 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -90,7 +90,7 @@ class _CreateGameViewState extends State { AppColor.values.length, (index) => ( AppColor.values[index], - translateGameColorToString(AppColor.values[index], context), + translateAppColorToString(AppColor.values[index], context), ), ); @@ -507,7 +507,7 @@ class _CreateGameViewState extends State { ), Padding( padding: const EdgeInsets.only(right: 5), - child: Text(translateGameColorToString(selectedColor!, context)), + child: Text(translateAppColorToString(selectedColor!, context)), ), Transform.rotate( angle: pi / 2, From 398c7a4168f7d5eba5c2c631e2e6b7d6bda70d29 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 17:07:09 +0200 Subject: [PATCH 23/34] Refactoring --- lib/core/common.dart | 2 +- .../create_match/choose_game_view.dart | 2 +- .../create_match/create_game_view.dart | 4 ++-- .../statistic_tile_factory.dart | 24 ++++++------------- lib/presentation/widgets/game_label.dart | 2 +- lib/presentation/widgets/tiles/game_tile.dart | 2 +- 6 files changed, 13 insertions(+), 23 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 31d039a..df88ea3 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -48,7 +48,7 @@ String translateAppColorToString(AppColor color, BuildContext context) { } /// Returns the [Color] object corresponding to a [AppColor] enum value. -Color getColorFromGameColor(AppColor color) { +Color getColorFromAppColor(AppColor color) { switch (color) { case AppColor.red: return Colors.red; diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 3c51cab..4d085d7 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -164,7 +164,7 @@ class _ChooseGameViewState extends State { game.ruleset, context, ), - badgeColor: getColorFromGameColor(game.color), + badgeColor: getColorFromAppColor(game.color), isHighlighted: selectedGameId == game.id, onTap: () async { setState(() { diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 06f15db..0671055 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -467,7 +467,7 @@ class _CreateGameViewState extends State { height: 16, margin: const EdgeInsets.only(left: 12), decoration: BoxDecoration( - color: getColorFromGameColor( + color: getColorFromAppColor( _colors[index].$1, ), shape: BoxShape.circle, @@ -501,7 +501,7 @@ class _CreateGameViewState extends State { width: 16, height: 16, decoration: BoxDecoration( - color: getColorFromGameColor(selectedColor!), + color: getColorFromAppColor(selectedColor!), shape: BoxShape.circle, ), ), diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart index c482a7d..8461f6d 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; @@ -9,8 +10,11 @@ import 'package:tallee/presentation/views/main_menu/statistics_view/create_stati show translateStatisticTypeToString; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; -/// Build the [StatisticsTile] for a given [Statistic]. +List _colorPalette = AppColor.values + .map((c) => getColorFromAppColor(c)) + .toList(); +/// Build the [StatisticsTile] for a given [Statistic]. Widget buildStatisticTile({ required Statistic statistic, required List matches, @@ -272,24 +276,10 @@ IconData _getStatisticIcon({required StatisticType type}) { } } -/// The bar colors for the statistic tiles -const List _palette = [ - Color(0xFF4CAF50), // green - Color(0xFF2196F3), // blue - Color(0xFFFF9800), // orange - Color(0xFFE91E63), // pink - Color(0xFF9C27B0), // purple - Color(0xFF00BCD4), // cyan - Color(0xFFFFC107), // amber - Color(0xFF3F51B5), // indigo - Color(0xFF8BC34A), // light green - Color(0xFFFF5722), // deep orange -]; - /// Returns a color from the palette based on the statistic's ID as random seed. Color _getStatisticColor(Statistic stat) { final seed = stat.id.hashCode; - return _palette[seed.abs() % _palette.length]; + return _colorPalette[seed.abs() % _colorPalette.length]; } /* Skeleton data */ @@ -308,7 +298,7 @@ Widget buildSkeletonStatisticTile({required BuildContext context}) { width: MediaQuery.sizeOf(context).width * 0.95, values: values, itemCount: count, - barColor: _palette[Random().nextInt(_palette.length)], + barColor: _colorPalette[Random().nextInt(_colorPalette.length)], statistic: Statistic( type: StatisticType.totalMatches, scopes: [StatisticScope.allPlayers], diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart index 3eae9b1..dd179d6 100644 --- a/lib/presentation/widgets/game_label.dart +++ b/lib/presentation/widgets/game_label.dart @@ -16,7 +16,7 @@ class GameLabel extends StatelessWidget { @override Widget build(BuildContext context) { - final backgroundColor = getColorFromGameColor(color); + final backgroundColor = getColorFromAppColor(color); final fontColor = backgroundColor.computeLuminance() > 0.5 ? Colors.black : Colors.white; diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index 11d96d8..4fb12d1 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -51,7 +51,7 @@ class GameTile extends StatelessWidget { ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) : Colors.white; - final gameColor = badgeColor ?? getColorFromGameColor(AppColor.orange); + final gameColor = badgeColor ?? getColorFromAppColor(AppColor.orange); return GestureDetector( onTap: () async { From ffd52055fabc8dc1acb52213d4bea2888992e34d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 17:28:29 +0200 Subject: [PATCH 24/34] fixed bar length --- .../widgets/tiles/statistics_tile.dart | 144 +++++++++++------- 1 file changed, 90 insertions(+), 54 deletions(-) diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index b740eb5..0abe3bb 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; @@ -62,86 +63,121 @@ class StatisticsTile extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Visibility( visible: values.isNotEmpty, + + // No data avaiable message replacement: Center( heightFactor: 4, child: Text(loc.no_data_available), ), + + // Bar chart child: LayoutBuilder( builder: (context, constraints) { - final maxBarWidth = constraints.maxWidth * 0.65; + final maxBarWidth = constraints.maxWidth * 0.8; + final displayCount = min(values.length, itemCount); + final displayValues = values.take(displayCount).toList(); + final maxVal = displayValues.isNotEmpty + ? displayValues.fold( + 0, + (currentMax, entry) => + entry.$2 > currentMax ? entry.$2 : currentMax, + ) + : 0; + return Column( children: [ - // Bar chart - ...List.generate(min(values.length, itemCount), (index) { - /// The maximum wins among all players - final maxMatches = values.isNotEmpty ? values[0].$2 : 0; - + // Bars + ...List.generate(displayCount, (index) { /// Fraction of wins - final double fraction = (maxMatches > 0) - ? (values[index].$2 / maxMatches) + final double fraction = (maxVal > 0) + ? (displayValues[index].$2 / maxVal) : 0.0; /// Calculated width for current the bar - final double barWidth = maxBarWidth * fraction; + final double barWidth = (maxBarWidth * fraction).clamp( + 0.0, + maxBarWidth, + ); return Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, children: [ - Stack( - children: [ - // Bar - Container( - height: 24, - width: barWidth, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: barColor, - ), - ), - - // Player - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: values[index].$1.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: getNameCountText( - values[index].$1, - ), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor - .withAlpha(150), - ), - ), - ], + SizedBox( + width: maxBarWidth, + child: Stack( + clipBehavior: Clip.hardEdge, + children: [ + // Bar + Container( + height: 24, + width: barWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: barColor, ), ), - ), - ], + + // Player + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: RichText( + maxLines: 1, + softWrap: false, + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: displayValues[index].$1.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: + barColor == + getColorFromAppColor( + AppColor.yellow, + ) + ? const Color(0xFF101010) + : CustomTheme.textColor, + ), + ), + TextSpan( + text: getNameCountText( + displayValues[index].$1, + ), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: + (barColor == + getColorFromAppColor( + AppColor.yellow, + ) + ? const Color( + 0xFF101010, + ) + : CustomTheme.textColor) + .withAlpha(150), + ), + ), + ], + ), + ), + ), + ], + ), ), const Spacer(), // Value Center( child: Text( - values[index].$2 <= 1 && - values[index].$2 is double - ? values[index].$2.toStringAsFixed(2) - : values[index].$2.toString(), + displayValues[index].$2 <= 1 && + displayValues[index].$2 is double + ? displayValues[index].$2.toStringAsFixed(2) + : displayValues[index].$2.toString(), textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, From f65ea09cbe1c0ad6cfd6f6a1148665091f1ebb9c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 17:33:41 +0200 Subject: [PATCH 25/34] Added spacing --- .../widgets/tiles/statistics_tile.dart | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 0abe3bb..ad850dd 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -196,11 +196,10 @@ class StatisticsTile extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 8.0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisAlignment: MainAxisAlignment.start, children: [ // Game - if (statistic.selectedGames != null && - statistic.selectedGames!.isNotEmpty) + if (hasGroup) Row( spacing: 8, children: [ @@ -220,10 +219,10 @@ class StatisticsTile extends StatelessWidget { ), ], ), + if (hasGroup && hasGame) const SizedBox(width: 20), // Group - if (statistic.selectedGroups != null && - statistic.selectedGroups!.isNotEmpty) + if (hasGroup) Row( spacing: 8, children: [ @@ -269,4 +268,10 @@ class StatisticsTile extends StatelessWidget { } return text; } + + bool get hasGroup => + statistic.selectedGroups != null && statistic.selectedGroups!.isNotEmpty; + + bool get hasGame => + statistic.selectedGames != null && statistic.selectedGames!.isNotEmpty; } From 428f9670103b47e40c969c7bd40b076d6a1158a3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 23:09:08 +0200 Subject: [PATCH 26/34] feat: displayCount --- lib/data/dao/statistic_dao.dart | 22 +++++- lib/data/db/database.g.dart | 78 ++++++++++++++++++- lib/data/db/tables/statistic_table.dart | 1 + lib/data/models/statistic.dart | 4 +- .../statistic_tile_factory.dart | 3 - .../widgets/tiles/statistics_tile.dart | 6 +- pubspec.yaml | 2 +- 7 files changed, 98 insertions(+), 18 deletions(-) diff --git a/lib/data/dao/statistic_dao.dart b/lib/data/dao/statistic_dao.dart index 39904fc..7bdebde 100644 --- a/lib/data/dao/statistic_dao.dart +++ b/lib/data/dao/statistic_dao.dart @@ -20,6 +20,7 @@ class StatisticDao extends DatabaseAccessor id: statistic.id, type: statistic.type.name, timeframe: Value(statistic.timeframe?.name), + displayCount: Value(statistic.displayCount), ), mode: InsertMode.insertOrReplace, ); @@ -59,12 +60,13 @@ class StatisticDao extends DatabaseAccessor return Statistic( type: StatisticType.values.firstWhere((type) => type.name == row.type), scopes: scopes, - id: row.id, timeframe: Timeframe.values.firstWhereOrNull( (t) => t.name == row.timeframe, ), selectedGroups: groups, selectedGames: games, + displayCount: row.displayCount, + id: row.id, ); } return null; @@ -73,9 +75,9 @@ class StatisticDao extends DatabaseAccessor /// Retrieves all statistics from the database, including their associated groups and games. Future> getAllStatistics() async { final query = select(statisticTable); - final rows = await query.get(); + final result = await query.get(); return Future.wait( - rows.map((row) async { + result.map((row) async { final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id); final games = await db.statisticGameDao.getGamesForStatistic(row.id); @@ -84,17 +86,29 @@ class StatisticDao extends DatabaseAccessor (type) => type.name == row.type, ), scopes: [], - id: row.id, timeframe: Timeframe.values.firstWhereOrNull( (t) => t.name == row.timeframe, ), selectedGroups: groups, selectedGames: games, + displayCount: row.displayCount, + id: row.id, ); }), ); } + /* Update */ + + Future updateDisplayCount(String statisticId, int displayCount) async { + final rowsUpdated = + await (update(statisticTable) + ..where((tbl) => tbl.id.equals(statisticId))) + .write(StatisticTableCompanion(displayCount: Value(displayCount))); + + return rowsUpdated > 0; + } + /* Delete */ Future deleteStatistic(String statisticId) async { diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 489b890..3a9d277 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -2767,8 +2767,20 @@ class $StatisticTableTable extends StatisticTable type: DriftSqlType.string, requiredDuringInsert: false, ); + static const VerificationMeta _displayCountMeta = const VerificationMeta( + 'displayCount', + ); @override - List get $columns => [id, type, timeframe]; + late final GeneratedColumn displayCount = GeneratedColumn( + 'display_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(5), + ); + @override + List get $columns => [id, type, timeframe, displayCount]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -2800,6 +2812,15 @@ class $StatisticTableTable extends StatisticTable timeframe.isAcceptableOrUnknown(data['timeframe']!, _timeframeMeta), ); } + if (data.containsKey('display_count')) { + context.handle( + _displayCountMeta, + displayCount.isAcceptableOrUnknown( + data['display_count']!, + _displayCountMeta, + ), + ); + } return context; } @@ -2821,6 +2842,10 @@ class $StatisticTableTable extends StatisticTable DriftSqlType.string, data['${effectivePrefix}timeframe'], ), + displayCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}display_count'], + )!, ); } @@ -2835,10 +2860,12 @@ class StatisticTableData extends DataClass final String id; final String type; final String? timeframe; + final int displayCount; const StatisticTableData({ required this.id, required this.type, this.timeframe, + required this.displayCount, }); @override Map toColumns(bool nullToAbsent) { @@ -2848,6 +2875,7 @@ class StatisticTableData extends DataClass if (!nullToAbsent || timeframe != null) { map['timeframe'] = Variable(timeframe); } + map['display_count'] = Variable(displayCount); return map; } @@ -2858,6 +2886,7 @@ class StatisticTableData extends DataClass timeframe: timeframe == null && nullToAbsent ? const Value.absent() : Value(timeframe), + displayCount: Value(displayCount), ); } @@ -2870,6 +2899,7 @@ class StatisticTableData extends DataClass id: serializer.fromJson(json['id']), type: serializer.fromJson(json['type']), timeframe: serializer.fromJson(json['timeframe']), + displayCount: serializer.fromJson(json['displayCount']), ); } @override @@ -2879,6 +2909,7 @@ class StatisticTableData extends DataClass 'id': serializer.toJson(id), 'type': serializer.toJson(type), 'timeframe': serializer.toJson(timeframe), + 'displayCount': serializer.toJson(displayCount), }; } @@ -2886,16 +2917,21 @@ class StatisticTableData extends DataClass String? id, String? type, Value timeframe = const Value.absent(), + int? displayCount, }) => StatisticTableData( id: id ?? this.id, type: type ?? this.type, timeframe: timeframe.present ? timeframe.value : this.timeframe, + displayCount: displayCount ?? this.displayCount, ); StatisticTableData copyWithCompanion(StatisticTableCompanion data) { return StatisticTableData( id: data.id.present ? data.id.value : this.id, type: data.type.present ? data.type.value : this.type, timeframe: data.timeframe.present ? data.timeframe.value : this.timeframe, + displayCount: data.displayCount.present + ? data.displayCount.value + : this.displayCount, ); } @@ -2904,37 +2940,42 @@ class StatisticTableData extends DataClass return (StringBuffer('StatisticTableData(') ..write('id: $id, ') ..write('type: $type, ') - ..write('timeframe: $timeframe') + ..write('timeframe: $timeframe, ') + ..write('displayCount: $displayCount') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, type, timeframe); + int get hashCode => Object.hash(id, type, timeframe, displayCount); @override bool operator ==(Object other) => identical(this, other) || (other is StatisticTableData && other.id == this.id && other.type == this.type && - other.timeframe == this.timeframe); + other.timeframe == this.timeframe && + other.displayCount == this.displayCount); } class StatisticTableCompanion extends UpdateCompanion { final Value id; final Value type; final Value timeframe; + final Value displayCount; final Value rowid; const StatisticTableCompanion({ this.id = const Value.absent(), this.type = const Value.absent(), this.timeframe = const Value.absent(), + this.displayCount = const Value.absent(), this.rowid = const Value.absent(), }); StatisticTableCompanion.insert({ required String id, required String type, this.timeframe = const Value.absent(), + this.displayCount = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), type = Value(type); @@ -2942,12 +2983,14 @@ class StatisticTableCompanion extends UpdateCompanion { Expression? id, Expression? type, Expression? timeframe, + Expression? displayCount, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (type != null) 'type': type, if (timeframe != null) 'timeframe': timeframe, + if (displayCount != null) 'display_count': displayCount, if (rowid != null) 'rowid': rowid, }); } @@ -2956,12 +2999,14 @@ class StatisticTableCompanion extends UpdateCompanion { Value? id, Value? type, Value? timeframe, + Value? displayCount, Value? rowid, }) { return StatisticTableCompanion( id: id ?? this.id, type: type ?? this.type, timeframe: timeframe ?? this.timeframe, + displayCount: displayCount ?? this.displayCount, rowid: rowid ?? this.rowid, ); } @@ -2978,6 +3023,9 @@ class StatisticTableCompanion extends UpdateCompanion { if (timeframe.present) { map['timeframe'] = Variable(timeframe.value); } + if (displayCount.present) { + map['display_count'] = Variable(displayCount.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2990,6 +3038,7 @@ class StatisticTableCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('type: $type, ') ..write('timeframe: $timeframe, ') + ..write('displayCount: $displayCount, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -7516,6 +7565,7 @@ typedef $$StatisticTableTableCreateCompanionBuilder = required String id, required String type, Value timeframe, + Value displayCount, Value rowid, }); typedef $$StatisticTableTableUpdateCompanionBuilder = @@ -7523,6 +7573,7 @@ typedef $$StatisticTableTableUpdateCompanionBuilder = Value id, Value type, Value timeframe, + Value displayCount, Value rowid, }); @@ -7645,6 +7696,11 @@ class $$StatisticTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get displayCount => $composableBuilder( + column: $table.displayCount, + builder: (column) => ColumnFilters(column), + ); + Expression statisticScopeTableRefs( Expression Function($$StatisticScopeTableTableFilterComposer f) f, ) { @@ -7744,6 +7800,11 @@ class $$StatisticTableTableOrderingComposer column: $table.timeframe, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get displayCount => $composableBuilder( + column: $table.displayCount, + builder: (column) => ColumnOrderings(column), + ); } class $$StatisticTableTableAnnotationComposer @@ -7764,6 +7825,11 @@ class $$StatisticTableTableAnnotationComposer GeneratedColumn get timeframe => $composableBuilder(column: $table.timeframe, builder: (column) => column); + GeneratedColumn get displayCount => $composableBuilder( + column: $table.displayCount, + builder: (column) => column, + ); + Expression statisticScopeTableRefs( Expression Function($$StatisticScopeTableTableAnnotationComposer a) f, ) { @@ -7880,11 +7946,13 @@ class $$StatisticTableTableTableManager Value id = const Value.absent(), Value type = const Value.absent(), Value timeframe = const Value.absent(), + Value displayCount = const Value.absent(), Value rowid = const Value.absent(), }) => StatisticTableCompanion( id: id, type: type, timeframe: timeframe, + displayCount: displayCount, rowid: rowid, ), createCompanionCallback: @@ -7892,11 +7960,13 @@ class $$StatisticTableTableTableManager required String id, required String type, Value timeframe = const Value.absent(), + Value displayCount = const Value.absent(), Value rowid = const Value.absent(), }) => StatisticTableCompanion.insert( id: id, type: type, timeframe: timeframe, + displayCount: displayCount, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/data/db/tables/statistic_table.dart b/lib/data/db/tables/statistic_table.dart index d627894..ef368a5 100644 --- a/lib/data/db/tables/statistic_table.dart +++ b/lib/data/db/tables/statistic_table.dart @@ -4,6 +4,7 @@ class StatisticTable extends Table { TextColumn get id => text()(); TextColumn get type => text()(); TextColumn get timeframe => text().nullable()(); + IntColumn get displayCount => integer().withDefault(const Constant(5))(); @override Set> get primaryKey => {id}; diff --git a/lib/data/models/statistic.dart b/lib/data/models/statistic.dart index 3d118f7..4fdbb05 100644 --- a/lib/data/models/statistic.dart +++ b/lib/data/models/statistic.dart @@ -10,14 +10,16 @@ class Statistic { final Timeframe? timeframe; final List? selectedGroups; final List? selectedGames; + final int displayCount; Statistic({ required this.type, required this.scopes, - String? id, this.timeframe, this.selectedGroups, this.selectedGames, + this.displayCount = 5, + String? id, }) : id = id ?? const Uuid().v4(); @override diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart index 8461f6d..fd43390 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart @@ -21,7 +21,6 @@ Widget buildStatisticTile({ required List players, required BuildContext context, double? width, - int itemCount = 5, }) { final filteredMatches = _getFilterMatches(statistic, matches); final filteredPlayers = _getFilteredPlayers( @@ -46,7 +45,6 @@ Widget buildStatisticTile({ title: translateStatisticTypeToString(statistic.type, context), width: width ?? MediaQuery.sizeOf(context).width * 0.95, values: values, - itemCount: itemCount, barColor: _getStatisticColor(statistic), statistic: statistic, ); @@ -297,7 +295,6 @@ Widget buildSkeletonStatisticTile({required BuildContext context}) { title: 'Skeleton title', width: MediaQuery.sizeOf(context).width * 0.95, values: values, - itemCount: count, barColor: _colorPalette[Random().nextInt(_colorPalette.length)], statistic: Statistic( type: StatisticType.totalMatches, diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index ad850dd..24d51fc 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -26,7 +26,6 @@ class StatisticsTile extends StatelessWidget { required this.title, required this.width, required this.values, - required this.itemCount, required this.barColor, required this.statistic, }); @@ -43,9 +42,6 @@ class StatisticsTile extends StatelessWidget { /// A list of tuples containing labels and their corresponding numeric values. final List<(Player, num)> values; - /// The maximum number of items to display. - final int itemCount; - /// The color of the bars representing the values. final Color barColor; @@ -74,7 +70,7 @@ class StatisticsTile extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final maxBarWidth = constraints.maxWidth * 0.8; - final displayCount = min(values.length, itemCount); + final displayCount = min(values.length, statistic.displayCount); final displayValues = values.take(displayCount).toList(); final maxVal = displayValues.isNotEmpty ? displayValues.fold( diff --git a/pubspec.yaml b/pubspec.yaml index b6563cc..d6fd994 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.33+274 +version: 0.0.33+276 environment: sdk: ^3.8.1 From bccd47e20ec00ba4c8d4d2ac01b6a0a637ad2e72 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 23:27:14 +0200 Subject: [PATCH 27/34] Refactoring --- lib/data/dao/game_dao.dart | 32 +++++++------- lib/data/dao/group_dao.dart | 58 ++++++++++++------------- lib/data/dao/match_dao.dart | 61 ++++++++++++++------------- lib/data/dao/player_dao.dart | 48 ++++++++++----------- lib/data/dao/player_group_dao.dart | 26 +++++++----- lib/data/dao/player_match_dao.dart | 23 +++++----- lib/data/dao/score_entry_dao.dart | 42 +++++++++--------- lib/data/dao/statistic_game_dao.dart | 16 +++---- lib/data/dao/statistic_scope_dao.dart | 6 +-- lib/data/dao/team_dao.dart | 22 +++++----- 10 files changed, 171 insertions(+), 163 deletions(-) diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index 1adfc9e..3ee8ebd 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -77,8 +77,8 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Returns `true` if the game exists, `false` otherwise. Future gameExists({required String gameId}) async { final query = select(gameTable)..where((g) => g.id.equals(gameId)); - final result = await query.getSingleOrNull(); - return result != null; + final row = await query.getSingleOrNull(); + return row != null; } /// Retrieves all games from the database. @@ -103,15 +103,15 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Retrieves a [Game] by its [gameId]. Future getGameById({required String gameId}) async { final query = select(gameTable)..where((g) => g.id.equals(gameId)); - final result = await query.getSingle(); + final row = await query.getSingle(); return Game( - id: result.id, - name: result.name, - ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), - description: result.description, - color: AppColor.values.firstWhere((e) => e.name == result.color), - icon: result.icon, - createdAt: result.createdAt, + id: row.id, + name: row.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), + description: row.description, + color: AppColor.values.firstWhere((e) => e.name == row.color), + icon: row.icon, + createdAt: row.createdAt, ); } @@ -123,7 +123,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { required String name, }) async { final rowsAffected = - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( GameTableCompanion(name: Value(name)), ); return rowsAffected > 0; @@ -135,7 +135,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { required Ruleset ruleset, }) async { final rowsAffected = - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( GameTableCompanion(ruleset: Value(ruleset.name)), ); return rowsAffected > 0; @@ -147,7 +147,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { required String description, }) async { final rowsAffected = - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( GameTableCompanion(description: Value(description)), ); return rowsAffected > 0; @@ -159,7 +159,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { required AppColor color, }) async { final rowsAffected = - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( GameTableCompanion(color: Value(color.name)), ); return rowsAffected > 0; @@ -171,7 +171,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { required String icon, }) async { final rowsAffected = - await (update(gameTable)..where((g) => g.id.equals(gameId))).write( + await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write( GameTableCompanion(icon: Value(icon)), ); return rowsAffected > 0; @@ -182,7 +182,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Deletes the game with the given [gameId] from the database. /// Returns `true` if the game was deleted, `false` if the game did not exist. Future deleteGame({required String gameId}) async { - final query = delete(gameTable)..where((g) => g.id.equals(gameId)); + final query = delete(gameTable)..where((tbl) => tbl.id.equals(gameId)); final rowsAffected = await query.go(); return rowsAffected > 0; } diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 2de2ab9..8d1c0a2 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -143,16 +143,16 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { final query = select(groupTable); final result = await query.get(); return Future.wait( - result.map((groupData) async { + result.map((row) async { final members = await db.playerGroupDao.getPlayersOfGroup( - groupId: groupData.id, + groupId: row.id, ); return Group( - id: groupData.id, - name: groupData.name, - description: groupData.description, + id: row.id, + name: row.name, + description: row.description, members: members, - createdAt: groupData.createdAt, + createdAt: row.createdAt, ); }), ); @@ -161,18 +161,18 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { /// Retrieves a [Group] by its [groupId], including its members. Future getGroupById({required String groupId}) async { final query = select(groupTable)..where((g) => g.id.equals(groupId)); - final result = await query.getSingle(); + final row = await query.getSingle(); List members = await db.playerGroupDao.getPlayersOfGroup( groupId: groupId, ); return Group( - id: result.id, - name: result.name, - description: result.description, + id: row.id, + name: row.name, + description: row.description, members: members, - createdAt: result.createdAt, + createdAt: row.createdAt, ); } @@ -180,7 +180,7 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { Future getGroupCount() async { final count = await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) - .map((row) => row.read(groupTable.id.count())) + .map((tbl) => tbl.read(groupTable.id.count())) .getSingle(); return count ?? 0; } @@ -190,28 +190,28 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { Future> getGroupsByPlayer({required String playerId}) async { final playerGroups = await (select( playerGroupTable, - )..where((pg) => pg.playerId.equals(playerId))).get(); + )..where((tbl) => tbl.playerId.equals(playerId))).get(); if (playerGroups.isEmpty) return []; final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList(); - final rows = + final result = await (select(groupTable) - ..where((g) => g.id.isIn(groupIds)) - ..orderBy([(g) => OrderingTerm.desc(g.createdAt)])) + ..where((tbl) => tbl.id.isIn(groupIds)) + ..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)])) .get(); return Future.wait( - rows.map((groupData) async { + result.map((row) async { final members = await db.playerGroupDao.getPlayersOfGroup( - groupId: groupData.id, + groupId: row.id, ); return Group( - id: groupData.id, - name: groupData.name, - description: groupData.description, + id: row.id, + name: row.name, + description: row.description, members: members, - createdAt: groupData.createdAt, + createdAt: row.createdAt, ); }), ); @@ -221,8 +221,8 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { /// Returns `true` if the group exists, `false` otherwise. Future groupExists({required String groupId}) async { final query = select(groupTable)..where((g) => g.id.equals(groupId)); - final result = await query.getSingleOrNull(); - return result != null; + final row = await query.getSingleOrNull(); + return row != null; } /* Delete */ @@ -252,9 +252,8 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { required String name, }) async { final rowsAffected = - await (update(groupTable)..where((g) => g.id.equals(groupId))).write( - GroupTableCompanion(name: Value(name)), - ); + await (update(groupTable)..where((tbl) => tbl.id.equals(groupId))) + .write(GroupTableCompanion(name: Value(name))); return rowsAffected > 0; } @@ -265,9 +264,8 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { required String description, }) async { final rowsAffected = - await (update(groupTable)..where((g) => g.id.equals(groupId))).write( - GroupTableCompanion(description: Value(description)), - ); + await (update(groupTable)..where((tbl) => tbl.id.equals(groupId))) + .write(GroupTableCompanion(description: Value(description))); return rowsAffected > 0; } } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index e8414f4..74611b6 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -258,15 +258,15 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Returns `true` if the match exists, otherwise `false`. Future matchExists({required String matchId}) async { final query = select(matchTable)..where((g) => g.id.equals(matchId)); - final result = await query.getSingleOrNull(); - return result != null; + final row = await query.getSingleOrNull(); + return row != null; } /// Retrieves the number of matches in the database. Future getMatchCount() async { final count = await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) - .map((row) => row.read(matchTable.id.count())) + .map((tbl) => tbl.read(matchTable.id.count())) .getSingle(); return count ?? 0; } @@ -279,10 +279,12 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { return Future.wait( result.map((row) async { final game = await db.gameDao.getGameById(gameId: row.gameId); + Group? group; if (row.groupId != null) { group = await db.groupDao.getGroupById(groupId: row.groupId!); } + final players = await db.playerMatchDao.getPlayersOfMatch( matchId: row.id, ); @@ -312,13 +314,13 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Retrieves a [Match] by its [matchId]. Future getMatchById({required String matchId}) async { final query = select(matchTable)..where((g) => g.id.equals(matchId)); - final result = await query.getSingle(); + final row = await query.getSingle(); - final game = await db.gameDao.getGameById(gameId: result.gameId); + final game = await db.gameDao.getGameById(gameId: row.gameId); Group? group; - if (result.groupId != null) { - group = await db.groupDao.getGroupById(groupId: result.groupId!); + if (row.groupId != null) { + group = await db.groupDao.getGroupById(groupId: row.groupId!); } final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); @@ -328,15 +330,15 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { final teams = await _getMatchTeams(matchId: matchId); return Match( - id: result.id, - name: result.name, + id: row.id, + name: row.name, game: game, group: group, players: players, teams: teams.isEmpty ? null : teams, - notes: result.notes, - createdAt: result.createdAt, - endedAt: result.endedAt, + notes: row.notes, + createdAt: row.createdAt, + endedAt: row.endedAt, scores: scores, ); } @@ -347,7 +349,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { await (selectOnly(matchTable) ..where(matchTable.gameId.equals(gameId)) ..addColumns([matchTable.id.count()])) - .map((row) => row.read(matchTable.id.count())) + .map((tbl) => tbl.read(matchTable.id.count())) .getSingle(); return count ?? 0; } @@ -355,19 +357,19 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { Future> getMatchesByPlayer({required String playerId}) async { final playerMatches = await (select( playerMatchTable, - )..where((pm) => pm.playerId.equals(playerId))).get(); + )..where((tbl) => tbl.playerId.equals(playerId))).get(); if (playerMatches.isEmpty) return []; - final matchIds = playerMatches.map((pm) => pm.matchId).toSet().toList(); - final rows = + final matchIds = playerMatches.map((tbl) => tbl.matchId).toSet().toList(); + final result = await (select(matchTable) - ..where((m) => m.id.isIn(matchIds)) - ..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) + ..where((tbl) => tbl.id.isIn(matchIds)) + ..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)])) .get(); return Future.wait( - rows.map((row) async { + result.map((row) async { final game = await db.gameDao.getGameById(gameId: row.gameId); Group? group; @@ -403,16 +405,17 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Queries the database directly, filtering by [groupId]. Future> getMatchesByGroup({required String groupId}) async { final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); - final rows = await query.get(); + final result = await query.get(); return Future.wait( - rows.map((row) async { + result.map((row) async { final game = await db.gameDao.getGameById(gameId: row.gameId); final group = await db.groupDao.getGroupById(groupId: groupId); final players = await db.playerMatchDao.getPlayersOfMatch( matchId: row.id, ); final teams = await _getMatchTeams(matchId: row.id); + return Match( id: row.id, name: row.name, @@ -432,7 +435,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { Future> _getMatchTeams({required String matchId}) async { // Get all unique team IDs from PlayerMatchTable for this match final playerMatchQuery = select(db.playerMatchTable) - ..where((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull()); + ..where((tbl) => tbl.matchId.equals(matchId) & tbl.teamId.isNotNull()); final playerMatches = await playerMatchQuery.get(); if (playerMatches.isEmpty) return []; @@ -459,7 +462,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { required String matchId, required String name, }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(name: Value(name)), ); @@ -474,7 +477,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { required String matchId, required String? groupId, }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(groupId: Value(groupId)), ); @@ -487,7 +490,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { required String matchId, required String notes, }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(notes: Value(notes)), ); @@ -498,7 +501,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Sets the groupId to null. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future removeMatchGroup({required String matchId}) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final rowsAffected = await query.write( const MatchTableCompanion(groupId: Value(null)), ); @@ -512,7 +515,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { required String matchId, required DateTime endedAt, }) async { - final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId)); final rowsAffected = await query.write( MatchTableCompanion(endedAt: Value(endedAt)), ); @@ -524,7 +527,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Deletes the match with the given [matchId] from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteMatch({required String matchId}) async { - final query = delete(matchTable)..where((g) => g.id.equals(matchId)); + final query = delete(matchTable)..where((tbl) => tbl.id.equals(matchId)); final rowsAffected = await query.go(); return rowsAffected > 0; } @@ -540,7 +543,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Deletes all matches associated with a specific game. /// Returns the number of matches deleted. Future deleteMatchesByGame({required String gameId}) async { - final query = delete(matchTable)..where((m) => m.gameId.equals(gameId)); + final query = delete(matchTable)..where((tbl) => tbl.gameId.equals(gameId)); final rowsAffected = await query.go(); return rowsAffected; } diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index a6fd1c5..1a60243 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -113,7 +113,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { Future getPlayerCount() async { final count = await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) - .map((row) => row.read(playerTable.id.count())) + .map((tbl) => tbl.read(playerTable.id.count())) .getSingle(); return count ?? 0; } @@ -122,8 +122,8 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// Returns `true` if the player exists, `false` otherwise. Future playerExists({required String playerId}) async { final query = select(playerTable)..where((p) => p.id.equals(playerId)); - final result = await query.getSingleOrNull(); - return result != null; + final row = await query.getSingleOrNull(); + return row != null; } /// Retrieves all players from the database. @@ -146,13 +146,13 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// Retrieves a [Player] by their [id]. Future getPlayerById({required String playerId}) async { final query = select(playerTable)..where((p) => p.id.equals(playerId)); - final result = await query.getSingle(); + final row = await query.getSingle(); return Player( - id: result.id, - name: result.name, - description: result.description, - createdAt: result.createdAt, - nameCount: result.nameCount, + id: row.id, + name: row.name, + description: row.description, + createdAt: row.createdAt, + nameCount: row.nameCount, ); } @@ -174,7 +174,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return transaction(() async { final previousPlayer = await (select( playerTable, - )..where((p) => p.id.equals(playerId))).getSingleOrNull(); + )..where((tbl) => tbl.id.equals(playerId))).getSingleOrNull(); if (previousPlayer == null) return false; final previousName = previousPlayer.name; @@ -186,7 +186,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { final rowsAffected = await (update( playerTable, - )..where((p) => p.id.equals(playerId))).write( + )..where((tbl) => tbl.id.equals(playerId))).write( PlayerTableCompanion( name: Value(name), nameCount: Value(newNameCount), @@ -203,9 +203,9 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { } else if (remainingCount > 1 && previousCount > 0) { // Shift every player above the gap down by one to keep numbering in order. await (update(playerTable)..where( - (p) => - p.name.equals(previousName) & - p.nameCount.isBiggerThanValue(previousCount), + (tbl) => + tbl.name.equals(previousName) & + tbl.nameCount.isBiggerThanValue(previousCount), )) .write( PlayerTableCompanion.custom( @@ -226,9 +226,8 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { required String description, }) async { final rowsAffected = - await (update(playerTable)..where((g) => g.id.equals(playerId))).write( - PlayerTableCompanion(description: Value(description)), - ); + await (update(playerTable)..where((tbl) => tbl.id.equals(playerId))) + .write(PlayerTableCompanion(description: Value(description))); return rowsAffected > 0; } @@ -237,7 +236,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// Deletes the player with the given [id] from the database. /// Returns `true` if the player was deleted, `false` if the player did not exist. Future deletePlayer({required String playerId}) async { - final query = delete(playerTable)..where((p) => p.id.equals(playerId)); + final query = delete(playerTable)..where((tbl) => tbl.id.equals(playerId)); final rowsAffected = await query.go(); return rowsAffected > 0; } @@ -248,7 +247,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// Returns the highest name count if players with the same name exist, /// otherwise `null`. Future getNameCount({required String name}) async { - final query = select(playerTable)..where((p) => p.name.equals(name)); + final query = select(playerTable)..where((tbl) => tbl.name.equals(name)); final result = await query.get(); return result.length; } @@ -259,7 +258,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { required String playerId, required int nameCount, }) async { - final query = update(playerTable)..where((p) => p.id.equals(playerId)); + final query = update(playerTable)..where((tbl) => tbl.id.equals(playerId)); final rowsAffected = await query.write( PlayerTableCompanion(nameCount: Value(nameCount)), ); @@ -269,8 +268,8 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { @visibleForTesting Future getPlayerWithHighestNameCount({required String name}) async { final query = select(playerTable) - ..where((p) => p.name.equals(name)) - ..orderBy([(p) => OrderingTerm.desc(p.nameCount)]) + ..where((tbl) => tbl.name.equals(name)) + ..orderBy([(tbl) => OrderingTerm.desc(tbl.nameCount)]) ..limit(1); final result = await query.getSingleOrNull(); if (result != null) { @@ -324,9 +323,8 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { @visibleForTesting Future initializeNameCount({required String name}) async { final rowsAffected = - await (update(playerTable)..where((p) => p.name.equals(name))).write( - const PlayerTableCompanion(nameCount: Value(1)), - ); + await (update(playerTable)..where((tbl) => tbl.name.equals(name))) + .write(const PlayerTableCompanion(nameCount: Value(1))); return rowsAffected > 0; } diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart index 5139eea..b48dc23 100644 --- a/lib/data/dao/player_group_dao.dart +++ b/lib/data/dao/player_group_dao.dart @@ -46,15 +46,15 @@ class PlayerGroupDao extends DatabaseAccessor ), ])..where(playerGroupTable.groupId.equals(groupId)); - final results = await query.map((row) => row.readTable(playerTable)).get(); - return results + final result = await query.map((row) => row.readTable(playerTable)).get(); + return result .map( - (result) => Player( - id: result.id, - createdAt: result.createdAt, - name: result.name, - nameCount: result.nameCount, - description: result.description, + (row) => Player( + id: row.id, + createdAt: row.createdAt, + name: row.name, + nameCount: row.nameCount, + description: row.description, ), ) .toList(); @@ -67,7 +67,9 @@ class PlayerGroupDao extends DatabaseAccessor required String groupId, }) async { final query = select(playerGroupTable) - ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); + ..where( + (tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId), + ); final result = await query.getSingleOrNull(); return result != null; } @@ -88,7 +90,7 @@ class PlayerGroupDao extends DatabaseAccessor await db.transaction(() async { // Remove all existing players from the group final deleteQuery = delete(db.playerGroupTable) - ..where((p) => p.groupId.equals(groupId)); + ..where((tbl) => tbl.groupId.equals(groupId)); await deleteQuery.go(); // Add new players to the player table if they don't exist @@ -128,7 +130,9 @@ class PlayerGroupDao extends DatabaseAccessor required String groupId, }) async { final query = delete(playerGroupTable) - ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); + ..where( + (tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId), + ); final rowsAffected = await query.go(); return rowsAffected > 0; } diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index d119468..912cfcc 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -40,7 +40,7 @@ class PlayerMatchDao extends DatabaseAccessor await (selectOnly(playerMatchTable) ..where(playerMatchTable.matchId.equals(matchId)) ..addColumns([playerMatchTable.playerId.count()])) - .map((row) => row.read(playerMatchTable.playerId.count())) + .map((tbl) => tbl.read(playerMatchTable.playerId.count())) .getSingle(); return (count ?? 0) > 0; } @@ -56,7 +56,7 @@ class PlayerMatchDao extends DatabaseAccessor ..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.playerId.equals(playerId)) ..addColumns([playerMatchTable.playerId.count()])) - .map((row) => row.read(playerMatchTable.playerId.count())) + .map((tbl) => tbl.read(playerMatchTable.playerId.count())) .getSingle(); return (count ?? 0) > 0; } @@ -66,7 +66,7 @@ class PlayerMatchDao extends DatabaseAccessor Future> getPlayersOfMatch({required String matchId}) async { final result = await (select( playerMatchTable, - )..where((p) => p.matchId.equals(matchId))).get(); + )..where((tbl) => tbl.matchId.equals(matchId))).get(); if (result.isEmpty) return []; @@ -85,8 +85,8 @@ class PlayerMatchDao extends DatabaseAccessor }) async { final result = await (select(playerMatchTable) - ..where((p) => p.matchId.equals(matchId)) - ..where((p) => p.teamId.equals(teamId))) + ..where((tbl) => tbl.matchId.equals(matchId)) + ..where((tbl) => tbl.teamId.equals(teamId))) .get(); if (result.isEmpty) return []; @@ -109,7 +109,8 @@ class PlayerMatchDao extends DatabaseAccessor }) async { final rowsAffected = await (update(playerMatchTable)..where( - (p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), + (tbl) => + tbl.matchId.equals(matchId) & tbl.playerId.equals(playerId), )) .write(PlayerMatchTableCompanion(teamId: Value(teamId))); return rowsAffected > 0; @@ -143,9 +144,9 @@ class PlayerMatchDao extends DatabaseAccessor // Remove old players if (playersToRemove.isNotEmpty) { await (delete(playerMatchTable)..where( - (pg) => - pg.matchId.equals(matchId) & - pg.playerId.isIn(playersToRemove.toList()), + (tbl) => + tbl.matchId.equals(matchId) & + tbl.playerId.isIn(playersToRemove.toList()), )) .go(); } @@ -182,8 +183,8 @@ class PlayerMatchDao extends DatabaseAccessor required String playerId, }) async { final query = delete(playerMatchTable) - ..where((pg) => pg.matchId.equals(matchId)) - ..where((pg) => pg.playerId.equals(playerId)); + ..where((tbl) => tbl.matchId.equals(matchId)) + ..where((tbl) => tbl.playerId.equals(playerId)); final rowsAffected = await query.go(); return rowsAffected > 0; } diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 830135d..276e8fd 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -70,10 +70,10 @@ class ScoreEntryDao extends DatabaseAccessor }) async { final query = select(scoreEntryTable) ..where( - (s) => - s.playerId.equals(playerId) & - s.matchId.equals(matchId) & - s.roundNumber.equals(roundNumber), + (tbl) => + tbl.playerId.equals(playerId) & + tbl.matchId.equals(matchId) & + tbl.roundNumber.equals(roundNumber), ); final result = await query.getSingleOrNull(); @@ -91,7 +91,7 @@ class ScoreEntryDao extends DatabaseAccessor required String matchId, }) async { final query = select(scoreEntryTable) - ..where((s) => s.matchId.equals(matchId)); + ..where((tbl) => tbl.matchId.equals(matchId)); final result = await query.get(); final Map scoresByPlayer = {}; @@ -113,8 +113,10 @@ class ScoreEntryDao extends DatabaseAccessor required String matchId, }) async { final query = select(scoreEntryTable) - ..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId)) - ..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]); + ..where( + (tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId), + ) + ..orderBy([(tbl) => OrderingTerm.asc(tbl.roundNumber)]); final result = await query.get(); return result .map( @@ -136,8 +138,8 @@ class ScoreEntryDao extends DatabaseAccessor final query = selectOnly(scoreEntryTable) ..where(scoreEntryTable.matchId.equals(matchId)) ..addColumns([scoreEntryTable.roundNumber.max()]); - final result = await query.getSingle(); - return result.read(scoreEntryTable.roundNumber.max()); + final row = await query.getSingle(); + return row.read(scoreEntryTable.roundNumber.max()); } /// Aggregates the total score for a player in a match by summing all their @@ -166,10 +168,10 @@ class ScoreEntryDao extends DatabaseAccessor }) async { final rowsAffected = await (update(scoreEntryTable)..where( - (s) => - s.playerId.equals(playerId) & - s.matchId.equals(matchId) & - s.roundNumber.equals(entry.roundNumber), + (tbl) => + tbl.playerId.equals(playerId) & + tbl.matchId.equals(matchId) & + tbl.roundNumber.equals(entry.roundNumber), )) .write( ScoreEntryTableCompanion( @@ -190,10 +192,10 @@ class ScoreEntryDao extends DatabaseAccessor }) async { final query = delete(scoreEntryTable) ..where( - (s) => - s.playerId.equals(playerId) & - s.matchId.equals(matchId) & - s.roundNumber.equals(roundNumber), + (tbl) => + tbl.playerId.equals(playerId) & + tbl.matchId.equals(matchId) & + tbl.roundNumber.equals(roundNumber), ); final rowsAffected = await query.go(); return rowsAffected > 0; @@ -201,7 +203,7 @@ class ScoreEntryDao extends DatabaseAccessor Future deleteAllScoresForMatch({required String matchId}) async { final query = delete(scoreEntryTable) - ..where((s) => s.matchId.equals(matchId)); + ..where((tbl) => tbl.matchId.equals(matchId)); final rowsAffected = await query.go(); return rowsAffected > 0; } @@ -211,7 +213,9 @@ class ScoreEntryDao extends DatabaseAccessor required String playerId, }) async { final query = delete(scoreEntryTable) - ..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId)); + ..where( + (tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId), + ); final rowsAffected = await query.go(); return rowsAffected > 0; } diff --git a/lib/data/dao/statistic_game_dao.dart b/lib/data/dao/statistic_game_dao.dart index 4ee5b84..76e3429 100644 --- a/lib/data/dao/statistic_game_dao.dart +++ b/lib/data/dao/statistic_game_dao.dart @@ -20,14 +20,14 @@ class StatisticGameDao extends DatabaseAccessor final results = await query.map((row) => row.readTable(gameTable)).get(); return results .map( - (result) => Game( - id: result.id, - name: result.name, - ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), - description: result.description, - color: AppColor.values.firstWhere((e) => e.name == result.color), - icon: result.icon, - createdAt: result.createdAt, + (row) => Game( + id: row.id, + name: row.name, + ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), + description: row.description, + color: AppColor.values.firstWhere((e) => e.name == row.color), + icon: row.icon, + createdAt: row.createdAt, ), ) .toList(); diff --git a/lib/data/dao/statistic_scope_dao.dart b/lib/data/dao/statistic_scope_dao.dart index 2027acd..d2fc315 100644 --- a/lib/data/dao/statistic_scope_dao.dart +++ b/lib/data/dao/statistic_scope_dao.dart @@ -18,10 +18,10 @@ class StatisticScopeDao extends DatabaseAccessor final results = await query.get(); return results .map( - (result) => StatisticScope.values.firstWhere( - (e) => e.name == result.scope, + (row) => StatisticScope.values.firstWhere( + (e) => e.name == row.scope, orElse: () => throw Exception( - 'Invalid scope value: ${result.scope} for statistic ID: $statisticId', + 'Invalid scope value: ${row.scope} for statistic ID: $statisticId', ), ), ) diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index cba68fb..333db68 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -86,7 +86,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { Future getTeamCount() async { final count = await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) - .map((row) => row.read(teamTable.id.count())) + .map((tbl) => tbl.read(teamTable.id.count())) .getSingle(); return count ?? 0; } @@ -95,8 +95,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /// Returns `true` if the team exists, `false` otherwise. Future teamExists({required String teamId}) async { final query = select(teamTable)..where((t) => t.id.equals(teamId)); - final result = await query.getSingleOrNull(); - return result != null; + final row = await query.getSingleOrNull(); + return row != null; } /// Retrieves all teams from the database. @@ -119,12 +119,12 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /// Retrieves a [Team] by its [teamId], including its members. Future getTeamById({required String teamId}) async { final query = select(teamTable)..where((t) => t.id.equals(teamId)); - final result = await query.getSingle(); + final row = await query.getSingle(); final members = await _getTeamMembers(teamId: teamId); return Team( - id: result.id, - name: result.name, - createdAt: result.createdAt, + id: row.id, + name: row.name, + createdAt: row.createdAt, members: members, ); } @@ -133,13 +133,13 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { Future> _getTeamMembers({required String teamId}) async { // Get all player_match entries with this teamId final playerMatchQuery = select(db.playerMatchTable) - ..where((pm) => pm.teamId.equals(teamId)); + ..where((tbl) => tbl.teamId.equals(teamId)); final playerMatches = await playerMatchQuery.get(); if (playerMatches.isEmpty) return []; // Get unique player IDs - final playerIds = playerMatches.map((pm) => pm.playerId).toSet(); + final playerIds = playerMatches.map((tbl) => tbl.playerId).toSet(); // Fetch all players final players = await Future.wait( @@ -156,7 +156,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { required String name, }) async { final rowsAffected = - await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + await (update(teamTable)..where((tbl) => tbl.id.equals(teamId))).write( TeamTableCompanion(name: Value(name)), ); return rowsAffected > 0; @@ -175,7 +175,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /// Deletes the team with the given [teamId] from the database. /// Returns `true` if the team was deleted, `false` otherwise. Future deleteTeam({required String teamId}) async { - final query = delete(teamTable)..where((t) => t.id.equals(teamId)); + final query = delete(teamTable)..where((tbl) => tbl.id.equals(teamId)); final rowsAffected = await query.go(); return rowsAffected > 0; } From 72442b53753ae30bdd05221595f4941c0939d310 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 23:34:53 +0200 Subject: [PATCH 28/34] fix: added delete function --- lib/services/data_transfer_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 0f2f8fa..9dbf955 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -20,6 +20,7 @@ class DataTransferService { static Future deleteAllData(BuildContext context) async { final db = Provider.of(context, listen: false); + await db.statisticDao.deleteAllStatistics(); await db.matchDao.deleteAllMatches(); await db.teamDao.deleteAllTeams(); await db.groupDao.deleteAllGroups(); From bfb40d2eab387ad632d4bae2942bc7e03075d6b2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 25 May 2026 00:39:01 +0200 Subject: [PATCH 29/34] feat: statistic detail view --- lib/data/dao/statistic_dao.dart | 3 +- lib/data/dao/statistic_game_dao.dart | 3 +- lib/data/dao/statistic_group_dao.dart | 3 +- lib/data/dao/statistic_scope_dao.dart | 4 +- lib/l10n/arb/app_de.arb | 7 + lib/l10n/arb/app_en.arb | 30 +-- lib/l10n/generated/app_localizations.dart | 58 +++--- lib/l10n/generated/app_localizations_de.dart | 36 ++-- lib/l10n/generated/app_localizations_en.dart | 32 ++-- .../main_menu/match_view/match_view.dart | 2 +- .../create_statistic_view.dart | 20 +- .../statistic_detail_view.dart | 178 ++++++++++++++++++ .../statistic_tile_factory.dart | 49 +++-- .../statistics_view/statistics_view.dart | 82 +++++--- .../widgets/tiles/statistics_tile.dart | 40 ++-- pubspec.yaml | 2 +- 16 files changed, 406 insertions(+), 143 deletions(-) create mode 100644 lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart diff --git a/lib/data/dao/statistic_dao.dart b/lib/data/dao/statistic_dao.dart index 7bdebde..092ceb0 100644 --- a/lib/data/dao/statistic_dao.dart +++ b/lib/data/dao/statistic_dao.dart @@ -80,12 +80,13 @@ class StatisticDao extends DatabaseAccessor result.map((row) async { final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id); final games = await db.statisticGameDao.getGamesForStatistic(row.id); + final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id); return Statistic( type: StatisticType.values.firstWhere( (type) => type.name == row.type, ), - scopes: [], + scopes: scopes, timeframe: Timeframe.values.firstWhereOrNull( (t) => t.name == row.timeframe, ), diff --git a/lib/data/dao/statistic_game_dao.dart b/lib/data/dao/statistic_game_dao.dart index 76e3429..d546ce0 100644 --- a/lib/data/dao/statistic_game_dao.dart +++ b/lib/data/dao/statistic_game_dao.dart @@ -12,12 +12,13 @@ class StatisticGameDao extends DatabaseAccessor StatisticGameDao(super.db); /// Retrieves a list of games associated with a specific statistic. - Future> getGamesForStatistic(String statisticId) async { + Future?> getGamesForStatistic(String statisticId) async { final query = select(statisticGameTable).join([ innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)), ])..where(statisticGameTable.statisticId.equals(statisticId)); final results = await query.map((row) => row.readTable(gameTable)).get(); + if (results.isEmpty) return null; return results .map( (row) => Game( diff --git a/lib/data/dao/statistic_group_dao.dart b/lib/data/dao/statistic_group_dao.dart index 9eb9397..449b6a8 100644 --- a/lib/data/dao/statistic_group_dao.dart +++ b/lib/data/dao/statistic_group_dao.dart @@ -12,7 +12,7 @@ class StatisticGroupDao extends DatabaseAccessor StatisticGroupDao(super.db); /// Retrieves a list of groups associated with a specific statistic. - Future> getGroupsForStatistic(String statisticId) async { + Future?> getGroupsForStatistic(String statisticId) async { final query = select(statisticGroupTable).join([ innerJoin( groupTable, @@ -21,6 +21,7 @@ class StatisticGroupDao extends DatabaseAccessor ])..where(statisticGroupTable.statisticId.equals(statisticId)); final results = await query.map((row) => row.readTable(groupTable)).get(); + if (results.isEmpty) return null; final groups = await Future.wait( results.map((result) async { final groupMembers = await db.playerGroupDao.getPlayersOfGroup( diff --git a/lib/data/dao/statistic_scope_dao.dart b/lib/data/dao/statistic_scope_dao.dart index d2fc315..eb286af 100644 --- a/lib/data/dao/statistic_scope_dao.dart +++ b/lib/data/dao/statistic_scope_dao.dart @@ -15,8 +15,8 @@ class StatisticScopeDao extends DatabaseAccessor final query = select(statisticScopeTable) ..where((tbl) => tbl.statisticId.equals(statisticId)); - final results = await query.get(); - return results + final result = await query.get(); + return result .map( (row) => StatisticScope.values.firstWhere( (e) => e.name == row.scope, diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 8f92cb0..b6ae56b 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -21,6 +21,13 @@ "color_yellow": "Gelb", "confirm": "Bestätigen", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "@could_not_add_player": { + "placeholders": { + "playerName": { + "type": "String" + } + } + }, "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", "create_match": "Spiel erstellen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b7548bd..baf5006 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -20,23 +20,29 @@ "color_teal": "Teal", "color_yellow": "Yellow", "confirm": "Confirm", - "could_not_add_player": "Could not add player", + "could_not_add_player": "Could not add player {playerName}", + "@could_not_add_player": { + "placeholders": { + "playerName": { + "type": "String" + } + } + }, "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", "create_new_match": "Create new match", "create_statistic": "Create statistic", - "create_statistic_classifier_subtitle": "Select which key metric you want to display", - "create_statistic_classifier_title": "Classifier", - "create_statistic_games_subtitle": "Select the filtered games", - "create_statistic_games_title": "Games", - "create_statistic_groups_subtitle": "Select the filtered groups", - "create_statistic_groups_title": "Groups", - "create_statistic_scope_subtitle": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", - "create_statistic_scope_title": "Scope", - "create_statistic_timeframe_subtitle": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", - "create_statistic_timeframe_title": "Timeframe", + "which_key_metric": "Select which key metric you want to display", + "classifier": "Classifier", + "select_the_filtered_games": "Select the filtered games", + "games": "Games", + "select_the_filtered_groups": "Select the filtered groups", + "select_main_filter": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", + "scope": "Scope", + "select_a_timeframe_for_which_data_will_be_filtered": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", + "timeframe": "Timeframe", "created_on": "Created on", "data": "Data", "data_successfully_deleted": "Data successfully deleted", @@ -54,6 +60,7 @@ } } }, + "filter": "Filter", "delete_group": "Delete Group", "delete_match": "Delete Match", "delete_player": "Delete player?", @@ -120,6 +127,7 @@ "no_results_entered_yet": "No results entered yet", "no_second_match_available": "No second match available", "no_statistics_available": "No statistics available", + "no_statistics_created_yet": "No statistics created yet", "none": "None", "none_group": "None", "not_available": "Not available", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index e8af2e8..20067d4 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -221,8 +221,8 @@ abstract class AppLocalizations { /// No description provided for @could_not_add_player. /// /// In en, this message translates to: - /// **'Could not add player'** - String could_not_add_player(Object playerName); + /// **'Could not add player {playerName}'** + String could_not_add_player(String playerName); /// No description provided for @create_game. /// @@ -260,65 +260,59 @@ abstract class AppLocalizations { /// **'Create statistic'** String get create_statistic; - /// No description provided for @create_statistic_classifier_subtitle. + /// No description provided for @which_key_metric. /// /// In en, this message translates to: /// **'Select which key metric you want to display'** - String get create_statistic_classifier_subtitle; + String get which_key_metric; - /// No description provided for @create_statistic_classifier_title. + /// No description provided for @classifier. /// /// In en, this message translates to: /// **'Classifier'** - String get create_statistic_classifier_title; + String get classifier; - /// No description provided for @create_statistic_games_subtitle. + /// No description provided for @select_the_filtered_games. /// /// In en, this message translates to: /// **'Select the filtered games'** - String get create_statistic_games_subtitle; + String get select_the_filtered_games; - /// No description provided for @create_statistic_games_title. + /// No description provided for @games. /// /// In en, this message translates to: /// **'Games'** - String get create_statistic_games_title; + String get games; - /// No description provided for @create_statistic_groups_subtitle. + /// No description provided for @select_the_filtered_groups. /// /// In en, this message translates to: /// **'Select the filtered groups'** - String get create_statistic_groups_subtitle; + String get select_the_filtered_groups; - /// No description provided for @create_statistic_groups_title. - /// - /// In en, this message translates to: - /// **'Groups'** - String get create_statistic_groups_title; - - /// No description provided for @create_statistic_scope_subtitle. + /// No description provided for @select_main_filter. /// /// In en, this message translates to: /// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'** - String get create_statistic_scope_subtitle; + String get select_main_filter; - /// No description provided for @create_statistic_scope_title. + /// No description provided for @scope. /// /// In en, this message translates to: /// **'Scope'** - String get create_statistic_scope_title; + String get scope; - /// No description provided for @create_statistic_timeframe_subtitle. + /// No description provided for @select_a_timeframe_for_which_data_will_be_filtered. /// /// In en, this message translates to: /// **'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'** - String get create_statistic_timeframe_subtitle; + String get select_a_timeframe_for_which_data_will_be_filtered; - /// No description provided for @create_statistic_timeframe_title. + /// No description provided for @timeframe. /// /// In en, this message translates to: /// **'Timeframe'** - String get create_statistic_timeframe_title; + String get timeframe; /// No description provided for @created_on. /// @@ -380,6 +374,12 @@ abstract class AppLocalizations { /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** String delete_game_with_matches_warning(int count); + /// No description provided for @filter. + /// + /// In en, this message translates to: + /// **'Filter'** + String get filter; + /// No description provided for @delete_group. /// /// In en, this message translates to: @@ -776,6 +776,12 @@ abstract class AppLocalizations { /// **'No statistics available'** String get no_statistics_available; + /// No description provided for @no_statistics_created_yet. + /// + /// In en, this message translates to: + /// **'No statistics created yet'** + String get no_statistics_created_yet; + /// No description provided for @none. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 4ff81bb..8152185 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -69,7 +69,7 @@ class AppLocalizationsDe extends AppLocalizations { String get confirm => 'Bestätigen'; @override - String could_not_add_player(Object playerName) { + String could_not_add_player(String playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; } @@ -92,39 +92,33 @@ class AppLocalizationsDe extends AppLocalizations { String get create_statistic => 'Statistik erstellen'; @override - String get create_statistic_classifier_subtitle => - 'Wähle die anzuzeigende Hauptmetrik aus'; + String get which_key_metric => 'Select which key metric you want to display'; @override - String get create_statistic_classifier_title => 'Klassifikator'; + String get classifier => 'Classifier'; @override - String get create_statistic_games_subtitle => - 'Wähle die gefilterten Spielvorlagen'; + String get select_the_filtered_games => 'Select the filtered games'; @override - String get create_statistic_games_title => 'Spielvorlagen'; + String get games => 'Games'; @override - String get create_statistic_groups_subtitle => - 'Wähle die gefilterten Gruppen'; + String get select_the_filtered_groups => 'Select the filtered groups'; @override - String get create_statistic_groups_title => 'Gruppen'; + String get select_main_filter => + 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; @override - String get create_statistic_scope_subtitle => - 'Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.'; + String get scope => 'Scope'; @override - String get create_statistic_scope_title => 'Bereich'; + String get select_a_timeframe_for_which_data_will_be_filtered => + 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; @override - String get create_statistic_timeframe_subtitle => - 'Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.'; - - @override - String get create_statistic_timeframe_title => 'Zeitraum'; + String get timeframe => 'Timeframe'; @override String get created_on => 'Erstellt am'; @@ -166,6 +160,9 @@ class AppLocalizationsDe extends AppLocalizations { return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; } + @override + String get filter => 'Filter'; + @override String get delete_group => 'Gruppe löschen'; @@ -369,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_statistics_available => 'Keine Statistiken verfügbar'; + @override + String get no_statistics_created_yet => 'No statistics created yet'; + @override String get none => 'Kein'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 4bc1dd7..2366ce2 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -69,8 +69,8 @@ class AppLocalizationsEn extends AppLocalizations { String get confirm => 'Confirm'; @override - String could_not_add_player(Object playerName) { - return 'Could not add player'; + String could_not_add_player(String playerName) { + return 'Could not add player $playerName'; } @override @@ -92,37 +92,33 @@ class AppLocalizationsEn extends AppLocalizations { String get create_statistic => 'Create statistic'; @override - String get create_statistic_classifier_subtitle => - 'Select which key metric you want to display'; + String get which_key_metric => 'Select which key metric you want to display'; @override - String get create_statistic_classifier_title => 'Classifier'; + String get classifier => 'Classifier'; @override - String get create_statistic_games_subtitle => 'Select the filtered games'; + String get select_the_filtered_games => 'Select the filtered games'; @override - String get create_statistic_games_title => 'Games'; + String get games => 'Games'; @override - String get create_statistic_groups_subtitle => 'Select the filtered groups'; + String get select_the_filtered_groups => 'Select the filtered groups'; @override - String get create_statistic_groups_title => 'Groups'; - - @override - String get create_statistic_scope_subtitle => + String get select_main_filter => 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; @override - String get create_statistic_scope_title => 'Scope'; + String get scope => 'Scope'; @override - String get create_statistic_timeframe_subtitle => + String get select_a_timeframe_for_which_data_will_be_filtered => 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; @override - String get create_statistic_timeframe_title => 'Timeframe'; + String get timeframe => 'Timeframe'; @override String get created_on => 'Created on'; @@ -164,6 +160,9 @@ class AppLocalizationsEn extends AppLocalizations { return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; } + @override + String get filter => 'Filter'; + @override String get delete_group => 'Delete Group'; @@ -367,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_statistics_available => 'No statistics available'; + @override + String get no_statistics_created_yet => 'No statistics created yet'; + @override String get none => 'None'; diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 3c01f45..1d30afb 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -79,7 +79,7 @@ class _MatchViewState extends State { visible: matches.isNotEmpty, replacement: Center( child: TopCenteredMessage( - icon: Icons.report, + icon: Icons.info, title: loc.info, message: loc.no_matches_created_yet, ), diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart index 9ac03aa..92a01bb 100644 --- a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -68,7 +68,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_classifier_title, + loc.classifier, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -77,7 +77,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_classifier_subtitle, + loc.select_a_classifier, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -139,7 +139,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_scope_title, + loc.scope, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -148,7 +148,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_scope_subtitle, + loc.select_a_scope, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -214,7 +214,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_games_title, + loc.games, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -223,7 +223,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_games_subtitle, + loc.select_the_filtered_games, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -310,7 +310,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_groups_title, + loc.groups, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -319,7 +319,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_groups_subtitle, + loc.select_the_filtered_groups, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -396,7 +396,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_timeframe_title, + loc.timeframe, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -405,7 +405,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_timeframe_subtitle, + loc.select_a_timeframe_for_which_data_will_be_filtered, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart new file mode 100644 index 0000000..4053cb3 --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart' + show + translateScopeToString, + translateStatisticTypeToString, + translateTimeframeToString; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; + +class StatisticDetailView extends StatefulWidget { + const StatisticDetailView({ + super.key, + required this.statistic, + required this.values, + required this.icon, + required this.barColor, + }); + + final Statistic statistic; + final List<(Player, num)> values; + final IconData icon; + final Color barColor; + + @override + State createState() => _StatisticDetailViewState(); +} + +class _StatisticDetailViewState extends State { + int displayCount = 0; + + @override + void initState() { + super.initState(); + displayCount = widget.statistic.displayCount; + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final title = translateStatisticTypeToString( + widget.statistic.type, + context, + ); + const style = TextStyle(fontWeight: FontWeight.bold); + + return Scaffold( + appBar: AppBar(title: Text(title)), + body: SingleChildScrollView( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + StatisticsTile( + icon: widget.icon, + title: title, + width: MediaQuery.sizeOf(context).width * 0.95, + values: widget.values, + barColor: widget.barColor, + selectedGroups: widget.statistic.selectedGroups, + selectedGames: widget.statistic.selectedGames, + ), + const SizedBox(height: 12), + + InfoTile( + icon: Icons.filter_alt, + title: loc.filter, + content: Column( + spacing: 12, + + children: [ + // Scopes + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.scope, style: style), + Text( + widget.statistic.scopes + .map( + (scope) => translateScopeToString(scope, context), + ) + .join('\n'), + textAlign: TextAlign.end, + ), + ], + ), + + // Timeframe + if (widget.statistic.timeframe != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.timeframe, style: style), + Text( + translateTimeframeToString( + widget.statistic.timeframe!, + context, + ), + textAlign: TextAlign.end, + ), + ], + ), + + // Groups + if (widget.statistic.selectedGroups != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.groups, style: style), + Text( + widget.statistic.selectedGroups! + .map((group) => group.name) + .join('\n'), + textAlign: TextAlign.end, + ), + ], + ), + + // Games + if (widget.statistic.selectedGames != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.games, style: style), + Text( + widget.statistic.selectedGames! + .map((game) => game.name) + .join('\n'), + textAlign: TextAlign.end, + ), + ], + ), + + if (widget.values.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Display count', style: style), + Row( + children: [ + HapticIconButton( + icon: const Icon(Icons.remove), + onPressed: displayCount <= 1 + ? null + : () => setState(() => displayCount -= 1), + ), + SizedBox( + width: 30, + child: Text( + '$displayCount', + textAlign: TextAlign.center, + ), + ), + HapticIconButton( + icon: const Icon(Icons.add), + onPressed: displayCount >= widget.values.length + ? null + : () => setState(() => displayCount += 1), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart index fd43390..807358e 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart @@ -3,6 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/statistic.dart'; @@ -14,13 +16,18 @@ List _colorPalette = AppColor.values .map((c) => getColorFromAppColor(c)) .toList(); -/// Build the [StatisticsTile] for a given [Statistic]. -Widget buildStatisticTile({ +/// Returns the icon for the given statistic type. +IconData getStatisticIconForType(StatisticType type) => + _getStatisticIcon(type: type); + +/// Returns a color from the palette based on the statistic's ID. +Color getStatisticColorForStatistic(Statistic stat) => _getStatisticColor(stat); + +/// Computes the statistic values for a given [Statistic]. +List<(Player, num)> computeStatisticValues({ required Statistic statistic, required List matches, required List players, - required BuildContext context, - double? width, }) { final filteredMatches = _getFilterMatches(statistic, matches); final filteredPlayers = _getFilteredPlayers( @@ -29,16 +36,26 @@ Widget buildStatisticTile({ filteredMatches, ); - print('Building tile for statistic: $statistic'); - print('Filtered matches count: ${filteredMatches.length}'); - print('Filtered players count: ${filteredPlayers.length}'); - - final values = _computeValuesForType( + return _computeValuesForType( type: statistic.type, matches: filteredMatches, players: filteredPlayers, ); - print(values); +} + +/// Build the [StatisticsTile] for a given [Statistic]. +Widget buildStatisticTile({ + required Statistic statistic, + required List matches, + required List players, + required BuildContext context, + double? width, +}) { + final values = computeStatisticValues( + statistic: statistic, + matches: matches, + players: players, + ); return StatisticsTile( icon: _getStatisticIcon(type: statistic.type), @@ -46,7 +63,9 @@ Widget buildStatisticTile({ width: width ?? MediaQuery.sizeOf(context).width * 0.95, values: values, barColor: _getStatisticColor(statistic), - statistic: statistic, + displayCount: statistic.displayCount, + selectedGroups: statistic.selectedGroups, + selectedGames: statistic.selectedGames, ); } @@ -296,10 +315,8 @@ Widget buildSkeletonStatisticTile({required BuildContext context}) { width: MediaQuery.sizeOf(context).width * 0.95, values: values, barColor: _colorPalette[Random().nextInt(_colorPalette.length)], - statistic: Statistic( - type: StatisticType.totalMatches, - scopes: [StatisticScope.allPlayers], - timeframe: Timeframe.last7Days, - ), + selectedGames: [Game(name: 'Game 1', ruleset: Ruleset.highestScore)], + selectedGroups: [Group(name: 'Group 1', members: [])], + displayCount: 5, ); } diff --git a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart index 8c3ac16..d981b0a 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -8,9 +8,11 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_detail_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class StatisticsView extends StatefulWidget { /// A view that displays player statistics @@ -45,18 +47,30 @@ class _StatisticsViewState extends State { alignment: AlignmentDirectional.bottomCenter, fit: StackFit.expand, children: [ - SingleChildScrollView( - child: AppSkeleton( - enabled: isLoading, - fixLayoutBuilder: true, - child: Column( - spacing: 12, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...statisticTiles, - SizedBox(height: MediaQuery.paddingOf(context).bottom + 80), - ], + Visibility( + visible: statisticTiles.isNotEmpty, + replacement: Center( + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_statistics_created_yet, + ), + ), + child: SingleChildScrollView( + child: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...statisticTiles, + SizedBox( + height: MediaQuery.paddingOf(context).bottom + 80, + ), + ], + ), ), ), ), @@ -75,12 +89,7 @@ class _StatisticsViewState extends State { ), ); if (!context.mounted) return; - final newTile = buildStatisticTile( - statistic: newStatistic, - matches: _allMatches, - players: _allPlayers, - context: context, - ); + final newTile = _buildStatisticTile(context, newStatistic); setState(() { statisticTiles.add(newTile); @@ -126,15 +135,40 @@ class _StatisticsViewState extends State { setState(() { statisticTiles = [ for (final statistic in statistics) ...[ - buildStatisticTile( - statistic: statistic, - matches: _allMatches, - players: _allPlayers, - context: context, - ), + _buildStatisticTile(context, statistic), ], ]; isLoading = false; }); } + + Widget _buildStatisticTile(BuildContext context, Statistic statistic) { + return GestureDetector( + onTap: () { + final values = computeStatisticValues( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + ); + + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => StatisticDetailView( + statistic: statistic, + values: values, + icon: getStatisticIconForType(statistic.type), + barColor: getStatisticColorForStatistic(statistic), + ), + ), + ); + }, + child: buildStatisticTile( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + context: context, + ), + ); + } } diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 24d51fc..f6db2a1 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -8,7 +8,6 @@ import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; -import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; @@ -27,7 +26,9 @@ class StatisticsTile extends StatelessWidget { required this.width, required this.values, required this.barColor, - required this.statistic, + this.displayCount, + this.selectedGroups, + this.selectedGames, }); /// The icon displayed next to the title. @@ -45,7 +46,10 @@ class StatisticsTile extends StatelessWidget { /// The color of the bars representing the values. final Color barColor; - final Statistic statistic; + final int? displayCount; + + final List? selectedGroups; + final List? selectedGames; @override Widget build(BuildContext context) { @@ -70,8 +74,12 @@ class StatisticsTile extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final maxBarWidth = constraints.maxWidth * 0.8; - final displayCount = min(values.length, statistic.displayCount); - final displayValues = values.take(displayCount).toList(); + + // If displayCount wasnt provided, take all values + final valuesShown = displayCount == null + ? values.length + : min(values.length, displayCount!); + final displayValues = values.take(valuesShown).toList(); final maxVal = displayValues.isNotEmpty ? displayValues.fold( 0, @@ -83,7 +91,7 @@ class StatisticsTile extends StatelessWidget { return Column( children: [ // Bars - ...List.generate(displayCount, (index) { + ...List.generate(valuesShown, (index) { /// Fraction of wins final double fraction = (maxVal > 0) ? (displayValues[index].$2 / maxVal) @@ -187,12 +195,14 @@ class StatisticsTile extends StatelessWidget { }), // Group & Game info - if (statistic.selectedGames != null || - statistic.selectedGroups != null) + if (hasGame || hasGroup) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + child: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4, + runSpacing: 4, children: [ // Game if (hasGroup) @@ -205,7 +215,7 @@ class StatisticsTile extends StatelessWidget { size: 20, ), Text( - getGameText(statistic.selectedGames!), + getGameText(selectedGames!), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -227,7 +237,7 @@ class StatisticsTile extends StatelessWidget { color: CustomTheme.hintColor, ), Text( - getGroupText(statistic.selectedGroups!), + getGroupText(selectedGroups!), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -265,9 +275,7 @@ class StatisticsTile extends StatelessWidget { return text; } - bool get hasGroup => - statistic.selectedGroups != null && statistic.selectedGroups!.isNotEmpty; + bool get hasGroup => selectedGroups != null && selectedGroups!.isNotEmpty; - bool get hasGame => - statistic.selectedGames != null && statistic.selectedGames!.isNotEmpty; + bool get hasGame => selectedGames != null && selectedGames!.isNotEmpty; } diff --git a/pubspec.yaml b/pubspec.yaml index d6fd994..b8b0aa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.33+276 +version: 0.0.33+280 environment: sdk: ^3.8.1 From efd1097d5a78265fe933c5d973dafd231e6ce43c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 25 May 2026 12:51:24 +0200 Subject: [PATCH 30/34] feat: changing display count --- lib/data/models/statistic.dart | 19 +++++++ lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 +++ lib/l10n/generated/app_localizations_de.dart | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../statistic_detail_view.dart | 27 +++++++--- .../statistics_view/statistics_view.dart | 54 ++++++++++++------- pubspec.yaml | 2 +- 9 files changed, 88 insertions(+), 28 deletions(-) diff --git a/lib/data/models/statistic.dart b/lib/data/models/statistic.dart index 4fdbb05..e995d93 100644 --- a/lib/data/models/statistic.dart +++ b/lib/data/models/statistic.dart @@ -26,4 +26,23 @@ class Statistic { String toString() { return 'Statistic(id: $id, type: $type, scopes: $scopes, timeframe: $timeframe, selectedGroups: $selectedGroups, selectedGames: $selectedGames)'; } + + Statistic copyWith({ + StatisticType? type, + List? scopes, + Timeframe? timeframe, + List? selectedGroups, + List? selectedGames, + int? displayCount, + }) { + return Statistic( + id: id, + type: type ?? this.type, + scopes: scopes ?? this.scopes, + timeframe: timeframe ?? this.timeframe, + selectedGroups: selectedGroups ?? this.selectedGroups, + selectedGames: selectedGames ?? this.selectedGames, + displayCount: displayCount ?? this.displayCount, + ); + } } diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index b6ae56b..aef31dc 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -19,6 +19,7 @@ "color_red": "Rot", "color_teal": "Türkis", "color_yellow": "Gelb", + "displayed_entries": "Angezeigte Einträge", "confirm": "Bestätigen", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "@could_not_add_player": { diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index baf5006..902caad 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -18,6 +18,7 @@ "color_purple": "Purple", "color_red": "Red", "color_teal": "Teal", + "displayed_entries": "Displayed entries", "color_yellow": "Yellow", "confirm": "Confirm", "could_not_add_player": "Could not add player {playerName}", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 20067d4..37d0763 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -206,6 +206,12 @@ abstract class AppLocalizations { /// **'Teal'** String get color_teal; + /// No description provided for @displayed_entries. + /// + /// In en, this message translates to: + /// **'Displayed entries'** + String get displayed_entries; + /// No description provided for @color_yellow. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 8152185..9457988 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -62,6 +62,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get color_teal => 'Türkis'; + @override + String get displayed_entries => 'Angezeigte Einträge'; + @override String get color_yellow => 'Gelb'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 2366ce2..99ecca3 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -62,6 +62,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get color_teal => 'Teal'; + @override + String get displayed_entries => 'Displayed entries'; + @override String get color_yellow => 'Yellow'; diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart index 4053cb3..06d93a4 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart' - show - translateScopeToString, - translateStatisticTypeToString, - translateTimeframeToString; +import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; @@ -30,7 +28,7 @@ class StatisticDetailView extends StatefulWidget { } class _StatisticDetailViewState extends State { - int displayCount = 0; + late int displayCount; @override void initState() { @@ -48,7 +46,13 @@ class _StatisticDetailViewState extends State { const style = TextStyle(fontWeight: FontWeight.bold); return Scaffold( - appBar: AppBar(title: Text(title)), + appBar: AppBar( + title: Text(title), + leading: HapticIconButton( + icon: const Icon(Icons.arrow_back_ios_new), + onPressed: () => handleBack(context), + ), + ), body: SingleChildScrollView( padding: const EdgeInsets.all(12.0), child: Column( @@ -141,7 +145,7 @@ class _StatisticDetailViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('Display count', style: style), + Text(loc.displayed_entries, style: style), Row( children: [ HapticIconButton( @@ -175,4 +179,11 @@ class _StatisticDetailViewState extends State { ), ); } + + // Handles saving the display count and giving it to statistics view + Future handleBack(BuildContext context) async { + final db = Provider.of(context, listen: false); + await db.statisticDao.updateDisplayCount(widget.statistic.id, displayCount); + if (context.mounted) Navigator.of(context).pop(displayCount); + } } diff --git a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart index d981b0a..3381267 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -26,6 +26,7 @@ class _StatisticsViewState extends State { bool isLoading = true; List _allMatches = const []; List _allPlayers = const []; + List _statistics = const []; List statisticTiles = []; @override @@ -34,7 +35,7 @@ class _StatisticsViewState extends State { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - getStatisticTiles(context); + loadStatistics(context); }); } @@ -84,15 +85,16 @@ class _StatisticsViewState extends State { context, adaptivePageRoute( builder: (context) => CreateStatisticView( - onStatisticCreated: () => getStatisticTiles(context), + onStatisticCreated: () => loadStatistics(context), ), ), ); if (!context.mounted) return; - final newTile = _buildStatisticTile(context, newStatistic); - setState(() { - statisticTiles.add(newTile); + _statistics = [..._statistics, newStatistic]; + statisticTiles = _statistics + .map((stat) => _buildStatisticTile(context, stat)) + .toList(); }); }, ), @@ -103,7 +105,7 @@ class _StatisticsViewState extends State { ); } - Future getStatisticTiles(BuildContext context) async { + Future loadStatistics(BuildContext context) async { setState(() { isLoading = true; statisticTiles = List.generate( @@ -131,27 +133,26 @@ class _StatisticsViewState extends State { final statistics = results[0] as List; _allMatches = results[1] as List; _allPlayers = results[2] as List; + _statistics = statistics; setState(() { - statisticTiles = [ - for (final statistic in statistics) ...[ - _buildStatisticTile(context, statistic), - ], - ]; + statisticTiles = _statistics + .map((stat) => _buildStatisticTile(context, stat)) + .toList(); isLoading = false; }); } Widget _buildStatisticTile(BuildContext context, Statistic statistic) { - return GestureDetector( - onTap: () { - final values = computeStatisticValues( - statistic: statistic, - matches: _allMatches, - players: _allPlayers, - ); + final values = computeStatisticValues( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + ); - Navigator.push( + return GestureDetector( + onTap: () async { + final newDisplayCount = await Navigator.push( context, adaptivePageRoute( builder: (context) => StatisticDetailView( @@ -162,6 +163,21 @@ class _StatisticsViewState extends State { ), ), ); + if (newDisplayCount != null && + newDisplayCount != statistic.displayCount) { + setState(() { + _statistics = _statistics + .map( + (stat) => stat.id == statistic.id + ? stat.copyWith(displayCount: newDisplayCount) + : stat, + ) + .toList(); + statisticTiles = _statistics + .map((stat) => _buildStatisticTile(context, stat)) + .toList(); + }); + } }, child: buildStatisticTile( statistic: statistic, diff --git a/pubspec.yaml b/pubspec.yaml index b8b0aa8..6c5f4d6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.33+280 +version: 0.0.33+281 environment: sdk: ^3.8.1 From b9710ed851747004cd33a7925f63cb58cb0eebd2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 25 May 2026 12:55:36 +0200 Subject: [PATCH 31/34] fix: pixel overflow --- lib/presentation/widgets/game_label.dart | 56 +++++++++++++----------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart index dd179d6..2e6bf74 100644 --- a/lib/presentation/widgets/game_label.dart +++ b/lib/presentation/widgets/game_label.dart @@ -21,32 +21,35 @@ class GameLabel extends StatelessWidget { ? Colors.black : Colors.white; - return IntrinsicHeight( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - // Title - Container( - decoration: BoxDecoration( - color: backgroundColor.withAlpha(230), - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(8), - bottomLeft: Radius.circular(8), - ), - ), - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Text( - title, - style: TextStyle( - fontSize: 12, - color: fontColor, - fontWeight: FontWeight.bold, - ), + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + // Title + Container( + decoration: BoxDecoration( + color: backgroundColor.withAlpha(230), + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(8), + bottomLeft: Radius.circular(8), ), ), + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: TextStyle( + fontSize: 12, + color: fontColor, + fontWeight: FontWeight.bold, + ), + ), + ), - // Description - Container( + // Description + Flexible( + child: Container( decoration: BoxDecoration( color: backgroundColor.withAlpha(140), borderRadius: const BorderRadius.only( @@ -57,6 +60,9 @@ class GameLabel extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), child: Text( description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, style: TextStyle( fontSize: 12, color: fontColor, @@ -64,8 +70,8 @@ class GameLabel extends StatelessWidget { ), ), ), - ], - ), + ), + ], ); } } From fb2f6d3adc8a3c306b171fffff9fbb38bd8dd479 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 25 May 2026 13:06:11 +0200 Subject: [PATCH 32/34] feat: dynamic display count shown in tile --- .../statistic_detail_view.dart | 2 ++ .../widgets/tiles/statistics_tile.dart | 34 ++++++++++++------- test/db_tests/statistics/statistic_test.dart | 10 +++--- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart index 06d93a4..f1bc209 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart @@ -65,6 +65,8 @@ class _StatisticDetailViewState extends State { barColor: widget.barColor, selectedGroups: widget.statistic.selectedGroups, selectedGames: widget.statistic.selectedGames, + displayCount: displayCount, + showAllValues: true, ), const SizedBox(height: 12), diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index f6db2a1..02cdb75 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -26,9 +26,10 @@ class StatisticsTile extends StatelessWidget { required this.width, required this.values, required this.barColor, - this.displayCount, + required this.displayCount, this.selectedGroups, this.selectedGames, + this.showAllValues = false, }); /// The icon displayed next to the title. @@ -46,11 +47,13 @@ class StatisticsTile extends StatelessWidget { /// The color of the bars representing the values. final Color barColor; - final int? displayCount; - + // statistic data + final int displayCount; final List? selectedGroups; final List? selectedGames; + final bool showAllValues; + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); @@ -76,9 +79,9 @@ class StatisticsTile extends StatelessWidget { final maxBarWidth = constraints.maxWidth * 0.8; // If displayCount wasnt provided, take all values - final valuesShown = displayCount == null + final valuesShown = showAllValues ? values.length - : min(values.length, displayCount!); + : min(values.length, displayCount); final displayValues = values.take(valuesShown).toList(); final maxVal = displayValues.isNotEmpty ? displayValues.fold( @@ -103,6 +106,17 @@ class StatisticsTile extends StatelessWidget { maxBarWidth, ); + final barClr = index >= displayCount + ? barColor.withAlpha(150) + : barColor; + + var textClr = barColor.computeLuminance() > 0.5 + ? const Color(0xFF101010) + : CustomTheme.textColor; + textClr = textClr.withAlpha( + index >= displayCount ? 220 : 255, + ); + return Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Row( @@ -119,7 +133,7 @@ class StatisticsTile extends StatelessWidget { width: barWidth, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), - color: barColor, + color: barClr, ), ), @@ -138,13 +152,7 @@ class StatisticsTile extends StatelessWidget { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: - barColor == - getColorFromAppColor( - AppColor.yellow, - ) - ? const Color(0xFF101010) - : CustomTheme.textColor, + color: textClr, ), ), TextSpan( diff --git a/test/db_tests/statistics/statistic_test.dart b/test/db_tests/statistics/statistic_test.dart index cd08615..bd590c7 100644 --- a/test/db_tests/statistics/statistic_test.dart +++ b/test/db_tests/statistics/statistic_test.dart @@ -8,9 +8,7 @@ import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; -import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; -import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/statistic.dart'; void main() { @@ -23,10 +21,10 @@ void main() { late Group testGroup1; late Group testGroup2; late Game testGame; - late Match testMatch1; + /*late Match testMatch1; late Match testMatch2; late Match testMatchOnlyPlayers; - late Match testMatchOnlyGroup; + late Match testMatchOnlyGroup;*/ final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); @@ -62,7 +60,7 @@ void main() { color: AppColor.blue, icon: '', ); - testMatch1 = Match( + /*testMatch1 = Match( name: 'First Test Match', game: testGame, group: testGroup1, @@ -85,7 +83,7 @@ void main() { game: testGame, group: testGroup2, players: testGroup2.members, - ); + );*/ }); await database.playerDao.addPlayersAsList( players: [ From 730341dc7e5464b11fa14b427c110ec66b1c76ca Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 25 May 2026 13:17:02 +0200 Subject: [PATCH 33/34] fix: localizations --- lib/l10n/arb/app_de.arb | 36 +++++--- lib/l10n/arb/app_en.arb | 35 ++++---- lib/l10n/generated/app_localizations.dart | 90 +++++++------------ lib/l10n/generated/app_localizations_de.dart | 62 +++++-------- lib/l10n/generated/app_localizations_en.dart | 46 ++++------ .../create_statistic_view.dart | 42 ++++----- 6 files changed, 129 insertions(+), 182 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index aef31dc..94ec9ac 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -2,14 +2,18 @@ "@@locale": "de", "all_players": "Alle Spieler:innen", "all_players_selected": "Alle Spieler:innen ausgewählt", + "all_time": "Gesamter Zeitraum", "amount_of_matches": "Anzahl der Spiele", "app_name": "Tallee", + "average_score": "Durchschnittliche Punktzahl", "best_player": "Beste:r Spieler:in", + "best_score": "Beste Punktzahl", "cancel": "Abbrechen", "choose_color": "Farbe wählen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", "choose_ruleset": "Regelwerk wählen", + "classifier": "Klassifikator", "color": "Farbe", "color_blue": "Blau", "color_green": "Grün", @@ -19,7 +23,6 @@ "color_red": "Rot", "color_teal": "Türkis", "color_yellow": "Gelb", - "displayed_entries": "Angezeigte Einträge", "confirm": "Bestätigen", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "@could_not_add_player": { @@ -66,6 +69,7 @@ "delete_match": "Spiel löschen", "delete_player": "Spieler:in löschen", "description": "Beschreibung", + "displayed_entries": "Angezeigte Einträge", "drag_to_set_placement": "Ziehen um Platzierung zu setzen", "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", @@ -82,9 +86,11 @@ "exit_view": "Ansicht verlassen", "export_canceled": "Export abgebrochen", "export_data": "Daten exportieren", + "filter": "Filter", "format_exception": "Formatfehler (siehe Konsole)", "game": "Spielvorlage", "game_name": "Spielvorlagenname", + "games": "Spielvorlagen", "group": "Gruppe", "group_name": "Gruppenname", "group_profile": "Gruppenprofil", @@ -96,6 +102,11 @@ "import_data": "Daten importieren", "info": "Info", "invalid_schema": "Ungültiges Schema", + "last_180_days": "Letzte 180 Tage", + "last_30_days": "Letzte 30 Tage", + "last_7_days": "Letzte 7 Tage", + "last_90_days": "Letzte 90 Tage", + "last_year": "Letztes Jahr", "least_points": "Niedrigste Punkte", "legal": "Rechtliches", "legal_notice": "Impressum", @@ -128,6 +139,7 @@ "no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_second_match_available": "Kein zweites Spiel verfügbar", "no_statistics_available": "Keine Statistiken verfügbar", + "no_statistics_created_yet": "Noch keine Statistiken erstellt", "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", @@ -152,6 +164,7 @@ "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", "save_changes": "Änderungen speichern", + "scope": "Bereich", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_a_classifier": "Klassifikator auswählen", @@ -159,18 +172,19 @@ "select_a_group": "Gruppe auswählen", "select_a_scope": "Bereich auswählen", "select_a_timeframe": "Zeitraum auswählen", + "select_a_timeframe_for_which_data_will_be_filtered": "Wähle einen Zeitraum, für den die Daten gefiltert werden sollen", "select_loser": "Verlierer:in wählen", + "select_the_filtered_games": "Wähle die gefilterten Spielvorlagen", + "select_the_filtered_groups": "Wähle die gefilterten Gruppen", "select_winner": "Gewinner:in wählen", "select_winners": "Gewinner:innen wählen", + "selected_games": "Ausgewählte Spielvorlagen", + "selected_groups": "Ausgewählte Gruppen", "selected_players": "Ausgewählte Spieler:innen", "set_name": "Name setzen", "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", - "statistic_scope_all_players": "Alle Spieler:innen", - "statistic_scope_selected_games": "Ausgewählte Spielvorlagen", - "statistic_scope_selected_groups": "Ausgewählte Gruppen", - "statistic_scope_timeframe": "Zeitraum", "statistic_type_average_score": "Durchschnittliche Punktzahl", "statistic_type_best_score": "Beste Punktzahl", "statistic_type_total_losses": "Niederlagen insgesamt", @@ -186,18 +200,18 @@ "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", - "timeframe_all_time": "Gesamter Zeitraum", - "timeframe_last_180_days": "Letzte 180 Tage", - "timeframe_last_30_days": "Letzte 30 Tage", - "timeframe_last_7_days": "Letzte 7 Tage", - "timeframe_last_90_days": "Letzte 90 Tage", - "timeframe_last_year": "Letztes Jahr", + "timeframe": "Zeitraum", "today_at": "Heute um", + "total_losses": "Niederlagen insgesamt", + "total_matches": "Spiele insgesamt", + "total_score": "Punktzahl insgesamt", + "total_wins": "Siege insgesamt", "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", "winner": "Gewinner:in", "winners": "Gewinner:innen", "winrate": "Siegquote", "wins": "Siege", + "worst_score": "Schlechteste Punktzahl", "yesterday_at": "Gestern um" } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 902caad..427e955 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -35,12 +35,10 @@ "create_new_group": "Create new group", "create_new_match": "Create new match", "create_statistic": "Create statistic", - "which_key_metric": "Select which key metric you want to display", "classifier": "Classifier", "select_the_filtered_games": "Select the filtered games", "games": "Games", "select_the_filtered_groups": "Select the filtered groups", - "select_main_filter": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", "scope": "Scope", "select_a_timeframe_for_which_data_will_be_filtered": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", "timeframe": "Timeframe", @@ -169,18 +167,15 @@ "single_winner": "Single Winner", "statistics": "Statistics", "stats": "Stats", - "statistic_scope_all_players": "All players", - "statistic_scope_selected_games": "Selected games", - "statistic_scope_selected_groups": "Selected groups", - "statistic_scope_timeframe": "Timeframe", - "statistic_type_average_score": "Average score", - "statistic_type_best_score": "Best score", - "statistic_type_total_losses": "Total losses", - "statistic_type_total_matches": "Total matches", - "statistic_type_total_score": "Total score", - "statistic_type_total_wins": "Total wins", - "statistic_type_winrate": "Winrate", - "statistic_type_worst_score": "Worst score", + "selected_games": "Selected games", + "selected_groups": "Selected groups", + "average_score": "Average score", + "best_score": "Best score", + "total_losses": "Total losses", + "total_matches": "Total matches", + "total_score": "Total score", + "total_wins": "Total wins", + "worst_score": "Worst score", "successfully_added_player": "Successfully added player {playerName}", "@successfully_added_player": { "description": "Success message when adding a player", @@ -195,12 +190,12 @@ "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", - "timeframe_all_time": "All time", - "timeframe_last_180_days": "Last 180 days", - "timeframe_last_30_days": "Last 30 days", - "timeframe_last_7_days": "Last 7 days", - "timeframe_last_90_days": "Last 90 days", - "timeframe_last_year": "Last year", + "all_time": "All time", + "last_180_days": "Last 180 days", + "last_30_days": "Last 30 days", + "last_7_days": "Last 7 days", + "last_90_days": "Last 90 days", + "last_year": "Last year", "today_at": "Today at", "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 37d0763..41d1603 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -266,12 +266,6 @@ abstract class AppLocalizations { /// **'Create statistic'** String get create_statistic; - /// No description provided for @which_key_metric. - /// - /// In en, this message translates to: - /// **'Select which key metric you want to display'** - String get which_key_metric; - /// No description provided for @classifier. /// /// In en, this message translates to: @@ -296,12 +290,6 @@ abstract class AppLocalizations { /// **'Select the filtered groups'** String get select_the_filtered_groups; - /// No description provided for @select_main_filter. - /// - /// In en, this message translates to: - /// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'** - String get select_main_filter; - /// No description provided for @scope. /// /// In en, this message translates to: @@ -1028,77 +1016,59 @@ abstract class AppLocalizations { /// **'Stats'** String get stats; - /// No description provided for @statistic_scope_all_players. - /// - /// In en, this message translates to: - /// **'All players'** - String get statistic_scope_all_players; - - /// No description provided for @statistic_scope_selected_games. + /// No description provided for @selected_games. /// /// In en, this message translates to: /// **'Selected games'** - String get statistic_scope_selected_games; + String get selected_games; - /// No description provided for @statistic_scope_selected_groups. + /// No description provided for @selected_groups. /// /// In en, this message translates to: /// **'Selected groups'** - String get statistic_scope_selected_groups; + String get selected_groups; - /// No description provided for @statistic_scope_timeframe. - /// - /// In en, this message translates to: - /// **'Timeframe'** - String get statistic_scope_timeframe; - - /// No description provided for @statistic_type_average_score. + /// No description provided for @average_score. /// /// In en, this message translates to: /// **'Average score'** - String get statistic_type_average_score; + String get average_score; - /// No description provided for @statistic_type_best_score. + /// No description provided for @best_score. /// /// In en, this message translates to: /// **'Best score'** - String get statistic_type_best_score; + String get best_score; - /// No description provided for @statistic_type_total_losses. + /// No description provided for @total_losses. /// /// In en, this message translates to: /// **'Total losses'** - String get statistic_type_total_losses; + String get total_losses; - /// No description provided for @statistic_type_total_matches. + /// No description provided for @total_matches. /// /// In en, this message translates to: /// **'Total matches'** - String get statistic_type_total_matches; + String get total_matches; - /// No description provided for @statistic_type_total_score. + /// No description provided for @total_score. /// /// In en, this message translates to: /// **'Total score'** - String get statistic_type_total_score; + String get total_score; - /// No description provided for @statistic_type_total_wins. + /// No description provided for @total_wins. /// /// In en, this message translates to: /// **'Total wins'** - String get statistic_type_total_wins; + String get total_wins; - /// No description provided for @statistic_type_winrate. - /// - /// In en, this message translates to: - /// **'Winrate'** - String get statistic_type_winrate; - - /// No description provided for @statistic_type_worst_score. + /// No description provided for @worst_score. /// /// In en, this message translates to: /// **'Worst score'** - String get statistic_type_worst_score; + String get worst_score; /// Success message when adding a player /// @@ -1130,41 +1100,41 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// No description provided for @timeframe_all_time. + /// No description provided for @all_time. /// /// In en, this message translates to: /// **'All time'** - String get timeframe_all_time; + String get all_time; - /// No description provided for @timeframe_last_180_days. + /// No description provided for @last_180_days. /// /// In en, this message translates to: /// **'Last 180 days'** - String get timeframe_last_180_days; + String get last_180_days; - /// No description provided for @timeframe_last_30_days. + /// No description provided for @last_30_days. /// /// In en, this message translates to: /// **'Last 30 days'** - String get timeframe_last_30_days; + String get last_30_days; - /// No description provided for @timeframe_last_7_days. + /// No description provided for @last_7_days. /// /// In en, this message translates to: /// **'Last 7 days'** - String get timeframe_last_7_days; + String get last_7_days; - /// No description provided for @timeframe_last_90_days. + /// No description provided for @last_90_days. /// /// In en, this message translates to: /// **'Last 90 days'** - String get timeframe_last_90_days; + String get last_90_days; - /// No description provided for @timeframe_last_year. + /// No description provided for @last_year. /// /// In en, this message translates to: /// **'Last year'** - String get timeframe_last_year; + String get last_year; /// No description provided for @today_at. /// diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 9457988..35984a7 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -95,33 +95,26 @@ class AppLocalizationsDe extends AppLocalizations { String get create_statistic => 'Statistik erstellen'; @override - String get which_key_metric => 'Select which key metric you want to display'; + String get classifier => 'Klassifikator'; @override - String get classifier => 'Classifier'; + String get select_the_filtered_games => 'Wähle die gefilterten Spielvorlagen'; @override - String get select_the_filtered_games => 'Select the filtered games'; + String get games => 'Spielvorlagen'; @override - String get games => 'Games'; + String get select_the_filtered_groups => 'Wähle die gefilterten Gruppen'; @override - String get select_the_filtered_groups => 'Select the filtered groups'; - - @override - String get select_main_filter => - 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; - - @override - String get scope => 'Scope'; + String get scope => 'Bereich'; @override String get select_a_timeframe_for_which_data_will_be_filtered => - 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; + 'Wähle einen Zeitraum, für den die Daten gefiltert werden sollen'; @override - String get timeframe => 'Timeframe'; + String get timeframe => 'Zeitraum'; @override String get created_on => 'Erstellt am'; @@ -370,7 +363,7 @@ class AppLocalizationsDe extends AppLocalizations { String get no_statistics_available => 'Keine Statistiken verfügbar'; @override - String get no_statistics_created_yet => 'No statistics created yet'; + String get no_statistics_created_yet => 'Noch keine Statistiken erstellt'; @override String get none => 'Kein'; @@ -498,40 +491,31 @@ class AppLocalizationsDe extends AppLocalizations { String get stats => 'Statistiken'; @override - String get statistic_scope_all_players => 'Alle Spieler:innen'; + String get selected_games => 'Ausgewählte Spielvorlagen'; @override - String get statistic_scope_selected_games => 'Ausgewählte Spielvorlagen'; + String get selected_groups => 'Ausgewählte Gruppen'; @override - String get statistic_scope_selected_groups => 'Ausgewählte Gruppen'; + String get average_score => 'Durchschnittliche Punktzahl'; @override - String get statistic_scope_timeframe => 'Zeitraum'; + String get best_score => 'Beste Punktzahl'; @override - String get statistic_type_average_score => 'Durchschnittliche Punktzahl'; + String get total_losses => 'Niederlagen insgesamt'; @override - String get statistic_type_best_score => 'Beste Punktzahl'; + String get total_matches => 'Spiele insgesamt'; @override - String get statistic_type_total_losses => 'Niederlagen insgesamt'; + String get total_score => 'Punktzahl insgesamt'; @override - String get statistic_type_total_matches => 'Spiele insgesamt'; + String get total_wins => 'Siege insgesamt'; @override - String get statistic_type_total_score => 'Punktzahl insgesamt'; - - @override - String get statistic_type_total_wins => 'Siege insgesamt'; - - @override - String get statistic_type_winrate => 'Siegquote'; - - @override - String get statistic_type_worst_score => 'Schlechteste Punktzahl'; + String get worst_score => 'Schlechteste Punktzahl'; @override String successfully_added_player(String playerName) { @@ -554,22 +538,22 @@ class AppLocalizationsDe extends AppLocalizations { String get tie => 'Unentschieden'; @override - String get timeframe_all_time => 'Gesamter Zeitraum'; + String get all_time => 'Gesamter Zeitraum'; @override - String get timeframe_last_180_days => 'Letzte 180 Tage'; + String get last_180_days => 'Letzte 180 Tage'; @override - String get timeframe_last_30_days => 'Letzte 30 Tage'; + String get last_30_days => 'Letzte 30 Tage'; @override - String get timeframe_last_7_days => 'Letzte 7 Tage'; + String get last_7_days => 'Letzte 7 Tage'; @override - String get timeframe_last_90_days => 'Letzte 90 Tage'; + String get last_90_days => 'Letzte 90 Tage'; @override - String get timeframe_last_year => 'Letztes Jahr'; + String get last_year => 'Letztes Jahr'; @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 99ecca3..8f7236a 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -94,9 +94,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_statistic => 'Create statistic'; - @override - String get which_key_metric => 'Select which key metric you want to display'; - @override String get classifier => 'Classifier'; @@ -109,10 +106,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get select_the_filtered_groups => 'Select the filtered groups'; - @override - String get select_main_filter => - 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; - @override String get scope => 'Scope'; @@ -498,40 +491,31 @@ class AppLocalizationsEn extends AppLocalizations { String get stats => 'Stats'; @override - String get statistic_scope_all_players => 'All players'; + String get selected_games => 'Selected games'; @override - String get statistic_scope_selected_games => 'Selected games'; + String get selected_groups => 'Selected groups'; @override - String get statistic_scope_selected_groups => 'Selected groups'; + String get average_score => 'Average score'; @override - String get statistic_scope_timeframe => 'Timeframe'; + String get best_score => 'Best score'; @override - String get statistic_type_average_score => 'Average score'; + String get total_losses => 'Total losses'; @override - String get statistic_type_best_score => 'Best score'; + String get total_matches => 'Total matches'; @override - String get statistic_type_total_losses => 'Total losses'; + String get total_score => 'Total score'; @override - String get statistic_type_total_matches => 'Total matches'; + String get total_wins => 'Total wins'; @override - String get statistic_type_total_score => 'Total score'; - - @override - String get statistic_type_total_wins => 'Total wins'; - - @override - String get statistic_type_winrate => 'Winrate'; - - @override - String get statistic_type_worst_score => 'Worst score'; + String get worst_score => 'Worst score'; @override String successfully_added_player(String playerName) { @@ -553,22 +537,22 @@ class AppLocalizationsEn extends AppLocalizations { String get tie => 'Tie'; @override - String get timeframe_all_time => 'All time'; + String get all_time => 'All time'; @override - String get timeframe_last_180_days => 'Last 180 days'; + String get last_180_days => 'Last 180 days'; @override - String get timeframe_last_30_days => 'Last 30 days'; + String get last_30_days => 'Last 30 days'; @override - String get timeframe_last_7_days => 'Last 7 days'; + String get last_7_days => 'Last 7 days'; @override - String get timeframe_last_90_days => 'Last 90 days'; + String get last_90_days => 'Last 90 days'; @override - String get timeframe_last_year => 'Last year'; + String get last_year => 'Last year'; @override String get today_at => 'Today at'; diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart index 92a01bb..00542cf 100644 --- a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -76,10 +76,10 @@ class _CreateStatisticViewState extends State { fontWeight: FontWeight.bold, ), ), - Text( - loc.select_a_classifier, + const Text( + 'description', textAlign: TextAlign.start, - style: const TextStyle( + style: TextStyle( color: CustomTheme.textColor, fontSize: 12, ), @@ -581,17 +581,17 @@ String translateTimeframeToString(Timeframe timeframe, BuildContext context) { final loc = AppLocalizations.of(context); switch (timeframe) { case Timeframe.last7Days: - return loc.timeframe_last_7_days; + return loc.last_7_days; case Timeframe.last30Days: - return loc.timeframe_last_30_days; + return loc.last_30_days; case Timeframe.last90Days: - return loc.timeframe_last_90_days; + return loc.last_90_days; case Timeframe.last180Days: - return loc.timeframe_last_180_days; + return loc.last_180_days; case Timeframe.lastYear: - return loc.timeframe_last_year; + return loc.last_year; case Timeframe.allTime: - return loc.timeframe_all_time; + return loc.all_time; } } @@ -599,13 +599,13 @@ String translateScopeToString(StatisticScope scope, BuildContext context) { final loc = AppLocalizations.of(context); switch (scope) { case StatisticScope.allPlayers: - return loc.statistic_scope_all_players; + return loc.all_players; case StatisticScope.selectedGroups: - return loc.statistic_scope_selected_groups; + return loc.selected_groups; case StatisticScope.selectedGames: - return loc.statistic_scope_selected_games; + return loc.selected_games; case StatisticScope.timeframe: - return loc.statistic_scope_timeframe; + return loc.timeframe; } } @@ -616,20 +616,20 @@ String translateStatisticTypeToString( final loc = AppLocalizations.of(context); switch (type) { case StatisticType.totalMatches: - return loc.statistic_type_total_matches; + return loc.total_matches; case StatisticType.totalWins: - return loc.statistic_type_total_wins; + return loc.total_wins; case StatisticType.totalScore: - return loc.statistic_type_total_score; + return loc.total_score; case StatisticType.totalLosses: - return loc.statistic_type_total_losses; + return loc.total_losses; case StatisticType.averageScore: - return loc.statistic_type_average_score; + return loc.average_score; case StatisticType.bestScore: - return loc.statistic_type_best_score; + return loc.best_score; case StatisticType.worstScore: - return loc.statistic_type_worst_score; + return loc.worst_score; case StatisticType.winrate: - return loc.statistic_type_winrate; + return loc.winrate; } } From 4bd2f972df26cdd036f33a4064900e24a8b3b247 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 25 May 2026 13:17:02 +0200 Subject: [PATCH 34/34] fix: localizations --- lib/l10n/arb/app_de.arb | 36 ++++-- lib/l10n/arb/app_en.arb | 41 +++---- lib/l10n/generated/app_localizations.dart | 106 +++++++----------- lib/l10n/generated/app_localizations_de.dart | 66 +++++------ lib/l10n/generated/app_localizations_en.dart | 60 ++++------ .../create_statistic_view.dart | 50 ++++----- 6 files changed, 155 insertions(+), 204 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index aef31dc..4eb5fd1 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -2,14 +2,18 @@ "@@locale": "de", "all_players": "Alle Spieler:innen", "all_players_selected": "Alle Spieler:innen ausgewählt", + "all_time": "Gesamter Zeitraum", "amount_of_matches": "Anzahl der Spiele", "app_name": "Tallee", + "average_score": "Durchschnittliche Punktzahl", "best_player": "Beste:r Spieler:in", + "best_score": "Beste Punktzahl", "cancel": "Abbrechen", "choose_color": "Farbe wählen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", "choose_ruleset": "Regelwerk wählen", + "classifier": "Klassifikator", "color": "Farbe", "color_blue": "Blau", "color_green": "Grün", @@ -19,7 +23,6 @@ "color_red": "Rot", "color_teal": "Türkis", "color_yellow": "Gelb", - "displayed_entries": "Angezeigte Einträge", "confirm": "Bestätigen", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "@could_not_add_player": { @@ -66,6 +69,7 @@ "delete_match": "Spiel löschen", "delete_player": "Spieler:in löschen", "description": "Beschreibung", + "displayed_entries": "Angezeigte Einträge", "drag_to_set_placement": "Ziehen um Platzierung zu setzen", "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", @@ -82,9 +86,11 @@ "exit_view": "Ansicht verlassen", "export_canceled": "Export abgebrochen", "export_data": "Daten exportieren", + "filter": "Filter", "format_exception": "Formatfehler (siehe Konsole)", "game": "Spielvorlage", "game_name": "Spielvorlagenname", + "games": "Spielvorlagen", "group": "Gruppe", "group_name": "Gruppenname", "group_profile": "Gruppenprofil", @@ -96,6 +102,11 @@ "import_data": "Daten importieren", "info": "Info", "invalid_schema": "Ungültiges Schema", + "last_180_days": "Letzte 180 Tage", + "last_30_days": "Letzte 30 Tage", + "last_7_days": "Letzte 7 Tage", + "last_90_days": "Letzte 90 Tage", + "last_year": "Letztes Jahr", "least_points": "Niedrigste Punkte", "legal": "Rechtliches", "legal_notice": "Impressum", @@ -128,6 +139,7 @@ "no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_second_match_available": "Kein zweites Spiel verfügbar", "no_statistics_available": "Keine Statistiken verfügbar", + "no_statistics_created_yet": "Noch keine Statistiken erstellt", "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", @@ -152,6 +164,7 @@ "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", "save_changes": "Änderungen speichern", + "scope": "Bereich", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_a_classifier": "Klassifikator auswählen", @@ -160,17 +173,18 @@ "select_a_scope": "Bereich auswählen", "select_a_timeframe": "Zeitraum auswählen", "select_loser": "Verlierer:in wählen", + "select_the_filtered_games": "Wähle Spiele, nach denen gefiltert werden soll.", + "select_the_filtered_groups": "Wähle Gruppen, nach denen gefiltert werden soll.", + "select_the_filtered_timeframe": "Wähle einen Zeitraum, nach dem gefiltert werden soll.", "select_winner": "Gewinner:in wählen", "select_winners": "Gewinner:innen wählen", + "selected_games": "Ausgewählte Spielvorlagen", + "selected_groups": "Ausgewählte Gruppen", "selected_players": "Ausgewählte Spieler:innen", "set_name": "Name setzen", "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", - "statistic_scope_all_players": "Alle Spieler:innen", - "statistic_scope_selected_games": "Ausgewählte Spielvorlagen", - "statistic_scope_selected_groups": "Ausgewählte Gruppen", - "statistic_scope_timeframe": "Zeitraum", "statistic_type_average_score": "Durchschnittliche Punktzahl", "statistic_type_best_score": "Beste Punktzahl", "statistic_type_total_losses": "Niederlagen insgesamt", @@ -186,18 +200,18 @@ "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "tie": "Unentschieden", - "timeframe_all_time": "Gesamter Zeitraum", - "timeframe_last_180_days": "Letzte 180 Tage", - "timeframe_last_30_days": "Letzte 30 Tage", - "timeframe_last_7_days": "Letzte 7 Tage", - "timeframe_last_90_days": "Letzte 90 Tage", - "timeframe_last_year": "Letztes Jahr", + "timeframe": "Zeitraum", "today_at": "Heute um", + "total_losses": "Niederlagen insgesamt", + "total_matches": "Spiele insgesamt", + "total_score": "Punktzahl insgesamt", + "total_wins": "Siege insgesamt", "undo": "Rückgängig", "unknown_exception": "Unbekannter Fehler (siehe Konsole)", "winner": "Gewinner:in", "winners": "Gewinner:innen", "winrate": "Siegquote", "wins": "Siege", + "worst_score": "Schlechteste Punktzahl", "yesterday_at": "Gestern um" } \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 902caad..988433b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -35,14 +35,12 @@ "create_new_group": "Create new group", "create_new_match": "Create new match", "create_statistic": "Create statistic", - "which_key_metric": "Select which key metric you want to display", "classifier": "Classifier", - "select_the_filtered_games": "Select the filtered games", + "select_the_filtered_timeframe": "Select the timeframe you want to filter by.", + "select_the_filtered_games": "Select the games you want to filter by.", "games": "Games", - "select_the_filtered_groups": "Select the filtered groups", - "select_main_filter": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", + "select_the_filtered_groups": "Select the groups you want to filter by.", "scope": "Scope", - "select_a_timeframe_for_which_data_will_be_filtered": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", "timeframe": "Timeframe", "created_on": "Created on", "data": "Data", @@ -169,18 +167,15 @@ "single_winner": "Single Winner", "statistics": "Statistics", "stats": "Stats", - "statistic_scope_all_players": "All players", - "statistic_scope_selected_games": "Selected games", - "statistic_scope_selected_groups": "Selected groups", - "statistic_scope_timeframe": "Timeframe", - "statistic_type_average_score": "Average score", - "statistic_type_best_score": "Best score", - "statistic_type_total_losses": "Total losses", - "statistic_type_total_matches": "Total matches", - "statistic_type_total_score": "Total score", - "statistic_type_total_wins": "Total wins", - "statistic_type_winrate": "Winrate", - "statistic_type_worst_score": "Worst score", + "selected_games": "Selected games", + "selected_groups": "Selected groups", + "average_score": "Average score", + "best_score": "Best score", + "total_losses": "Total losses", + "total_matches": "Total matches", + "total_score": "Total score", + "total_wins": "Total wins", + "worst_score": "Worst score", "successfully_added_player": "Successfully added player {playerName}", "@successfully_added_player": { "description": "Success message when adding a player", @@ -195,12 +190,12 @@ "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", "tie": "Tie", - "timeframe_all_time": "All time", - "timeframe_last_180_days": "Last 180 days", - "timeframe_last_30_days": "Last 30 days", - "timeframe_last_7_days": "Last 7 days", - "timeframe_last_90_days": "Last 90 days", - "timeframe_last_year": "Last year", + "all_time": "All time", + "last_180_days": "Last 180 days", + "last_30_days": "Last 30 days", + "last_7_days": "Last 7 days", + "last_90_days": "Last 90 days", + "last_year": "Last year", "today_at": "Today at", "undo": "Undo", "unknown_exception": "Unknown Exception (see console)", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 37d0763..30e1b33 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -266,22 +266,22 @@ abstract class AppLocalizations { /// **'Create statistic'** String get create_statistic; - /// No description provided for @which_key_metric. - /// - /// In en, this message translates to: - /// **'Select which key metric you want to display'** - String get which_key_metric; - /// No description provided for @classifier. /// /// In en, this message translates to: /// **'Classifier'** String get classifier; + /// No description provided for @select_the_filtered_timeframe. + /// + /// In en, this message translates to: + /// **'Select the timeframe you want to filter by.'** + String get select_the_filtered_timeframe; + /// No description provided for @select_the_filtered_games. /// /// In en, this message translates to: - /// **'Select the filtered games'** + /// **'Select the games you want to filter by.'** String get select_the_filtered_games; /// No description provided for @games. @@ -293,27 +293,15 @@ abstract class AppLocalizations { /// No description provided for @select_the_filtered_groups. /// /// In en, this message translates to: - /// **'Select the filtered groups'** + /// **'Select the groups you want to filter by.'** String get select_the_filtered_groups; - /// No description provided for @select_main_filter. - /// - /// In en, this message translates to: - /// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'** - String get select_main_filter; - /// No description provided for @scope. /// /// In en, this message translates to: /// **'Scope'** String get scope; - /// No description provided for @select_a_timeframe_for_which_data_will_be_filtered. - /// - /// In en, this message translates to: - /// **'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'** - String get select_a_timeframe_for_which_data_will_be_filtered; - /// No description provided for @timeframe. /// /// In en, this message translates to: @@ -1028,77 +1016,59 @@ abstract class AppLocalizations { /// **'Stats'** String get stats; - /// No description provided for @statistic_scope_all_players. - /// - /// In en, this message translates to: - /// **'All players'** - String get statistic_scope_all_players; - - /// No description provided for @statistic_scope_selected_games. + /// No description provided for @selected_games. /// /// In en, this message translates to: /// **'Selected games'** - String get statistic_scope_selected_games; + String get selected_games; - /// No description provided for @statistic_scope_selected_groups. + /// No description provided for @selected_groups. /// /// In en, this message translates to: /// **'Selected groups'** - String get statistic_scope_selected_groups; + String get selected_groups; - /// No description provided for @statistic_scope_timeframe. - /// - /// In en, this message translates to: - /// **'Timeframe'** - String get statistic_scope_timeframe; - - /// No description provided for @statistic_type_average_score. + /// No description provided for @average_score. /// /// In en, this message translates to: /// **'Average score'** - String get statistic_type_average_score; + String get average_score; - /// No description provided for @statistic_type_best_score. + /// No description provided for @best_score. /// /// In en, this message translates to: /// **'Best score'** - String get statistic_type_best_score; + String get best_score; - /// No description provided for @statistic_type_total_losses. + /// No description provided for @total_losses. /// /// In en, this message translates to: /// **'Total losses'** - String get statistic_type_total_losses; + String get total_losses; - /// No description provided for @statistic_type_total_matches. + /// No description provided for @total_matches. /// /// In en, this message translates to: /// **'Total matches'** - String get statistic_type_total_matches; + String get total_matches; - /// No description provided for @statistic_type_total_score. + /// No description provided for @total_score. /// /// In en, this message translates to: /// **'Total score'** - String get statistic_type_total_score; + String get total_score; - /// No description provided for @statistic_type_total_wins. + /// No description provided for @total_wins. /// /// In en, this message translates to: /// **'Total wins'** - String get statistic_type_total_wins; + String get total_wins; - /// No description provided for @statistic_type_winrate. - /// - /// In en, this message translates to: - /// **'Winrate'** - String get statistic_type_winrate; - - /// No description provided for @statistic_type_worst_score. + /// No description provided for @worst_score. /// /// In en, this message translates to: /// **'Worst score'** - String get statistic_type_worst_score; + String get worst_score; /// Success message when adding a player /// @@ -1130,41 +1100,41 @@ abstract class AppLocalizations { /// **'Tie'** String get tie; - /// No description provided for @timeframe_all_time. + /// No description provided for @all_time. /// /// In en, this message translates to: /// **'All time'** - String get timeframe_all_time; + String get all_time; - /// No description provided for @timeframe_last_180_days. + /// No description provided for @last_180_days. /// /// In en, this message translates to: /// **'Last 180 days'** - String get timeframe_last_180_days; + String get last_180_days; - /// No description provided for @timeframe_last_30_days. + /// No description provided for @last_30_days. /// /// In en, this message translates to: /// **'Last 30 days'** - String get timeframe_last_30_days; + String get last_30_days; - /// No description provided for @timeframe_last_7_days. + /// No description provided for @last_7_days. /// /// In en, this message translates to: /// **'Last 7 days'** - String get timeframe_last_7_days; + String get last_7_days; - /// No description provided for @timeframe_last_90_days. + /// No description provided for @last_90_days. /// /// In en, this message translates to: /// **'Last 90 days'** - String get timeframe_last_90_days; + String get last_90_days; - /// No description provided for @timeframe_last_year. + /// No description provided for @last_year. /// /// In en, this message translates to: /// **'Last year'** - String get timeframe_last_year; + String get last_year; /// No description provided for @today_at. /// diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 9457988..1417bb7 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -95,33 +95,28 @@ class AppLocalizationsDe extends AppLocalizations { String get create_statistic => 'Statistik erstellen'; @override - String get which_key_metric => 'Select which key metric you want to display'; + String get classifier => 'Klassifikator'; @override - String get classifier => 'Classifier'; + String get select_the_filtered_timeframe => + 'Wähle einen Zeitraum, nach dem gefiltert werden soll.'; @override - String get select_the_filtered_games => 'Select the filtered games'; + String get select_the_filtered_games => + 'Wähle Spiele, nach denen gefiltert werden soll.'; @override - String get games => 'Games'; + String get games => 'Spielvorlagen'; @override - String get select_the_filtered_groups => 'Select the filtered groups'; + String get select_the_filtered_groups => + 'Wähle Gruppen, nach denen gefiltert werden soll.'; @override - String get select_main_filter => - 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; + String get scope => 'Bereich'; @override - String get scope => 'Scope'; - - @override - String get select_a_timeframe_for_which_data_will_be_filtered => - 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; - - @override - String get timeframe => 'Timeframe'; + String get timeframe => 'Zeitraum'; @override String get created_on => 'Erstellt am'; @@ -370,7 +365,7 @@ class AppLocalizationsDe extends AppLocalizations { String get no_statistics_available => 'Keine Statistiken verfügbar'; @override - String get no_statistics_created_yet => 'No statistics created yet'; + String get no_statistics_created_yet => 'Noch keine Statistiken erstellt'; @override String get none => 'Kein'; @@ -498,40 +493,31 @@ class AppLocalizationsDe extends AppLocalizations { String get stats => 'Statistiken'; @override - String get statistic_scope_all_players => 'Alle Spieler:innen'; + String get selected_games => 'Ausgewählte Spielvorlagen'; @override - String get statistic_scope_selected_games => 'Ausgewählte Spielvorlagen'; + String get selected_groups => 'Ausgewählte Gruppen'; @override - String get statistic_scope_selected_groups => 'Ausgewählte Gruppen'; + String get average_score => 'Durchschnittliche Punktzahl'; @override - String get statistic_scope_timeframe => 'Zeitraum'; + String get best_score => 'Beste Punktzahl'; @override - String get statistic_type_average_score => 'Durchschnittliche Punktzahl'; + String get total_losses => 'Niederlagen insgesamt'; @override - String get statistic_type_best_score => 'Beste Punktzahl'; + String get total_matches => 'Spiele insgesamt'; @override - String get statistic_type_total_losses => 'Niederlagen insgesamt'; + String get total_score => 'Punktzahl insgesamt'; @override - String get statistic_type_total_matches => 'Spiele insgesamt'; + String get total_wins => 'Siege insgesamt'; @override - String get statistic_type_total_score => 'Punktzahl insgesamt'; - - @override - String get statistic_type_total_wins => 'Siege insgesamt'; - - @override - String get statistic_type_winrate => 'Siegquote'; - - @override - String get statistic_type_worst_score => 'Schlechteste Punktzahl'; + String get worst_score => 'Schlechteste Punktzahl'; @override String successfully_added_player(String playerName) { @@ -554,22 +540,22 @@ class AppLocalizationsDe extends AppLocalizations { String get tie => 'Unentschieden'; @override - String get timeframe_all_time => 'Gesamter Zeitraum'; + String get all_time => 'Gesamter Zeitraum'; @override - String get timeframe_last_180_days => 'Letzte 180 Tage'; + String get last_180_days => 'Letzte 180 Tage'; @override - String get timeframe_last_30_days => 'Letzte 30 Tage'; + String get last_30_days => 'Letzte 30 Tage'; @override - String get timeframe_last_7_days => 'Letzte 7 Tage'; + String get last_7_days => 'Letzte 7 Tage'; @override - String get timeframe_last_90_days => 'Letzte 90 Tage'; + String get last_90_days => 'Letzte 90 Tage'; @override - String get timeframe_last_year => 'Letztes Jahr'; + String get last_year => 'Letztes Jahr'; @override String get today_at => 'Heute um'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 99ecca3..2275b97 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -94,32 +94,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_statistic => 'Create statistic'; - @override - String get which_key_metric => 'Select which key metric you want to display'; - @override String get classifier => 'Classifier'; @override - String get select_the_filtered_games => 'Select the filtered games'; + String get select_the_filtered_timeframe => + 'Select the timeframe you want to filter by.'; + + @override + String get select_the_filtered_games => + 'Select the games you want to filter by.'; @override String get games => 'Games'; @override - String get select_the_filtered_groups => 'Select the filtered groups'; - - @override - String get select_main_filter => - 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; + String get select_the_filtered_groups => + 'Select the groups you want to filter by.'; @override String get scope => 'Scope'; - @override - String get select_a_timeframe_for_which_data_will_be_filtered => - 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; - @override String get timeframe => 'Timeframe'; @@ -498,40 +493,31 @@ class AppLocalizationsEn extends AppLocalizations { String get stats => 'Stats'; @override - String get statistic_scope_all_players => 'All players'; + String get selected_games => 'Selected games'; @override - String get statistic_scope_selected_games => 'Selected games'; + String get selected_groups => 'Selected groups'; @override - String get statistic_scope_selected_groups => 'Selected groups'; + String get average_score => 'Average score'; @override - String get statistic_scope_timeframe => 'Timeframe'; + String get best_score => 'Best score'; @override - String get statistic_type_average_score => 'Average score'; + String get total_losses => 'Total losses'; @override - String get statistic_type_best_score => 'Best score'; + String get total_matches => 'Total matches'; @override - String get statistic_type_total_losses => 'Total losses'; + String get total_score => 'Total score'; @override - String get statistic_type_total_matches => 'Total matches'; + String get total_wins => 'Total wins'; @override - String get statistic_type_total_score => 'Total score'; - - @override - String get statistic_type_total_wins => 'Total wins'; - - @override - String get statistic_type_winrate => 'Winrate'; - - @override - String get statistic_type_worst_score => 'Worst score'; + String get worst_score => 'Worst score'; @override String successfully_added_player(String playerName) { @@ -553,22 +539,22 @@ class AppLocalizationsEn extends AppLocalizations { String get tie => 'Tie'; @override - String get timeframe_all_time => 'All time'; + String get all_time => 'All time'; @override - String get timeframe_last_180_days => 'Last 180 days'; + String get last_180_days => 'Last 180 days'; @override - String get timeframe_last_30_days => 'Last 30 days'; + String get last_30_days => 'Last 30 days'; @override - String get timeframe_last_7_days => 'Last 7 days'; + String get last_7_days => 'Last 7 days'; @override - String get timeframe_last_90_days => 'Last 90 days'; + String get last_90_days => 'Last 90 days'; @override - String get timeframe_last_year => 'Last year'; + String get last_year => 'Last year'; @override String get today_at => 'Today at'; diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart index 92a01bb..a88432a 100644 --- a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -76,10 +76,10 @@ class _CreateStatisticViewState extends State { fontWeight: FontWeight.bold, ), ), - Text( - loc.select_a_classifier, + const Text( + 'description', textAlign: TextAlign.start, - style: const TextStyle( + style: TextStyle( color: CustomTheme.textColor, fontSize: 12, ), @@ -147,10 +147,10 @@ class _CreateStatisticViewState extends State { fontWeight: FontWeight.bold, ), ), - Text( - loc.select_a_scope, + const Text( + 'description', textAlign: TextAlign.start, - style: const TextStyle( + style: TextStyle( color: CustomTheme.textColor, fontSize: 12, ), @@ -405,7 +405,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.select_a_timeframe_for_which_data_will_be_filtered, + loc.select_the_filtered_timeframe, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -581,17 +581,17 @@ String translateTimeframeToString(Timeframe timeframe, BuildContext context) { final loc = AppLocalizations.of(context); switch (timeframe) { case Timeframe.last7Days: - return loc.timeframe_last_7_days; + return loc.last_7_days; case Timeframe.last30Days: - return loc.timeframe_last_30_days; + return loc.last_30_days; case Timeframe.last90Days: - return loc.timeframe_last_90_days; + return loc.last_90_days; case Timeframe.last180Days: - return loc.timeframe_last_180_days; + return loc.last_180_days; case Timeframe.lastYear: - return loc.timeframe_last_year; + return loc.last_year; case Timeframe.allTime: - return loc.timeframe_all_time; + return loc.all_time; } } @@ -599,13 +599,13 @@ String translateScopeToString(StatisticScope scope, BuildContext context) { final loc = AppLocalizations.of(context); switch (scope) { case StatisticScope.allPlayers: - return loc.statistic_scope_all_players; + return loc.all_players; case StatisticScope.selectedGroups: - return loc.statistic_scope_selected_groups; + return loc.selected_groups; case StatisticScope.selectedGames: - return loc.statistic_scope_selected_games; + return loc.selected_games; case StatisticScope.timeframe: - return loc.statistic_scope_timeframe; + return loc.timeframe; } } @@ -616,20 +616,20 @@ String translateStatisticTypeToString( final loc = AppLocalizations.of(context); switch (type) { case StatisticType.totalMatches: - return loc.statistic_type_total_matches; + return loc.total_matches; case StatisticType.totalWins: - return loc.statistic_type_total_wins; + return loc.total_wins; case StatisticType.totalScore: - return loc.statistic_type_total_score; + return loc.total_score; case StatisticType.totalLosses: - return loc.statistic_type_total_losses; + return loc.total_losses; case StatisticType.averageScore: - return loc.statistic_type_average_score; + return loc.average_score; case StatisticType.bestScore: - return loc.statistic_type_best_score; + return loc.best_score; case StatisticType.worstScore: - return loc.statistic_type_worst_score; + return loc.worst_score; case StatisticType.winrate: - return loc.statistic_type_winrate; + return loc.winrate; } }