From a1398623b0b8188afa8d773a2970b3d18adacd4d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 22:49:06 +0200 Subject: [PATCH 1/8] Added database functionality + tests --- lib/data/dao/player_dao.dart | 172 ++++++++++++++-- lib/data/db/database.g.dart | 249 +++++++++++++++--------- lib/data/db/tables/player_table.dart | 5 +- lib/data/models/player.dart | 3 + test/db_tests/entities/player_test.dart | 159 ++++++++++++++- 5 files changed, 484 insertions(+), 104 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index c58cb9a..74b2960 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:flutter/cupertino.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/player_table.dart'; import 'package:tallee/data/models/player.dart'; @@ -20,6 +21,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { name: row.name, description: row.description, createdAt: row.createdAt, + nameCount: row.nameCount, ), ) .toList(); @@ -34,6 +36,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { name: result.name, description: result.description, createdAt: result.createdAt, + nameCount: result.nameCount, ); } @@ -42,12 +45,15 @@ 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); + await into(playerTable).insert( PlayerTableCompanion.insert( id: player.id, name: player.name, description: player.description, createdAt: player.createdAt, + nameCount: Value(nameCount), ), mode: InsertMode.insertOrReplace, ); @@ -62,20 +68,67 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { Future addPlayersAsList({required List players}) async { if (players.isEmpty) return false; + // Filter out players that already exist + final newPlayers = []; + for (final player in players) { + if (!await playerExists(playerId: player.id)) { + newPlayers.add(player); + } + } + + if (newPlayers.isEmpty) return false; + + // Group players by name + final nameGroups = >{}; + for (final player in newPlayers) { + nameGroups.putIfAbsent(player.name, () => []).add(player); + } + + final playersToInsert = []; + + // Process each group of players with the same name + for (final entry in nameGroups.entries) { + final name = entry.key; + final playersWithName = entry.value; + + // Get the current nameCount + var nameCount = await calculateNameCount(name: name); + if (nameCount == 0) nameCount++; + + // One player with the same name + if (playersWithName.length == 1) { + final player = playersWithName[0]; + playersToInsert.add( + PlayerTableCompanion.insert( + id: player.id, + name: player.name, + description: player.description, + createdAt: player.createdAt, + nameCount: Value(nameCount), + ), + ); + } else { + // Multiple players with the same name + for (var i = 0; i < playersWithName.length; i++) { + final player = playersWithName[i]; + playersToInsert.add( + PlayerTableCompanion.insert( + id: player.id, + name: player.name, + description: player.description, + createdAt: player.createdAt, + nameCount: Value(nameCount + i), + ), + ); + } + } + } + await db.batch( (b) => b.insertAll( playerTable, - players - .map( - (player) => PlayerTableCompanion.insert( - id: player.id, - name: player.name, - description: player.description, - createdAt: player.createdAt, - ), - ) - .toList(), - mode: InsertMode.insertOrIgnore, + playersToInsert, + mode: InsertMode.insertOrReplace, ), ); @@ -103,9 +156,36 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { required String playerId, required String newName, }) 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).getSingle(); + final previousNameCount = await getNameCount(name: previousPlayerName); + await (update(playerTable)..where((p) => p.id.equals(playerId))).write( PlayerTableCompanion(name: Value(newName)), ); + + // Update name count for the new name + final count = await calculateNameCount(name: newName); + if (count > 0) { + await (update(playerTable)..where((p) => p.name.equals(newName))).write( + PlayerTableCompanion(nameCount: Value(count)), + ); + } + + if (previousNameCount > 0) { + // Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount + final player = await getPlayerWithHighestNameCount( + name: previousPlayerName, + ); + if (player != null) { + await updateNameCount( + playerId: player.id, + nameCount: previousNameCount, + ); + } + } } /// Retrieves the total count of players in the database. @@ -117,6 +197,76 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return count ?? 0; } + /// Retrieves the count of players with the given [name] in the database. + Future getNameCount({required String name}) async { + final query = select(playerTable)..where((p) => p.name.equals(name)); + final result = await query.get(); + return result.length; + } + + /// Updates the nameCount for the player with the given [playerId] to [nameCount]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateNameCount({ + required String playerId, + required int nameCount, + }) async { + final query = update(playerTable)..where((p) => p.id.equals(playerId)); + final rowsAffected = await query.write( + PlayerTableCompanion(nameCount: Value(nameCount)), + ); + return rowsAffected > 0; + } + + @visibleForTesting + Future getPlayerWithHighestNameCount({required String name}) async { + final query = select(playerTable) + ..where((p) => p.name.equals(name)) + ..orderBy([(p) => OrderingTerm.desc(p.nameCount)]) + ..limit(1); + final result = await query.getSingleOrNull(); + if (result != null) { + return Player( + id: result.id, + name: result.name, + description: result.description, + createdAt: result.createdAt, + nameCount: result.nameCount, + ); + } + return null; + } + + @visibleForTesting + Future calculateNameCount({required String name}) async { + final count = await getNameCount(name: name); + final int nameCount; + + if (count == 1) { + // 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) { + // 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; + } + + @visibleForTesting + Future initializeNameCount({required String name}) async { + final rowsAffected = + await (update(playerTable)..where((p) => p.name.equals(name))).write( + const PlayerTableCompanion(nameCount: Value(1)), + ); + return rowsAffected > 0; + } + /// Deletes all players from the database. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future deleteAllPlayers() async { diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 58175df..2190c3d 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -18,6 +18,17 @@ class $PlayerTableTable extends PlayerTable type: DriftSqlType.string, requiredDuringInsert: true, ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); static const VerificationMeta _nameMeta = const VerificationMeta('name'); @override late final GeneratedColumn name = GeneratedColumn( @@ -27,6 +38,18 @@ class $PlayerTableTable extends PlayerTable type: DriftSqlType.string, requiredDuringInsert: true, ); + static const VerificationMeta _nameCountMeta = const VerificationMeta( + 'nameCount', + ); + @override + late final GeneratedColumn nameCount = GeneratedColumn( + 'name_count', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); static const VerificationMeta _descriptionMeta = const VerificationMeta( 'description', ); @@ -38,19 +61,14 @@ class $PlayerTableTable extends PlayerTable type: DriftSqlType.string, requiredDuringInsert: true, ); - static const VerificationMeta _createdAtMeta = const VerificationMeta( - 'createdAt', - ); @override - late final GeneratedColumn createdAt = GeneratedColumn( - 'created_at', - aliasedName, - false, - type: DriftSqlType.dateTime, - requiredDuringInsert: true, - ); - @override - List get $columns => [id, name, description, createdAt]; + List get $columns => [ + id, + createdAt, + name, + nameCount, + description, + ]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -68,6 +86,14 @@ class $PlayerTableTable extends PlayerTable } else if (isInserting) { context.missing(_idMeta); } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } if (data.containsKey('name')) { context.handle( _nameMeta, @@ -76,6 +102,12 @@ class $PlayerTableTable extends PlayerTable } else if (isInserting) { context.missing(_nameMeta); } + if (data.containsKey('name_count')) { + context.handle( + _nameCountMeta, + nameCount.isAcceptableOrUnknown(data['name_count']!, _nameCountMeta), + ); + } if (data.containsKey('description')) { context.handle( _descriptionMeta, @@ -87,14 +119,6 @@ class $PlayerTableTable extends PlayerTable } else if (isInserting) { context.missing(_descriptionMeta); } - if (data.containsKey('created_at')) { - context.handle( - _createdAtMeta, - createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), - ); - } else if (isInserting) { - context.missing(_createdAtMeta); - } return context; } @@ -108,18 +132,22 @@ class $PlayerTableTable extends PlayerTable DriftSqlType.string, data['${effectivePrefix}id'], )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, name: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}name'], )!, + nameCount: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}name_count'], + )!, description: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}description'], )!, - createdAt: attachedDatabase.typeMapping.read( - DriftSqlType.dateTime, - data['${effectivePrefix}created_at'], - )!, ); } @@ -131,31 +159,35 @@ class $PlayerTableTable extends PlayerTable class PlayerTableData extends DataClass implements Insertable { final String id; - final String name; - final String description; final DateTime createdAt; + final String name; + final int nameCount; + final String description; const PlayerTableData({ required this.id, - required this.name, - required this.description, required this.createdAt, + required this.name, + required this.nameCount, + required this.description, }); @override Map toColumns(bool nullToAbsent) { final map = {}; map['id'] = Variable(id); - map['name'] = Variable(name); - map['description'] = Variable(description); map['created_at'] = Variable(createdAt); + map['name'] = Variable(name); + map['name_count'] = Variable(nameCount); + map['description'] = Variable(description); return map; } PlayerTableCompanion toCompanion(bool nullToAbsent) { return PlayerTableCompanion( id: Value(id), - name: Value(name), - description: Value(description), createdAt: Value(createdAt), + name: Value(name), + nameCount: Value(nameCount), + description: Value(description), ); } @@ -166,9 +198,10 @@ class PlayerTableData extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return PlayerTableData( id: serializer.fromJson(json['id']), - name: serializer.fromJson(json['name']), - description: serializer.fromJson(json['description']), createdAt: serializer.fromJson(json['createdAt']), + name: serializer.fromJson(json['name']), + nameCount: serializer.fromJson(json['nameCount']), + description: serializer.fromJson(json['description']), ); } @override @@ -176,31 +209,35 @@ class PlayerTableData extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'id': serializer.toJson(id), - 'name': serializer.toJson(name), - 'description': serializer.toJson(description), 'createdAt': serializer.toJson(createdAt), + 'name': serializer.toJson(name), + 'nameCount': serializer.toJson(nameCount), + 'description': serializer.toJson(description), }; } PlayerTableData copyWith({ String? id, - String? name, - String? description, DateTime? createdAt, + String? name, + int? nameCount, + String? description, }) => PlayerTableData( id: id ?? this.id, - name: name ?? this.name, - description: description ?? this.description, createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + nameCount: nameCount ?? this.nameCount, + description: description ?? this.description, ); PlayerTableData copyWithCompanion(PlayerTableCompanion data) { return PlayerTableData( id: data.id.present ? data.id.value : this.id, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, name: data.name.present ? data.name.value : this.name, + nameCount: data.nameCount.present ? data.nameCount.value : this.nameCount, description: data.description.present ? data.description.value : this.description, - createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, ); } @@ -208,76 +245,85 @@ class PlayerTableData extends DataClass implements Insertable { String toString() { return (StringBuffer('PlayerTableData(') ..write('id: $id, ') + ..write('createdAt: $createdAt, ') ..write('name: $name, ') - ..write('description: $description, ') - ..write('createdAt: $createdAt') + ..write('nameCount: $nameCount, ') + ..write('description: $description') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name, description, createdAt); + int get hashCode => Object.hash(id, createdAt, name, nameCount, description); @override bool operator ==(Object other) => identical(this, other) || (other is PlayerTableData && other.id == this.id && + other.createdAt == this.createdAt && other.name == this.name && - other.description == this.description && - other.createdAt == this.createdAt); + other.nameCount == this.nameCount && + other.description == this.description); } class PlayerTableCompanion extends UpdateCompanion { final Value id; - final Value name; - final Value description; final Value createdAt; + final Value name; + final Value nameCount; + final Value description; final Value rowid; const PlayerTableCompanion({ this.id = const Value.absent(), - this.name = const Value.absent(), - this.description = const Value.absent(), this.createdAt = const Value.absent(), + this.name = const Value.absent(), + this.nameCount = const Value.absent(), + this.description = const Value.absent(), this.rowid = const Value.absent(), }); PlayerTableCompanion.insert({ required String id, - required String name, - required String description, required DateTime createdAt, + required String name, + this.nameCount = const Value.absent(), + required String description, this.rowid = const Value.absent(), }) : id = Value(id), + createdAt = Value(createdAt), name = Value(name), - description = Value(description), - createdAt = Value(createdAt); + description = Value(description); static Insertable custom({ Expression? id, - Expression? name, - Expression? description, Expression? createdAt, + Expression? name, + Expression? nameCount, + Expression? description, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, - if (name != null) 'name': name, - if (description != null) 'description': description, if (createdAt != null) 'created_at': createdAt, + if (name != null) 'name': name, + if (nameCount != null) 'name_count': nameCount, + if (description != null) 'description': description, if (rowid != null) 'rowid': rowid, }); } PlayerTableCompanion copyWith({ Value? id, - Value? name, - Value? description, Value? createdAt, + Value? name, + Value? nameCount, + Value? description, Value? rowid, }) { return PlayerTableCompanion( id: id ?? this.id, - name: name ?? this.name, - description: description ?? this.description, createdAt: createdAt ?? this.createdAt, + name: name ?? this.name, + nameCount: nameCount ?? this.nameCount, + description: description ?? this.description, rowid: rowid ?? this.rowid, ); } @@ -288,15 +334,18 @@ class PlayerTableCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } if (name.present) { map['name'] = Variable(name.value); } + if (nameCount.present) { + map['name_count'] = Variable(nameCount.value); + } if (description.present) { map['description'] = Variable(description.value); } - if (createdAt.present) { - map['created_at'] = Variable(createdAt.value); - } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -307,9 +356,10 @@ class PlayerTableCompanion extends UpdateCompanion { String toString() { return (StringBuffer('PlayerTableCompanion(') ..write('id: $id, ') - ..write('name: $name, ') - ..write('description: $description, ') ..write('createdAt: $createdAt, ') + ..write('name: $name, ') + ..write('nameCount: $nameCount, ') + ..write('description: $description, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -2790,17 +2840,19 @@ abstract class _$AppDatabase extends GeneratedDatabase { typedef $$PlayerTableTableCreateCompanionBuilder = PlayerTableCompanion Function({ required String id, - required String name, - required String description, required DateTime createdAt, + required String name, + Value nameCount, + required String description, Value rowid, }); typedef $$PlayerTableTableUpdateCompanionBuilder = PlayerTableCompanion Function({ Value id, - Value name, - Value description, Value createdAt, + Value name, + Value nameCount, + Value description, Value rowid, }); @@ -2892,18 +2944,23 @@ class $$PlayerTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get name => $composableBuilder( column: $table.name, builder: (column) => ColumnFilters(column), ); - ColumnFilters get description => $composableBuilder( - column: $table.description, + ColumnFilters get nameCount => $composableBuilder( + column: $table.nameCount, builder: (column) => ColumnFilters(column), ); - ColumnFilters get createdAt => $composableBuilder( - column: $table.createdAt, + ColumnFilters get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnFilters(column), ); @@ -2997,18 +3054,23 @@ class $$PlayerTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get name => $composableBuilder( column: $table.name, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get description => $composableBuilder( - column: $table.description, + ColumnOrderings get nameCount => $composableBuilder( + column: $table.nameCount, builder: (column) => ColumnOrderings(column), ); - ColumnOrderings get createdAt => $composableBuilder( - column: $table.createdAt, + ColumnOrderings get description => $composableBuilder( + column: $table.description, builder: (column) => ColumnOrderings(column), ); } @@ -3025,17 +3087,20 @@ class $$PlayerTableTableAnnotationComposer GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get nameCount => + $composableBuilder(column: $table.nameCount, builder: (column) => column); + GeneratedColumn get description => $composableBuilder( column: $table.description, builder: (column) => column, ); - GeneratedColumn get createdAt => - $composableBuilder(column: $table.createdAt, builder: (column) => column); - Expression playerGroupTableRefs( Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, ) { @@ -3145,29 +3210,33 @@ class $$PlayerTableTableTableManager updateCompanionCallback: ({ Value id = const Value.absent(), - Value name = const Value.absent(), - Value description = const Value.absent(), Value createdAt = const Value.absent(), + Value name = const Value.absent(), + Value nameCount = const Value.absent(), + Value description = const Value.absent(), Value rowid = const Value.absent(), }) => PlayerTableCompanion( id: id, - name: name, - description: description, createdAt: createdAt, + name: name, + nameCount: nameCount, + description: description, rowid: rowid, ), createCompanionCallback: ({ required String id, - required String name, - required String description, required DateTime createdAt, + required String name, + Value nameCount = const Value.absent(), + required String description, Value rowid = const Value.absent(), }) => PlayerTableCompanion.insert( id: id, - name: name, - description: description, createdAt: createdAt, + name: name, + nameCount: nameCount, + description: description, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/data/db/tables/player_table.dart b/lib/data/db/tables/player_table.dart index 15b29a5..ce4f931 100644 --- a/lib/data/db/tables/player_table.dart +++ b/lib/data/db/tables/player_table.dart @@ -2,9 +2,10 @@ import 'package:drift/drift.dart'; class PlayerTable extends Table { TextColumn get id => text()(); - TextColumn get name => text()(); - TextColumn get description => text()(); DateTimeColumn get createdAt => dateTime()(); + TextColumn get name => text()(); + IntColumn get nameCount => integer().withDefault(const Constant(0))(); + TextColumn get description => text()(); @override Set> get primaryKey => {id}; diff --git a/lib/data/models/player.dart b/lib/data/models/player.dart index c405de9..b7de429 100644 --- a/lib/data/models/player.dart +++ b/lib/data/models/player.dart @@ -5,12 +5,14 @@ class Player { final String id; final DateTime createdAt; final String name; + final int nameCount; final String description; Player({ String? id, DateTime? createdAt, required this.name, + this.nameCount = 0, String? description, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(), @@ -26,6 +28,7 @@ class Player { : id = json['id'], createdAt = DateTime.parse(json['createdAt']), name = json['name'], + nameCount = 0, description = json['description']; /// Converts the Player instance to a JSON object. diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 3042b33..45eacf0 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart' hide isNull; +import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tallee/data/db/database.dart'; @@ -381,5 +381,162 @@ void main() { ); expect(playerExists, true); }); + + group('Name Count Tests', () { + test('Single player gets initialized wih name count 0', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 0); + }); + + test('Multiple players get initialized wih name count 0', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2], + ); + + final players = await database.playerDao.getAllPlayers(); + + expect(players.length, 2); + for (Player p in players) { + expect(p.nameCount, 0); + } + }); + + test( + 'Seperatly added players nameCount gets increased correctly', + () async { + await database.playerDao.addPlayer(player: testPlayer1); + + final player1 = Player(name: testPlayer1.name, description: ''); + await database.playerDao.addPlayer(player: player1); + + var players = await database.playerDao.getAllPlayers(); + + expect(players.length, 2); + players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); + + for (int i = 0; i < players.length - 1; i++) { + expect(players[i].nameCount, i + 1); + } + }, + ); + + test( + 'Together added players nameCount gets increased correctly', + () async { + final player1 = Player(name: testPlayer1.name, description: ''); + final player2 = Player(name: testPlayer1.name, description: ''); + final player3 = Player(name: testPlayer1.name, description: ''); + + // addPlayersAsList() with multiple players and with one player + await database.playerDao.addPlayersAsList(players: [testPlayer1]); + await database.playerDao.addPlayersAsList( + players: [player1, player2, player3], + ); + + var players = await database.playerDao.getAllPlayers(); + + expect(players.length, 4); + players.sort((a, b) => a.nameCount.compareTo(b.nameCount)); + + for (int i = 0; i < players.length - 1; i++) { + expect(players[i].nameCount, i + 1); + } + }, + ); + + test('getNameCount works correctly', () async { + final player2 = Player(name: testPlayer1.name, description: ''); + final player3 = Player(name: testPlayer1.name, description: ''); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, player2, player3], + ); + + final nameCount = await database.playerDao.getNameCount( + name: testPlayer1.name, + ); + + expect(nameCount, 2); + }); + + test('updateNameCount works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + final success = await database.playerDao.updateNameCount( + playerId: testPlayer1.id, + nameCount: 2, + ); + + expect(success, true); + + final nameCount = await database.playerDao.getNameCount( + name: testPlayer1.name, + ); + + expect(nameCount, 2); + }); + + test('getPlayerWithHighestNameCount works correctly', () async { + 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( + name: testPlayer1.name, + ); + + expect(player, isNotNull); + expect(player!.nameCount, 3); + }); + + test('getPlayerWithHighestNameCount with non existing player', () async { + final player = await database.playerDao.getPlayerWithHighestNameCount( + name: 'non-existing-name', + ); + 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); + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.nameCount, 1); + }); + }); }); } From 9a2afbfd3b67f49d08a7dcd2c3810fb3f8171049 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 22:49:21 +0200 Subject: [PATCH 2/8] Added ui implementation --- lib/core/common.dart | 8 +++++ .../group_view/group_detail_view.dart | 2 ++ .../match_view/match_detail_view.dart | 1 + .../widgets/player_selection.dart | 3 ++ .../widgets/tiles/group_tile.dart | 7 ++++- .../widgets/tiles/match_tile.dart | 6 +++- .../widgets/tiles/text_icon_list_tile.dart | 29 +++++++++++++++---- .../widgets/tiles/text_icon_tile.dart | 27 +++++++++++++++-- 8 files changed, 73 insertions(+), 10 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 20b0225..399872c 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -1,6 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; /// Translates a [Ruleset] enum value to its corresponding localized string. @@ -43,3 +44,10 @@ String getExtraPlayerCount(Match match) { } return ' + ${count.toString()}'; } + +String getNameCountText(Player player) { + if (player.nameCount >= 1) { + return ' #${player.nameCount}'; + } + return ''; +} diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 1ef89ef..0847ee5 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.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/db/database.dart'; import 'package:tallee/data/models/group.dart'; @@ -153,6 +154,7 @@ class _GroupDetailViewState extends State { children: _group.members.map((member) { return TextIconTile( text: member.name, + suffixText: getNameCountText(member), iconEnabled: false, ); }).toList(), diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index fc53aa8..6671196 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -161,6 +161,7 @@ class _MatchDetailViewState extends State { children: match.players.map((player) { return TextIconTile( text: player.name, + suffixText: getNameCountText(player), iconEnabled: false, ); }).toList(), diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 6d8769d..31bf942 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; @@ -140,6 +141,7 @@ class _PlayerSelectionState extends State { padding: const EdgeInsets.only(right: 8.0), child: TextIconTile( text: player.name, + suffixText: getNameCountText(player), onIconTap: () { setState(() { // Removes the player from the selection and notifies the parent. @@ -193,6 +195,7 @@ class _PlayerSelectionState extends State { itemBuilder: (BuildContext context, int index) { return TextIconListTile( text: suggestedPlayers[index].name, + suffixText: getNameCountText(suggestedPlayers[index]), onPressed: () { setState(() { // If the player is not already selected diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index b62f3ce..f4ace65 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.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/widgets/tiles/text_icon_tile.dart'; @@ -81,7 +82,11 @@ class _GroupTileState extends State { for (var member in [ ...widget.group.members, ]..sort((a, b) => a.name.compareTo(b.name))) - TextIconTile(text: member.name, iconEnabled: false), + TextIconTile( + text: member.name, + suffixText: getNameCountText(member), + iconEnabled: false, + ), ], ), const SizedBox(height: 2.5), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 39f9cdf..eaf6a4f 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -203,7 +203,11 @@ class _MatchTileState extends State { spacing: 6, runSpacing: 6, children: players.map((player) { - return TextIconTile(text: player.name, iconEnabled: false); + return TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: false, + ); }).toList(), ), ], diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index 2b29d41..a31f2ae 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -9,6 +9,7 @@ class TextIconListTile extends StatelessWidget { const TextIconListTile({ super.key, required this.text, + this.suffixText = '', this.iconEnabled = true, this.onPressed, }); @@ -16,6 +17,9 @@ class TextIconListTile extends StatelessWidget { /// The text to display in the tile. final String text; + /// An optional suffix text to display after the main text. + final String suffixText; + /// A boolean to determine if the icon should be displayed. final bool iconEnabled; @@ -35,12 +39,27 @@ class TextIconListTile extends StatelessWidget { Flexible( child: Container( padding: const EdgeInsets.symmetric(vertical: 12.5), - child: Text( - text, + child: RichText( overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: text, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + TextSpan( + text: suffixText, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: CustomTheme.textColor.withAlpha(100), + ), + ), + ], ), ), ), diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index f98e0a7..541b6ae 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -9,6 +9,7 @@ class TextIconTile extends StatelessWidget { const TextIconTile({ super.key, required this.text, + this.suffixText = '', this.iconEnabled = true, this.onIconTap, }); @@ -16,6 +17,8 @@ class TextIconTile extends StatelessWidget { /// The text to display in the tile. final String text; + final String suffixText; + /// A boolean to determine if the icon should be displayed. final bool iconEnabled; @@ -36,10 +39,28 @@ class TextIconTile extends StatelessWidget { children: [ if (iconEnabled) const SizedBox(width: 3), Flexible( - child: Text( - text, + child: RichText( overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + 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), + ), + ), + ], + ), ), ), if (iconEnabled) ...[ From 653b85d28d55cc557e28bc6191422328a4a823b9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 23:11:17 +0200 Subject: [PATCH 3/8] Fixed tests --- lib/data/dao/player_dao.dart | 2 +- test/db_tests/entities/player_test.dart | 14 ++++++-------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 74b2960..6d4cff7 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -93,7 +93,6 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { // Get the current nameCount var nameCount = await calculateNameCount(name: name); - if (nameCount == 0) nameCount++; // One player with the same name if (playersWithName.length == 1) { @@ -108,6 +107,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { ), ); } else { + if (nameCount == 0) nameCount++; // Multiple players with the same name for (var i = 0; i < playersWithName.length; i++) { final player = playersWithName[i]; diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 45eacf0..7462bbf 100644 --- a/test/db_tests/entities/player_test.dart +++ b/test/db_tests/entities/player_test.dart @@ -449,8 +449,8 @@ void main() { ); test('getNameCount works correctly', () async { - final player2 = Player(name: testPlayer1.name, description: ''); - final player3 = Player(name: testPlayer1.name, description: ''); + final player2 = Player(name: testPlayer1.name); + final player3 = Player(name: testPlayer1.name); await database.playerDao.addPlayersAsList( players: [testPlayer1, player2, player3], @@ -460,7 +460,7 @@ void main() { name: testPlayer1.name, ); - expect(nameCount, 2); + expect(nameCount, 3); }); test('updateNameCount works correctly', () async { @@ -470,14 +470,12 @@ void main() { playerId: testPlayer1.id, nameCount: 2, ); - expect(success, true); - final nameCount = await database.playerDao.getNameCount( - name: testPlayer1.name, + final player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, ); - - expect(nameCount, 2); + expect(player.nameCount, 2); }); test('getPlayerWithHighestNameCount works correctly', () async { From fcf845af4d775006088cd56c66baed12d1fc971c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 23:22:14 +0200 Subject: [PATCH 4/8] Implemented name count update in player selection --- lib/data/models/player.dart | 4 ++-- .../widgets/player_selection.dart | 19 ++++++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/data/models/player.dart b/lib/data/models/player.dart index b7de429..12d17f0 100644 --- a/lib/data/models/player.dart +++ b/lib/data/models/player.dart @@ -5,7 +5,7 @@ class Player { final String id; final DateTime createdAt; final String name; - final int nameCount; + int nameCount; final String description; Player({ @@ -20,7 +20,7 @@ class Player { @override String toString() { - return 'Player{id: $id, name: $name, description: $description}'; + return 'Player{id: $id, createdAt: $createdAt, name: $name, nameCount: $nameCount, description: $description}'; } /// Creates a Player instance from a JSON object. diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 31bf942..0aa6653 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -285,7 +285,8 @@ class _PlayerSelectionState extends State { final loc = AppLocalizations.of(context); final playerName = _searchBarController.text.trim(); - final createdPlayer = Player(name: playerName, description: ''); + int nameCount = _calculateNameCount(playerName); + final createdPlayer = Player(name: playerName, nameCount: nameCount); final success = await db.playerDao.addPlayer(player: createdPlayer); if (!context.mounted) return; @@ -298,6 +299,22 @@ class _PlayerSelectionState extends State { } } + int _calculateNameCount(String playerName) { + final playersWithSameName = + allPlayers.where((player) => player.name == playerName).toList() + ..sort((a, b) => a.nameCount.compareTo(b.nameCount)); + + if (playersWithSameName.isEmpty) { + return 0; + } else if (playersWithSameName.length == 1) { + // Initialize nameCount + playersWithSameName[0].nameCount = 1; + } + + // Return following count + return playersWithSameName.length + 1; + } + /// Updates the state after successfully adding a new player. void _handleSuccessfulPlayerCreation(Player player) { selectedPlayers.insert(0, player); From 9a0386f22d7c9059c745dfa2e0c1be76a1226758 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 19 Apr 2026 23:41:10 +0200 Subject: [PATCH 5/8] Added case for not fetching a player --- lib/data/dao/player_dao.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 6d4cff7..5d46343 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -143,7 +143,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return rowsAffected > 0; } - /// Checks if a player with the given [id] exists in the database. + /// Checks if a player with the given [playerId] exists in the database. /// Returns `true` if the player exists, `false` otherwise. Future playerExists({required String playerId}) async { final query = select(playerTable)..where((p) => p.id.equals(playerId)); @@ -157,9 +157,11 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { required String newName, }) 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).getSingle(); + final previousPlayerName = + await (select(playerTable)..where((p) => p.id.equals(playerId))) + .map((row) => row.name) + .getSingleOrNull() ?? + ''; final previousNameCount = await getNameCount(name: previousPlayerName); await (update(playerTable)..where((p) => p.id.equals(playerId))).write( @@ -197,7 +199,7 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return count ?? 0; } - /// Retrieves the count of players with the given [name] in the database. + /// Retrieves the count of players with the given [name]. Future getNameCount({required String name}) async { final query = select(playerTable)..where((p) => p.name.equals(name)); final result = await query.get(); From 4e97f6723a49a6de1853e4af8f99e0c84fdaa825 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 20 Apr 2026 16:39:33 +0200 Subject: [PATCH 6/8] Added nameCount to statistic tiles --- .../views/main_menu/statistics_view.dart | 108 +++++++++--------- .../widgets/tiles/statistics_tile.dart | 33 +++++- 2 files changed, 82 insertions(+), 59 deletions(-) diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 3a55115..221ffee 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -18,9 +18,18 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { - List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1)); - List<(String, int)> matchCounts = List.filled(6, ('Skeleton Player', 1)); - List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1)); + List<(Player, int)> winCounts = List.filled(6, ( + Player(name: 'Skeleton Player'), + 1, + )); + List<(Player, int)> matchCounts = List.filled(6, ( + Player(name: 'Skeleton Player'), + 1, + )); + List<(Player, double)> winRates = List.filled(6, ( + Player(name: 'Skeleton Player'), + 1, + )); bool isLoading = true; @override @@ -121,7 +130,10 @@ class _StatisticsViewState extends State { players: players, context: context, ); - winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts); + winRates = computeWinRatePercent( + winCounts: winCounts, + matchCounts: matchCounts, + ); setState(() { isLoading = false; }); @@ -130,47 +142,47 @@ class _StatisticsViewState extends State { /// Calculates the number of wins for each player /// and returns a sorted list of tuples (playerName, winCount) - List<(String, int)> _calculateWinsForAllPlayers({ + List<(Player, int)> _calculateWinsForAllPlayers({ required List matches, required List players, required BuildContext context, }) { - List<(String, int)> winCounts = []; + List<(Player, int)> winCounts = []; final loc = AppLocalizations.of(context); // Getting the winners for (var match in matches) { final winner = match.winner; if (winner != null) { - final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); + final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id); // -1 means winner not found in winCounts if (index != -1) { final current = winCounts[index].$2; - winCounts[index] = (winner.id, current + 1); + winCounts[index] = (winner, current + 1); } else { - winCounts.add((winner.id, 1)); + winCounts.add((winner, 1)); } } } // Adding all players with zero wins for (var player in players) { - final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + final index = winCounts.indexWhere((entry) => entry.$1.id == player.id); // -1 means player not found in winCounts if (index == -1) { - winCounts.add((player.id, 0)); + winCounts.add((player, 0)); } } // Replace player IDs with names for (int i = 0; i < winCounts.length; i++) { - final playerId = winCounts[i].$1; + final playerId = winCounts[i].$1.id; final player = players.firstWhere( (p) => p.id == playerId, orElse: () => Player(id: playerId, name: loc.not_available, description: ''), ); - winCounts[i] = (player.name, winCounts[i].$2); + winCounts[i] = (player, winCounts[i].$2); } winCounts.sort((a, b) => b.$2.compareTo(a.$2)); @@ -180,60 +192,51 @@ class _StatisticsViewState extends State { /// Calculates the number of matches played for each player /// and returns a sorted list of tuples (playerName, matchCount) - List<(String, int)> _calculateMatchAmountsForAllPlayers({ + List<(Player, int)> _calculateMatchAmountsForAllPlayers({ required List matches, required List players, required BuildContext context, }) { - List<(String, int)> matchCounts = []; + List<(Player, int)> matchCounts = []; final loc = AppLocalizations.of(context); // Counting matches for each player for (var match in matches) { - if (match.group != null) { - final members = match.group!.members.map((p) => p.id).toList(); - for (var playerId in members) { - final index = matchCounts.indexWhere((entry) => entry.$1 == playerId); - // -1 means player not found in matchCounts - if (index != -1) { - final current = matchCounts[index].$2; - matchCounts[index] = (playerId, current + 1); - } else { - matchCounts.add((playerId, 1)); - } - } - } - final members = match.players.map((p) => p.id).toList(); - for (var playerId in members) { - final index = matchCounts.indexWhere((entry) => entry.$1 == playerId); - // -1 means player not found in matchCounts - if (index != -1) { - final current = matchCounts[index].$2; - matchCounts[index] = (playerId, current + 1); + for (Player player in match.players) { + // Check if the player is already in matchCounts + final index = matchCounts.indexWhere( + (entry) => entry.$1.id == player.id, + ); + + // -1 -> not found + if (index == -1) { + // Add new entry + matchCounts.add((player, 1)); } else { - matchCounts.add((playerId, 1)); + // Update existing entry + final currentMatchAmount = matchCounts[index].$2; + matchCounts[index] = (player, currentMatchAmount + 1); } } } // Adding all players with zero matches for (var player in players) { - final index = matchCounts.indexWhere((entry) => entry.$1 == player.id); + final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id); // -1 means player not found in matchCounts if (index == -1) { - matchCounts.add((player.id, 0)); + matchCounts.add((player, 0)); } } // Replace player IDs with names for (int i = 0; i < matchCounts.length; i++) { - final playerId = matchCounts[i].$1; + final playerId = matchCounts[i].$1.id; final player = players.firstWhere( (p) => p.id == playerId, - orElse: () => - Player(id: playerId, name: loc.not_available, description: ''), + orElse: () => Player(id: playerId, name: loc.not_available), ); - matchCounts[i] = (player.name, matchCounts[i].$2); + matchCounts[i] = (player, matchCounts[i].$2); } matchCounts.sort((a, b) => b.$2.compareTo(a.$2)); @@ -241,25 +244,24 @@ class _StatisticsViewState extends State { return matchCounts; } - // dart - List<(String, double)> computeWinRatePercent({ - required List<(String, int)> wins, - required List<(String, int)> matches, + List<(Player, double)> computeWinRatePercent({ + required List<(Player, int)> winCounts, + required List<(Player, int)> matchCounts, }) { - final Map winsMap = {for (var e in wins) e.$1: e.$2}; - final Map matchesMap = {for (var e in matches) e.$1: e.$2}; + final Map winsMap = {for (var e in winCounts) e.$1: e.$2}; + final Map matchesMap = {for (var e in matchCounts) e.$1: e.$2}; // Get all unique player names - final names = {...winsMap.keys, ...matchesMap.keys}; + final player = {...matchesMap.keys}; // Calculate win rates - final result = names.map((name) { + final result = player.map((name) { final int w = winsMap[name] ?? 0; - final int g = matchesMap[name] ?? 0; + final int m = matchesMap[name] ?? 0; // Calculate percentage and round to 2 decimal places // Avoid division by zero - final double percent = (g > 0) - ? double.parse(((w / g)).toStringAsFixed(2)) + final double percent = (m > 0) + ? double.parse(((w / m)).toStringAsFixed(2)) : 0; return (name, percent); }).toList(); diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index bc2f7b6..ea9cb49 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -1,6 +1,9 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; @@ -32,7 +35,7 @@ class StatisticsTile extends StatelessWidget { final double width; /// A list of tuples containing labels and their corresponding numeric values. - final List<(String, num)> values; + final List<(Player, num)> values; /// The maximum number of items to display. final int itemCount; @@ -89,11 +92,29 @@ class StatisticsTile extends StatelessWidget { ), Padding( padding: const EdgeInsets.only(left: 4.0), - child: Text( - values[index].$1, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: values[index].$1.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: getNameCountText(values[index].$1), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor.withAlpha( + 150, + ), + ), + ), + ], ), ), ), From daf1bc27d8c63d08dc2637e468927020b5515e39 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 24 Apr 2026 09:19:39 +0000 Subject: [PATCH 7/8] Updated version number [skip ci] --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index b51c02a..2de3b32 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.21+255 +version: 0.0.22+256 environment: sdk: ^3.8.1 From 39eba80e3ff2d897a9e6a7f76392f10b6bc69554 Mon Sep 17 00:00:00 2001 From: "Gitea Actions [bot]" Date: Fri, 24 Apr 2026 09:20:20 +0000 Subject: [PATCH 8/8] Updated licenses [skip ci] --- .../settings_view/licenses/oss_licenses.dart | 101 +++++++++--------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index d712475..954b86e 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -30,7 +30,6 @@ const allDependencies = [ _cli_util, _clock, _code_assets, - _code_builder, _collection, _convert, _coverage, @@ -109,6 +108,7 @@ const allDependencies = [ _pubspec_parse, _quiver, _recase, + _record_use, _retry, _rfc_6901, _safe_url_check, @@ -567,17 +567,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// build_runner 2.13.1 +/// build_runner 2.14.0 const _build_runner = Package( name: 'build_runner', description: 'A build system for Dart code generation and modular compilation.', repository: 'https://github.com/dart-lang/build/tree/master/build_runner', authors: [], - version: '2.13.1', + version: '2.14.0', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('code_builder'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')], + dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')], devDependencies: [PackageRef('stream_channel'), PackageRef('test')], license: '''Copyright 2016, the Dart project authors. @@ -1153,47 +1153,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// code_builder 4.11.1 -const _code_builder = Package( - name: 'code_builder', - description: 'A fluent, builder-based library for generating valid Dart code.', - repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/code_builder', - authors: [], - version: '4.11.1', - spdxIdentifiers: ['BSD-3-Clause'], - isMarkdown: false, - isSdk: false, - dependencies: [PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('matcher'), PackageRef('meta')], - devDependencies: [PackageRef('build'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('source_gen'), PackageRef('test')], - license: '''Copyright 2016, the Dart project authors. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', - ); - /// collection 1.19.1 const _collection = Package( name: 'collection', @@ -2892,17 +2851,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); -/// hooks 1.0.2 +/// hooks 1.0.3 const _hooks = Package( name: 'hooks', description: 'A library that contains a Dart API for the JSON-based protocol for `hook/build.dart` and `hook/link.dart`.', repository: 'https://github.com/dart-lang/native/tree/main/pkgs/hooks', authors: [], - version: '1.0.2', + version: '1.0.3', spdxIdentifiers: ['BSD-3-Clause'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('yaml')], + dependencies: [PackageRef('collection'), PackageRef('crypto'), PackageRef('logging'), PackageRef('meta'), PackageRef('pub_semver'), PackageRef('record_use'), PackageRef('yaml')], devDependencies: [PackageRef('args'), PackageRef('code_assets'), PackageRef('glob'), PackageRef('json_schema'), PackageRef('path'), PackageRef('test')], license: '''Copyright 2025, the Dart project authors. @@ -5106,6 +5065,48 @@ Redistribution and use in source and binary forms, with or without modification, THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', ); +/// record_use 0.6.0 +const _record_use = Package( + name: 'record_use', + description: '''The serialization logic and API for the usage recording SDK feature. +''', + repository: 'https://github.com/dart-lang/native/tree/main/pkgs/record_use', + authors: [], + version: '0.6.0', + spdxIdentifiers: ['BSD-3-Clause'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('collection'), PackageRef('meta'), PackageRef('pub_semver')], + devDependencies: [PackageRef('json_schema'), PackageRef('test')], + license: '''Copyright 2024, the Dart project authors. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google LLC nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', + ); + /// retry 3.1.2 const _retry = Package( name: 'retry', @@ -37880,12 +37881,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.21+255 +/// tallee 0.0.22+256 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.21+255', + version: '0.0.22+256', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false,