From b305145d34cea6fb461971cd76ae90ba25671634 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Wed, 20 May 2026 15:15:47 +0200 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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