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/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index c58cb9a..5d46343 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); + + // 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 { + if (nameCount == 0) nameCount++; + // 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, ), ); @@ -90,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)); @@ -103,9 +156,38 @@ 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) + .getSingleOrNull() ?? + ''; + 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 +199,76 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { return count ?? 0; } + /// 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(); + 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..12d17f0 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; + 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(), @@ -18,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. @@ -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/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..0aa6653 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 @@ -282,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; @@ -295,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); 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) ...[ diff --git a/test/db_tests/entities/player_test.dart b/test/db_tests/entities/player_test.dart index 3042b33..7462bbf 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,160 @@ 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); + final player3 = Player(name: testPlayer1.name); + + await database.playerDao.addPlayersAsList( + players: [testPlayer1, player2, player3], + ); + + final nameCount = await database.playerDao.getNameCount( + name: testPlayer1.name, + ); + + expect(nameCount, 3); + }); + + 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 player = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(player.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); + }); + }); }); }