From badf5ea3114ad1b4be2d0d1dc272ceba5a8453df Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 11 May 2026 12:23:48 +0200 Subject: [PATCH 01/51] feat: added test for exported json --- assets/schema.json | 34 ++----------------- test/services/data_transfer_service_test.dart | 23 ++++++++++--- 2 files changed, 22 insertions(+), 35 deletions(-) diff --git a/assets/schema.json b/assets/schema.json index 6bcbe45..7f6aebd 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -102,36 +102,6 @@ ] } }, - "teams": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "createdAt": { - "type": "string" - }, - "memberIds": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "required": [ - "id", - "name", - "createdAt", - "memberIds" - ] - } - }, "matches": { "type": "array", "items": { @@ -195,6 +165,9 @@ }, "notes": { "type": "string" + }, + "teams": { + "type": ["array", "null"] } }, "additionalProperties": false, @@ -214,7 +187,6 @@ "players", "games", "groups", - "teams", "matches" ] } \ No newline at end of file diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index fec70b7..70313fa 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -137,9 +137,6 @@ void main() { await database.playerDao.addPlayer(player: testPlayer2); await database.gameDao.addGame(game: testGame); await database.groupDao.addGroup(group: testGroup); - /* - await database.teamDao.addTeam(team: testTeam); -*/ await database.matchDao.addMatch(match: testMatch); final ctx = await getContext(tester); @@ -853,7 +850,7 @@ void main() { }); }); - test('validateJsonSchema()', () async { + test('validateJsonSchema() works correctly', () async { final validJson = json.encode({ 'players': [ { @@ -912,5 +909,23 @@ void main() { final isValid = await DataTransferService.validateJsonSchema(validJson); expect(isValid, true); }); + + testWidgets('validateJsonSchema() validates exported json file', ( + tester, + ) async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); + await database.gameDao.addGame(game: testGame); + await database.groupDao.addGroup(group: testGroup); + await database.matchDao.addMatch(match: testMatch); + + final ctx = await getContext(tester); + final jsonString = await DataTransferService.getAppDataAsJson(ctx); + + expect(jsonString, isNotEmpty); + + final isValid = await DataTransferService.validateJsonSchema(jsonString); + expect(isValid, true); + }); }); } -- 2.49.1 From a957408c7eb482dbb3d3844dddc22793a97ca14d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 17 May 2026 21:29:16 +0200 Subject: [PATCH 02/51] feat: basic integration of teams --- lib/core/common.dart | 15 + lib/data/dao/match_dao.dart | 5 + lib/data/dao/team_dao.dart | 33 + lib/data/db/database.g.dart | 231 ++++++- lib/data/db/tables/match_table.dart | 1 + lib/data/db/tables/team_table.dart | 2 + lib/data/models/match.dart | 56 +- lib/data/models/team.dart | 19 +- .../create_match/create_match_view.dart | 190 +++--- .../create_match/organize_teams_view.dart | 263 ++++++++ .../match_view/match_result_view.dart | 562 ++++++++++++------ .../widgets/buttons/main_menu_button.dart | 65 +- .../widgets/text_input/text_input_field.dart | 1 - .../custom_radio_list_tile.dart | 15 +- .../match_result_view/score_list_tile.dart | 18 +- .../widgets/tiles/match_tile.dart | 6 + .../widgets/tiles/team_creation_tile.dart | 141 +++++ .../widgets/tiles/text_icon_list_tile.dart | 15 +- .../widgets/tiles/text_icon_tile.dart | 10 +- pubspec.yaml | 2 +- 20 files changed, 1325 insertions(+), 325 deletions(-) create mode 100644 lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart create mode 100644 lib/presentation/widgets/tiles/team_creation_tile.dart diff --git a/lib/core/common.dart b/lib/core/common.dart index 312e3fa..29d2f57 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -24,6 +24,21 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } } +// Returns a [GameColor] enum value based on the provided team [index]. +GameColor getTeamColor(int index) { + final colors = [ + GameColor.red, + GameColor.blue, + GameColor.green, + GameColor.yellow, + GameColor.purple, + GameColor.orange, + GameColor.pink, + GameColor.teal, + ]; + return colors[index % colors.length]; +} + /// Translates a [GameColor] enum value to its corresponding localized string. String translateGameColorToString(GameColor color, BuildContext context) { final loc = AppLocalizations.of(context); diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 88cca35..3a77147 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -30,6 +30,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { gameId: match.game.id, groupId: Value(match.group?.id), name: match.name, + isTeamMatch: Value(match.isTeamMatch), notes: match.notes, createdAt: match.createdAt, endedAt: Value(match.endedAt), @@ -142,6 +143,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { gameId: match.game.id, groupId: Value(match.group?.id), name: match.name, + isTeamMatch: Value(match.isTeamMatch), notes: match.notes, createdAt: match.createdAt, endedAt: Value(match.endedAt), @@ -300,6 +302,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group: group, players: players, teams: teams.isEmpty ? null : teams, + isTeamMatch: row.isTeamMatch, notes: row.notes, createdAt: row.createdAt, endedAt: row.endedAt, @@ -334,6 +337,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group: group, players: players, teams: teams.isEmpty ? null : teams, + isTeamMatch: result.isTeamMatch, notes: result.notes, createdAt: result.createdAt, endedAt: result.endedAt, @@ -373,6 +377,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group: group, players: players, teams: teams.isEmpty ? null : teams, + isTeamMatch: row.isTeamMatch, notes: row.notes, createdAt: row.createdAt, endedAt: row.endedAt, diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index cba68fb..cb61e63 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/team_table.dart'; @@ -22,6 +23,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { id: team.id, name: team.name, createdAt: team.createdAt, + color: Value(team.color.name), + score: Value(team.score), ), mode: InsertMode.insertOrReplace, ); @@ -56,6 +59,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { id: team.id, name: team.name, createdAt: team.createdAt, + color: Value(team.color.name), + score: Value(team.score), ), ) .toList(), @@ -110,6 +115,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { id: row.id, name: row.name, createdAt: row.createdAt, + color: GameColor.values.byName(row.color), + score: row.score, members: members, ); }), @@ -125,6 +132,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { id: result.id, name: result.name, createdAt: result.createdAt, + color: GameColor.values.byName(result.color), + score: result.score, members: members, ); } @@ -162,6 +171,30 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return rowsAffected > 0; } + /// Updates the color of the team with the given [teamId]. + Future updateTeamColor({ + required String teamId, + required GameColor color, + }) async { + final rowsAffected = + await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + TeamTableCompanion(color: Value(color.name)), + ); + return rowsAffected > 0; + } + + /// Updates the score of the team with the given [teamId]. + Future updateTeamScore({ + required String teamId, + required int score, + }) async { + final rowsAffected = + await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + TeamTableCompanion(score: Value(score)), + ); + return rowsAffected > 0; + } + /* Delete */ /// Deletes all teams from the database. diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index c8d0faa..5827dfb 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -1185,6 +1185,21 @@ class $MatchTableTable extends MatchTable type: DriftSqlType.string, requiredDuringInsert: true, ); + static const VerificationMeta _isTeamMatchMeta = const VerificationMeta( + 'isTeamMatch', + ); + @override + late final GeneratedColumn isTeamMatch = GeneratedColumn( + 'is_team_match', + aliasedName, + false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("is_team_match" IN (0, 1))', + ), + defaultValue: const Constant(false), + ); static const VerificationMeta _notesMeta = const VerificationMeta('notes'); @override late final GeneratedColumn notes = GeneratedColumn( @@ -1222,6 +1237,7 @@ class $MatchTableTable extends MatchTable gameId, groupId, name, + isTeamMatch, notes, createdAt, endedAt, @@ -1265,6 +1281,15 @@ class $MatchTableTable extends MatchTable } else if (isInserting) { context.missing(_nameMeta); } + if (data.containsKey('is_team_match')) { + context.handle( + _isTeamMatchMeta, + isTeamMatch.isAcceptableOrUnknown( + data['is_team_match']!, + _isTeamMatchMeta, + ), + ); + } if (data.containsKey('notes')) { context.handle( _notesMeta, @@ -1312,6 +1337,10 @@ class $MatchTableTable extends MatchTable DriftSqlType.string, data['${effectivePrefix}name'], )!, + isTeamMatch: attachedDatabase.typeMapping.read( + DriftSqlType.bool, + data['${effectivePrefix}is_team_match'], + )!, notes: attachedDatabase.typeMapping.read( DriftSqlType.string, data['${effectivePrefix}notes'], @@ -1338,6 +1367,7 @@ class MatchTableData extends DataClass implements Insertable { final String gameId; final String? groupId; final String name; + final bool isTeamMatch; final String notes; final DateTime createdAt; final DateTime? endedAt; @@ -1346,6 +1376,7 @@ class MatchTableData extends DataClass implements Insertable { required this.gameId, this.groupId, required this.name, + required this.isTeamMatch, required this.notes, required this.createdAt, this.endedAt, @@ -1359,6 +1390,7 @@ class MatchTableData extends DataClass implements Insertable { map['group_id'] = Variable(groupId); } map['name'] = Variable(name); + map['is_team_match'] = Variable(isTeamMatch); map['notes'] = Variable(notes); map['created_at'] = Variable(createdAt); if (!nullToAbsent || endedAt != null) { @@ -1375,6 +1407,7 @@ class MatchTableData extends DataClass implements Insertable { ? const Value.absent() : Value(groupId), name: Value(name), + isTeamMatch: Value(isTeamMatch), notes: Value(notes), createdAt: Value(createdAt), endedAt: endedAt == null && nullToAbsent @@ -1393,6 +1426,7 @@ class MatchTableData extends DataClass implements Insertable { gameId: serializer.fromJson(json['gameId']), groupId: serializer.fromJson(json['groupId']), name: serializer.fromJson(json['name']), + isTeamMatch: serializer.fromJson(json['isTeamMatch']), notes: serializer.fromJson(json['notes']), createdAt: serializer.fromJson(json['createdAt']), endedAt: serializer.fromJson(json['endedAt']), @@ -1406,6 +1440,7 @@ class MatchTableData extends DataClass implements Insertable { 'gameId': serializer.toJson(gameId), 'groupId': serializer.toJson(groupId), 'name': serializer.toJson(name), + 'isTeamMatch': serializer.toJson(isTeamMatch), 'notes': serializer.toJson(notes), 'createdAt': serializer.toJson(createdAt), 'endedAt': serializer.toJson(endedAt), @@ -1417,6 +1452,7 @@ class MatchTableData extends DataClass implements Insertable { String? gameId, Value groupId = const Value.absent(), String? name, + bool? isTeamMatch, String? notes, DateTime? createdAt, Value endedAt = const Value.absent(), @@ -1425,6 +1461,7 @@ class MatchTableData extends DataClass implements Insertable { gameId: gameId ?? this.gameId, groupId: groupId.present ? groupId.value : this.groupId, name: name ?? this.name, + isTeamMatch: isTeamMatch ?? this.isTeamMatch, notes: notes ?? this.notes, createdAt: createdAt ?? this.createdAt, endedAt: endedAt.present ? endedAt.value : this.endedAt, @@ -1435,6 +1472,9 @@ class MatchTableData extends DataClass implements Insertable { gameId: data.gameId.present ? data.gameId.value : this.gameId, groupId: data.groupId.present ? data.groupId.value : this.groupId, name: data.name.present ? data.name.value : this.name, + isTeamMatch: data.isTeamMatch.present + ? data.isTeamMatch.value + : this.isTeamMatch, notes: data.notes.present ? data.notes.value : this.notes, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, endedAt: data.endedAt.present ? data.endedAt.value : this.endedAt, @@ -1448,6 +1488,7 @@ class MatchTableData extends DataClass implements Insertable { ..write('gameId: $gameId, ') ..write('groupId: $groupId, ') ..write('name: $name, ') + ..write('isTeamMatch: $isTeamMatch, ') ..write('notes: $notes, ') ..write('createdAt: $createdAt, ') ..write('endedAt: $endedAt') @@ -1456,8 +1497,16 @@ class MatchTableData extends DataClass implements Insertable { } @override - int get hashCode => - Object.hash(id, gameId, groupId, name, notes, createdAt, endedAt); + int get hashCode => Object.hash( + id, + gameId, + groupId, + name, + isTeamMatch, + notes, + createdAt, + endedAt, + ); @override bool operator ==(Object other) => identical(this, other) || @@ -1466,6 +1515,7 @@ class MatchTableData extends DataClass implements Insertable { other.gameId == this.gameId && other.groupId == this.groupId && other.name == this.name && + other.isTeamMatch == this.isTeamMatch && other.notes == this.notes && other.createdAt == this.createdAt && other.endedAt == this.endedAt); @@ -1476,6 +1526,7 @@ class MatchTableCompanion extends UpdateCompanion { final Value gameId; final Value groupId; final Value name; + final Value isTeamMatch; final Value notes; final Value createdAt; final Value endedAt; @@ -1485,6 +1536,7 @@ class MatchTableCompanion extends UpdateCompanion { this.gameId = const Value.absent(), this.groupId = const Value.absent(), this.name = const Value.absent(), + this.isTeamMatch = const Value.absent(), this.notes = const Value.absent(), this.createdAt = const Value.absent(), this.endedAt = const Value.absent(), @@ -1495,6 +1547,7 @@ class MatchTableCompanion extends UpdateCompanion { required String gameId, this.groupId = const Value.absent(), required String name, + this.isTeamMatch = const Value.absent(), required String notes, required DateTime createdAt, this.endedAt = const Value.absent(), @@ -1509,6 +1562,7 @@ class MatchTableCompanion extends UpdateCompanion { Expression? gameId, Expression? groupId, Expression? name, + Expression? isTeamMatch, Expression? notes, Expression? createdAt, Expression? endedAt, @@ -1519,6 +1573,7 @@ class MatchTableCompanion extends UpdateCompanion { if (gameId != null) 'game_id': gameId, if (groupId != null) 'group_id': groupId, if (name != null) 'name': name, + if (isTeamMatch != null) 'is_team_match': isTeamMatch, if (notes != null) 'notes': notes, if (createdAt != null) 'created_at': createdAt, if (endedAt != null) 'ended_at': endedAt, @@ -1531,6 +1586,7 @@ class MatchTableCompanion extends UpdateCompanion { Value? gameId, Value? groupId, Value? name, + Value? isTeamMatch, Value? notes, Value? createdAt, Value? endedAt, @@ -1541,6 +1597,7 @@ class MatchTableCompanion extends UpdateCompanion { gameId: gameId ?? this.gameId, groupId: groupId ?? this.groupId, name: name ?? this.name, + isTeamMatch: isTeamMatch ?? this.isTeamMatch, notes: notes ?? this.notes, createdAt: createdAt ?? this.createdAt, endedAt: endedAt ?? this.endedAt, @@ -1563,6 +1620,9 @@ class MatchTableCompanion extends UpdateCompanion { if (name.present) { map['name'] = Variable(name.value); } + if (isTeamMatch.present) { + map['is_team_match'] = Variable(isTeamMatch.value); + } if (notes.present) { map['notes'] = Variable(notes.value); } @@ -1585,6 +1645,7 @@ class MatchTableCompanion extends UpdateCompanion { ..write('gameId: $gameId, ') ..write('groupId: $groupId, ') ..write('name: $name, ') + ..write('isTeamMatch: $isTeamMatch, ') ..write('notes: $notes, ') ..write('createdAt: $createdAt, ') ..write('endedAt: $endedAt, ') @@ -1854,8 +1915,28 @@ class $TeamTableTable extends TeamTable type: DriftSqlType.dateTime, requiredDuringInsert: true, ); + static const VerificationMeta _colorMeta = const VerificationMeta('color'); @override - List get $columns => [id, name, createdAt]; + late final GeneratedColumn color = GeneratedColumn( + 'color', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: false, + defaultValue: const Constant('blue'), + ); + static const VerificationMeta _scoreMeta = const VerificationMeta('score'); + @override + late final GeneratedColumn score = GeneratedColumn( + 'score', + aliasedName, + false, + type: DriftSqlType.int, + requiredDuringInsert: false, + defaultValue: const Constant(0), + ); + @override + List get $columns => [id, name, createdAt, color, score]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -1889,6 +1970,18 @@ class $TeamTableTable extends TeamTable } else if (isInserting) { context.missing(_createdAtMeta); } + if (data.containsKey('color')) { + context.handle( + _colorMeta, + color.isAcceptableOrUnknown(data['color']!, _colorMeta), + ); + } + if (data.containsKey('score')) { + context.handle( + _scoreMeta, + score.isAcceptableOrUnknown(data['score']!, _scoreMeta), + ); + } return context; } @@ -1910,6 +2003,14 @@ class $TeamTableTable extends TeamTable DriftSqlType.dateTime, data['${effectivePrefix}created_at'], )!, + color: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}color'], + )!, + score: attachedDatabase.typeMapping.read( + DriftSqlType.int, + data['${effectivePrefix}score'], + )!, ); } @@ -1923,10 +2024,14 @@ class TeamTableData extends DataClass implements Insertable { final String id; final String name; final DateTime createdAt; + final String color; + final int score; const TeamTableData({ required this.id, required this.name, required this.createdAt, + required this.color, + required this.score, }); @override Map toColumns(bool nullToAbsent) { @@ -1934,6 +2039,8 @@ class TeamTableData extends DataClass implements Insertable { map['id'] = Variable(id); map['name'] = Variable(name); map['created_at'] = Variable(createdAt); + map['color'] = Variable(color); + map['score'] = Variable(score); return map; } @@ -1942,6 +2049,8 @@ class TeamTableData extends DataClass implements Insertable { id: Value(id), name: Value(name), createdAt: Value(createdAt), + color: Value(color), + score: Value(score), ); } @@ -1954,6 +2063,8 @@ class TeamTableData extends DataClass implements Insertable { id: serializer.fromJson(json['id']), name: serializer.fromJson(json['name']), createdAt: serializer.fromJson(json['createdAt']), + color: serializer.fromJson(json['color']), + score: serializer.fromJson(json['score']), ); } @override @@ -1963,20 +2074,31 @@ class TeamTableData extends DataClass implements Insertable { 'id': serializer.toJson(id), 'name': serializer.toJson(name), 'createdAt': serializer.toJson(createdAt), + 'color': serializer.toJson(color), + 'score': serializer.toJson(score), }; } - TeamTableData copyWith({String? id, String? name, DateTime? createdAt}) => - TeamTableData( - id: id ?? this.id, - name: name ?? this.name, - createdAt: createdAt ?? this.createdAt, - ); + TeamTableData copyWith({ + String? id, + String? name, + DateTime? createdAt, + String? color, + int? score, + }) => TeamTableData( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + color: color ?? this.color, + score: score ?? this.score, + ); TeamTableData copyWithCompanion(TeamTableCompanion data) { return TeamTableData( id: data.id.present ? data.id.value : this.id, name: data.name.present ? data.name.value : this.name, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + color: data.color.present ? data.color.value : this.color, + score: data.score.present ? data.score.value : this.score, ); } @@ -1985,37 +2107,47 @@ class TeamTableData extends DataClass implements Insertable { return (StringBuffer('TeamTableData(') ..write('id: $id, ') ..write('name: $name, ') - ..write('createdAt: $createdAt') + ..write('createdAt: $createdAt, ') + ..write('color: $color, ') + ..write('score: $score') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, name, createdAt); + int get hashCode => Object.hash(id, name, createdAt, color, score); @override bool operator ==(Object other) => identical(this, other) || (other is TeamTableData && other.id == this.id && other.name == this.name && - other.createdAt == this.createdAt); + other.createdAt == this.createdAt && + other.color == this.color && + other.score == this.score); } class TeamTableCompanion extends UpdateCompanion { final Value id; final Value name; final Value createdAt; + final Value color; + final Value score; final Value rowid; const TeamTableCompanion({ this.id = const Value.absent(), this.name = const Value.absent(), this.createdAt = const Value.absent(), + this.color = const Value.absent(), + this.score = const Value.absent(), this.rowid = const Value.absent(), }); TeamTableCompanion.insert({ required String id, required String name, required DateTime createdAt, + this.color = const Value.absent(), + this.score = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), name = Value(name), @@ -2024,12 +2156,16 @@ class TeamTableCompanion extends UpdateCompanion { Expression? id, Expression? name, Expression? createdAt, + Expression? color, + Expression? score, Expression? rowid, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (name != null) 'name': name, if (createdAt != null) 'created_at': createdAt, + if (color != null) 'color': color, + if (score != null) 'score': score, if (rowid != null) 'rowid': rowid, }); } @@ -2038,12 +2174,16 @@ class TeamTableCompanion extends UpdateCompanion { Value? id, Value? name, Value? createdAt, + Value? color, + Value? score, Value? rowid, }) { return TeamTableCompanion( id: id ?? this.id, name: name ?? this.name, createdAt: createdAt ?? this.createdAt, + color: color ?? this.color, + score: score ?? this.score, rowid: rowid ?? this.rowid, ); } @@ -2060,6 +2200,12 @@ class TeamTableCompanion extends UpdateCompanion { if (createdAt.present) { map['created_at'] = Variable(createdAt.value); } + if (color.present) { + map['color'] = Variable(color.value); + } + if (score.present) { + map['score'] = Variable(score.value); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -2072,6 +2218,8 @@ class TeamTableCompanion extends UpdateCompanion { ..write('id: $id, ') ..write('name: $name, ') ..write('createdAt: $createdAt, ') + ..write('color: $color, ') + ..write('score: $score, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -4092,6 +4240,7 @@ typedef $$MatchTableTableCreateCompanionBuilder = required String gameId, Value groupId, required String name, + Value isTeamMatch, required String notes, required DateTime createdAt, Value endedAt, @@ -4103,6 +4252,7 @@ typedef $$MatchTableTableUpdateCompanionBuilder = Value gameId, Value groupId, Value name, + Value isTeamMatch, Value notes, Value createdAt, Value endedAt, @@ -4215,6 +4365,11 @@ class $$MatchTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get isTeamMatch => $composableBuilder( + column: $table.isTeamMatch, + builder: (column) => ColumnFilters(column), + ); + ColumnFilters get notes => $composableBuilder( column: $table.notes, builder: (column) => ColumnFilters(column), @@ -4346,6 +4501,11 @@ class $$MatchTableTableOrderingComposer builder: (column) => ColumnOrderings(column), ); + ColumnOrderings get isTeamMatch => $composableBuilder( + column: $table.isTeamMatch, + builder: (column) => ColumnOrderings(column), + ); + ColumnOrderings get notes => $composableBuilder( column: $table.notes, builder: (column) => ColumnOrderings(column), @@ -4423,6 +4583,11 @@ class $$MatchTableTableAnnotationComposer GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); + GeneratedColumn get isTeamMatch => $composableBuilder( + column: $table.isTeamMatch, + builder: (column) => column, + ); + GeneratedColumn get notes => $composableBuilder(column: $table.notes, builder: (column) => column); @@ -4566,6 +4731,7 @@ class $$MatchTableTableTableManager Value gameId = const Value.absent(), Value groupId = const Value.absent(), Value name = const Value.absent(), + Value isTeamMatch = const Value.absent(), Value notes = const Value.absent(), Value createdAt = const Value.absent(), Value endedAt = const Value.absent(), @@ -4575,6 +4741,7 @@ class $$MatchTableTableTableManager gameId: gameId, groupId: groupId, name: name, + isTeamMatch: isTeamMatch, notes: notes, createdAt: createdAt, endedAt: endedAt, @@ -4586,6 +4753,7 @@ class $$MatchTableTableTableManager required String gameId, Value groupId = const Value.absent(), required String name, + Value isTeamMatch = const Value.absent(), required String notes, required DateTime createdAt, Value endedAt = const Value.absent(), @@ -4595,6 +4763,7 @@ class $$MatchTableTableTableManager gameId: gameId, groupId: groupId, name: name, + isTeamMatch: isTeamMatch, notes: notes, createdAt: createdAt, endedAt: endedAt, @@ -5109,6 +5278,8 @@ typedef $$TeamTableTableCreateCompanionBuilder = required String id, required String name, required DateTime createdAt, + Value color, + Value score, Value rowid, }); typedef $$TeamTableTableUpdateCompanionBuilder = @@ -5116,6 +5287,8 @@ typedef $$TeamTableTableUpdateCompanionBuilder = Value id, Value name, Value createdAt, + Value color, + Value score, Value rowid, }); @@ -5171,6 +5344,16 @@ class $$TeamTableTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnFilters get color => $composableBuilder( + column: $table.color, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get score => $composableBuilder( + column: $table.score, + builder: (column) => ColumnFilters(column), + ); + Expression playerMatchTableRefs( Expression Function($$PlayerMatchTableTableFilterComposer f) f, ) { @@ -5220,6 +5403,16 @@ class $$TeamTableTableOrderingComposer column: $table.createdAt, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get color => $composableBuilder( + column: $table.color, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get score => $composableBuilder( + column: $table.score, + builder: (column) => ColumnOrderings(column), + ); } class $$TeamTableTableAnnotationComposer @@ -5240,6 +5433,12 @@ class $$TeamTableTableAnnotationComposer GeneratedColumn get createdAt => $composableBuilder(column: $table.createdAt, builder: (column) => column); + GeneratedColumn get color => + $composableBuilder(column: $table.color, builder: (column) => column); + + GeneratedColumn get score => + $composableBuilder(column: $table.score, builder: (column) => column); + Expression playerMatchTableRefs( Expression Function($$PlayerMatchTableTableAnnotationComposer a) f, ) { @@ -5297,11 +5496,15 @@ class $$TeamTableTableTableManager Value id = const Value.absent(), Value name = const Value.absent(), Value createdAt = const Value.absent(), + Value color = const Value.absent(), + Value score = const Value.absent(), Value rowid = const Value.absent(), }) => TeamTableCompanion( id: id, name: name, createdAt: createdAt, + color: color, + score: score, rowid: rowid, ), createCompanionCallback: @@ -5309,11 +5512,15 @@ class $$TeamTableTableTableManager required String id, required String name, required DateTime createdAt, + Value color = const Value.absent(), + Value score = const Value.absent(), Value rowid = const Value.absent(), }) => TeamTableCompanion.insert( id: id, name: name, createdAt: createdAt, + color: color, + score: score, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/lib/data/db/tables/match_table.dart b/lib/data/db/tables/match_table.dart index c565547..221128e 100644 --- a/lib/data/db/tables/match_table.dart +++ b/lib/data/db/tables/match_table.dart @@ -12,6 +12,7 @@ class MatchTable extends Table { .references(GroupTable, #id, onDelete: KeyAction.setNull) .nullable()(); TextColumn get name => text()(); + BoolColumn get isTeamMatch => boolean().withDefault(const Constant(false))(); TextColumn get notes => text()(); DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get endedAt => dateTime().nullable()(); diff --git a/lib/data/db/tables/team_table.dart b/lib/data/db/tables/team_table.dart index b1a24a9..4f6ce21 100644 --- a/lib/data/db/tables/team_table.dart +++ b/lib/data/db/tables/team_table.dart @@ -4,6 +4,8 @@ class TeamTable extends Table { TextColumn get id => text()(); TextColumn get name => text()(); DateTimeColumn get createdAt => dateTime()(); + TextColumn get color => text().withDefault(const Constant('blue'))(); + IntColumn get score => integer().withDefault(const Constant(0))(); @override Set> get primaryKey => {id}; diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 679f8a4..895070f 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -16,6 +16,7 @@ class Match { final Game game; final Group? group; final List players; + final bool isTeamMatch; final List? teams; final String notes; Map scores; @@ -26,6 +27,7 @@ class Match { required this.players, this.endedAt, this.group, + this.isTeamMatch = false, this.teams, this.notes = '', String? id, @@ -37,7 +39,7 @@ class Match { @override String toString() { - return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}'; + return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, isTeamMatch: $isTeamMatch, teams: $teams, notes: $notes, scores: $scores, mvp: $mvp}'; } Match copyWith({ @@ -48,6 +50,7 @@ class Match { Game? game, Group? group, List? players, + bool? isTeamMatch, List? teams, String? notes, Map? scores, @@ -60,6 +63,7 @@ class Match { game: game ?? this.game, group: group ?? this.group, players: players ?? this.players, + isTeamMatch: isTeamMatch ?? this.isTeamMatch, teams: teams ?? this.teams, notes: notes ?? this.notes, scores: scores ?? this.scores, @@ -78,6 +82,7 @@ class Match { game == other.game && group == other.group && const DeepCollectionEquality().equals(players, other.players) && + isTeamMatch == other.isTeamMatch && const DeepCollectionEquality().equals(teams, other.teams) && notes == other.notes && const DeepCollectionEquality().equals(scores, other.scores); @@ -91,6 +96,7 @@ class Match { game, group, const DeepCollectionEquality().hash(players), + isTeamMatch, const DeepCollectionEquality().hash(teams), notes, const DeepCollectionEquality().hash(scores), @@ -112,6 +118,7 @@ class Match { ), group = null, players = [], + isTeamMatch = json['isTeamMatch'], teams = [], scores = json['scores'] != null ? (json['scores'] as Map).map( @@ -133,11 +140,13 @@ class Match { 'gameId': game.id, 'groupId': group?.id, 'playerIds': players.map((player) => player.id).toList(), + 'isTeamMatch': isTeamMatch, 'teams': teams?.map((team) => team.toJson()).toList(), 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'notes': notes, }; + // Most Valuable Player(s) based on the match's ruleset List get mvp { if (players.isEmpty || scores.isEmpty) return []; @@ -195,4 +204,49 @@ class Match { return playerScore.score == lowestScore; }).toList(); } + + // MVP for team-based matches (Most Valuable Team) + List get mvt { + if (teams == null || teams!.isEmpty) return []; + + switch (game.ruleset) { + case Ruleset.highestScore: + return _getHighestScoreTeam(); + + case Ruleset.lowestScore: + return _getLowestScoreTeam(); + + case Ruleset.singleWinner: + return _getHighestScoreTeam().take(1).toList(); + + case Ruleset.singleLoser: + return _getLowestScoreTeam().take(1).toList(); + + case Ruleset.multipleWinners: + return _getHighestScoreTeam(); + + case Ruleset.placement: + return _getHighestScoreTeam().take(1).toList(); + } + } + + List _getHighestScoreTeam() { + final int highestScore = teams! + .map((team) => team.score) + .reduce((max, score) => score > max ? score : max); + + return teams!.where((team) { + return team.score == highestScore; + }).toList(); + } + + List _getLowestScoreTeam() { + final int lowestScore = teams! + .map((team) => team.score) + .reduce((min, score) => score < min ? score : min); + + return teams!.where((team) { + return team.score == lowestScore; + }).toList(); + } } diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index b16e2ec..ac82425 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -1,5 +1,6 @@ import 'package:clock/clock.dart'; import 'package:collection/collection.dart'; +import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/player.dart'; import 'package:uuid/uuid.dart'; @@ -7,31 +8,39 @@ class Team { final String id; final String name; final DateTime createdAt; + final GameColor color; + final int score; final List members; Team({ String? id, required this.name, DateTime? createdAt, + this.color = GameColor.blue, + this.score = 0, required this.members, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(); @override String toString() { - return 'Team{id: $id, name: $name, members: $members}'; + return 'Team{id: $id, name: $name, color: $color, score: $score, members: $members}'; } Team copyWith({ String? id, String? name, DateTime? createdAt, + GameColor? color, + int? score, List? members, }) { return Team( id: id ?? this.id, name: name ?? this.name, createdAt: createdAt ?? this.createdAt, + color: color ?? this.color, + score: score ?? this.score, members: members ?? this.members, ); } @@ -44,6 +53,8 @@ class Team { id == other.id && name == other.name && createdAt == other.createdAt && + color == other.color && + score == other.score && const DeepCollectionEquality().equals(members, other.members); @override @@ -51,6 +62,8 @@ class Team { id, name, createdAt, + color, + score, const DeepCollectionEquality().hash(members), ); @@ -58,12 +71,16 @@ class Team { : id = json['id'], name = json['name'], createdAt = DateTime.parse(json['createdAt']), + color = GameColor.values.byName(json['color'] ?? GameColor.blue.name), + score = json['score'] ?? 0, members = []; // Populated during import via DataTransferService Map toJson() => { 'id': id, 'name': name, 'createdAt': createdAt.toIso8601String(), + 'color': color.name, + 'score': score, 'memberIds': members.map((member) => member.id).toList(), }; } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 14908b6..785baaf 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -12,6 +12,7 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/player_selection.dart'; @@ -59,6 +60,7 @@ class _CreateMatchViewState extends State { Group? selectedGroup; Game? selectedGame; + bool isTeamMatch = false; List selectedPlayers = []; /// GlobalKey for ScaffoldMessenger to show snackbars @@ -135,24 +137,7 @@ class _CreateMatchViewState extends State { trailing: selectedGame == null ? Text(loc.none_group) : Text(selectedGame!.name), - onPressed: () async { - selectedGame = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseGameView( - games: gamesList, - initialGameId: selectedGame?.id ?? '', - onGamesUpdated: widget.onMatchesUpdated, - ), - ), - ); - setState(() { - if (selectedGame != null) { - hintText = selectedGame!.name; - } else { - hintText = loc.match_name; - } - }); - }, + onPressed: () async => await onChoosingGame(), ), // Group selection tile. @@ -161,37 +146,20 @@ class _CreateMatchViewState extends State { trailing: selectedGroup == null ? Text(loc.none_group) : Text(selectedGroup!.name), - onPressed: () async { - // Remove all players from the previously selected group from - // the selected players list, in case the user deselects the - // group or selects a different group. - selectedPlayers.removeWhere( - (player) => - selectedGroup?.members.any( - (member) => member.id == player.id, - ) ?? - false, - ); - - selectedGroup = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseGroupView( - groups: groupsList, - initialGroupId: selectedGroup?.id ?? '', - ), - ), - ); - - setState(() { - if (selectedGroup != null) { - setState(() { - selectedPlayers += [...selectedGroup!.members]; - }); - } - }); - }, + onPressed: () async => onChoosingGroup(), ), + if (!isEditMode()) + ChooseTile( + title: 'Team Match', + trailing: Switch.adaptive( + activeTrackColor: CustomTheme.primaryColor, + padding: const EdgeInsets.symmetric(vertical: -15), + value: isTeamMatch, + onChanged: (value) => setState(() => isTeamMatch = value), + ), + ), + // Player selection widget. Expanded( child: PlayerSelection( @@ -211,9 +179,9 @@ class _CreateMatchViewState extends State { text: buttonText, sizeRelativeToWidth: 0.95, buttonType: ButtonType.primary, - onPressed: _enableCreateGameButton() + onPressed: isSubmitButtonEnabled() ? () { - buttonNavigation(context); + submitButtonNavigation(context); } : null, ), @@ -228,12 +196,86 @@ class _CreateMatchViewState extends State { return widget.matchToEdit != null; } + // If a match was provided to the view, this method prefills the input fields + void prefillMatchDetails() { + final match = widget.matchToEdit!; + _matchNameController.text = match.name; + selectedPlayers = match.players; + selectedGame = match.game; + + if (match.group != null) { + selectedGroup = match.group; + } + } + + Future onChoosingGame() async { + selectedGame = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseGameView( + games: gamesList, + initialGameId: selectedGame?.id ?? '', + onGamesUpdated: widget.onMatchesUpdated, + ), + ), + ); + setState(() { + if (selectedGame != null) { + hintText = selectedGame!.name; + } else { + hintText = AppLocalizations.of(context).match_name; + } + }); + } + + Future onChoosingGroup() async { + // Remove all players from the previously selected group from + // the selected players list, in case the user deselects the + // group or selects a different group. + selectedPlayers.removeWhere( + (player) => + selectedGroup?.members.any((member) => member.id == player.id) ?? + false, + ); + + selectedGroup = await Navigator.of(context).push( + adaptivePageRoute( + builder: (context) => ChooseGroupView( + groups: groupsList, + initialGroupId: selectedGroup?.id ?? '', + ), + ), + ); + + setState(() { + if (selectedGroup != null) { + setState(() { + selectedPlayers += [...selectedGroup!.members]; + }); + } + }); + } + + // If none of the selected players are from the currently selected group, + // the group is also deselected. + Future removeGroupWhenNoMemberLeft() async { + if (selectedGroup == null) return; + + if (!selectedPlayers.any( + (player) => + selectedGroup!.members.any((member) => member.id == player.id), + )) { + setState(() { + selectedGroup = null; + }); + } + } + /// Determines whether the "Create Match" button should be enabled. /// /// Returns `true` if: /// - A ruleset is selected AND /// - Either a group is selected OR at least 2 players are selected. - bool _enableCreateGameButton() { + bool isSubmitButtonEnabled() { return (selectedGroup != null || (selectedPlayers.length > 1) && selectedGame != null); } @@ -242,20 +284,32 @@ class _CreateMatchViewState extends State { /// /// If a match is being edited, updates the match in the database. /// Otherwise, creates a new match and navigates to the MatchResultView. - void buttonNavigation(BuildContext context) async { + void submitButtonNavigation(BuildContext context) async { if (isEditMode()) { await updateMatch(); if (context.mounted) { Navigator.pop(context); } - } else { - final match = await createMatch(); + } + final match = await createMatch(); + + if (isTeamMatch) { if (context.mounted) { Navigator.pushReplacement( context, adaptivePageRoute( - fullscreenDialog: true, + fullscreenDialog: !isTeamMatch, + builder: (context) => OrganizeTeamsView(match: match), + ), + ); + } + } else { + if (context.mounted) { + Navigator.pushReplacement( + context, + adaptivePageRoute( + fullscreenDialog: !isTeamMatch, builder: (context) => MatchResultView( match: match, onWinnerChanged: widget.onWinnerChanged, @@ -328,36 +382,10 @@ class _CreateMatchViewState extends State { createdAt: DateTime.now(), group: selectedGroup, players: selectedPlayers, + isTeamMatch: isTeamMatch, game: selectedGame!, ); await db.matchDao.addMatch(match: match); return match; } - - // If a match was provided to the view, this method prefills the input fields - void prefillMatchDetails() { - final match = widget.matchToEdit!; - _matchNameController.text = match.name; - selectedPlayers = match.players; - selectedGame = match.game; - - if (match.group != null) { - selectedGroup = match.group; - } - } - - // If none of the selected players are from the currently selected group, - // the group is also deselected. - Future removeGroupWhenNoMemberLeft() async { - if (selectedGroup == null) return; - - if (!selectedPlayers.any( - (player) => - selectedGroup!.members.any((member) => member.id == player.id), - )) { - setState(() { - selectedGroup = null; - }); - } - } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart new file mode 100644 index 0000000..7169857 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart @@ -0,0 +1,263 @@ +import 'dart:math'; + +import 'package:flutter/material.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/core/enums.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/team.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; + +class OrganizeTeamsView extends StatefulWidget { + const OrganizeTeamsView({super.key, required this.match}); + + final Match match; + + @override + State createState() => _OrganizeTeamsViewState(); +} + +class _OrganizeTeamsViewState extends State { + final Random _random = Random(); + late final List<_TeamDraft> _teams; + + List get _players => widget.match.players; + + @override + void initState() { + super.initState(); + _teams = List.generate(2, _createTeamDraft); + _redistributePlayers(); + } + + @override + void dispose() { + for (final team in _teams) { + team.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: const Text('Teams organisieren')), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 12, bottom: 12), + itemCount: _teams.length, + itemBuilder: (context, index) { + return TeamCreationTile( + color: _teams[index].color, + controller: _teams[index].nameController, + players: _teams[index].members, + hintText: 'Team ${index + 1}', + onDelete: () => _removeTeam(index), + onColorSelection: (color) { + setState(() { + _teams[index].color = color; + }); + }, + onPlayerTap: (player) => + _showMovePlayerSheet(player, index), + ); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MainMenuButton( + icon: Icons.cached, + onPressed: () => setState(() { + _redistributePlayers(); + }), + ), + const SizedBox(width: 15), + MainMenuButton( + text: 'Add team', + icon: Icons.emoji_events, + onPressed: _teams.length >= widget.match.players.length + ? null + : _addTeam, + ), + const SizedBox(width: 15), + MainMenuButton( + icon: Icons.check, + onPressed: () async { + final match = await createMatchWithTeams(); + if (context.mounted) { + Navigator.pushReplacement( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => MatchResultView(match: match), + ), + ); + } + }, + ), + ], + ), + ], + ), + ), + ); + } + + Future createMatchWithTeams() async { + final teams = _teams + .map( + (team) => Team( + name: team.nameController.text.trim().isNotEmpty + ? team.nameController.text.trim() + : 'Team ${_teams.indexOf(team) + 1}', + color: team.color, + members: team.members, + ), + ) + .toList(); + final db = Provider.of(context, listen: false); + await db.teamDao.addTeamsAsList(teams: teams, matchId: widget.match.id); + return widget.match.copyWith(teams: teams); + } + + _TeamDraft _createTeamDraft(int index) { + return _TeamDraft( + nameController: TextEditingController(text: 'Team ${index + 1}'), + color: getTeamColor(index), + ); + } + + void _addTeam() { + setState(() { + _teams.add(_createTeamDraft(_teams.length)); + _redistributePlayers(); + }); + } + + void _removeTeam(int index) { + setState(() { + final removedTeam = _teams.removeAt(index); + removedTeam.dispose(); + + if (_teams.isEmpty) { + _teams.add(_createTeamDraft(0)); + } + + _redistributePlayers(); + }); + } + + void _movePlayer(Player player, int fromTeamIndex, int toTeamIndex) { + setState(() { + _teams[fromTeamIndex].members.remove(player); + _teams[toTeamIndex].members.add(player); + }); + } + + void _showMovePlayerSheet(Player player, int fromTeamIndex) { + final otherTeams = [ + for (int i = 0; i < _teams.length; i++) + if (i != fromTeamIndex) (index: i, team: _teams[i]), + ]; + + if (otherTeams.isEmpty) return; + + showModalBottomSheet( + context: context, + backgroundColor: CustomTheme.backgroundColor, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (context) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Text( + '${player.name} verschieben in …', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + ), + ), + const Divider(), + ...otherTeams.map((entry) { + final teamName = + entry.team.nameController.text.trim().isNotEmpty + ? entry.team.nameController.text.trim() + : 'Team ${entry.index + 1}'; + final teamColor = getColorFromGameColor(entry.team.color); + return ListTile( + leading: CircleAvatar( + radius: 12, + backgroundColor: teamColor, + ), + title: Text( + teamName, + style: const TextStyle(color: CustomTheme.textColor), + ), + onTap: () { + Navigator.pop(context); + _movePlayer(player, fromTeamIndex, entry.index); + }, + ); + }), + ], + ), + ), + ); + }, + ); + } + + void _redistributePlayers() { + for (final team in _teams) { + team.members.clear(); + } + + if (_players.isEmpty || _teams.isEmpty) { + return; + } + + final shuffledPlayers = [..._players]..shuffle(_random); + + for (int i = 0; i < shuffledPlayers.length; i++) { + final teamIndex = i % _teams.length; + _teams[teamIndex].members.add(shuffledPlayers[i]); + } + } +} + +class _TeamDraft { + _TeamDraft({required this.nameController, required this.color}); + + final TextEditingController nameController; + GameColor color; + final List members = []; + + void dispose() { + nameController.dispose(); + } +} diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 61b2a55..8a9ed02 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; +import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; @@ -36,8 +38,8 @@ class _MatchResultViewState extends State { late final Ruleset ruleset; - /// List of all players who participated in the match late final List allPlayers; + late final List allTeams; /// List of text controllers for score entry, one for each player late final List controller; @@ -45,44 +47,27 @@ class _MatchResultViewState extends State { /// Flag to indicate if the save button should be enabled late bool canSave; + late bool isTeamMatch; + /// Currently selected winner player Player? _selectedPlayer; + Team? _selectedTeam; @override void initState() { db = Provider.of(context, listen: false); ruleset = widget.match.game.ruleset; canSave = !rulesetSupportsScoreEntry(); + isTeamMatch = widget.match.isTeamMatch; + print(widget.match.teams); - allPlayers = widget.match.players; - allPlayers.sort((a, b) => a.name.compareTo(b.name)); - - controller = List.generate( - allPlayers.length, - (index) => TextEditingController()..addListener(() => onTextEnter()), - ); - - // Prefill fields - if (widget.match.mvp.isNotEmpty) { - if (rulesetSupportsWinnerSelection()) { - _selectedPlayer = allPlayers.firstWhere( - (p) => p.id == widget.match.mvp.first.id, - ); - } else if (rulesetSupportsScoreEntry()) { - for (int i = 0; i < allPlayers.length; i++) { - final scoreList = widget.match.scores[allPlayers[i].id]; - final score = scoreList?.score ?? 0; - controller[i].text = score.toString(); - } - } else if (rulesetSupportsPlacement()) { - allPlayers.sort((a, b) { - final scoreA = widget.match.scores[a.id]?.score ?? 0; - final scoreB = widget.match.scores[b.id]?.score ?? 0; - return scoreB.compareTo(scoreA); - }); - } - super.initState(); + if (isTeamMatch) { + initializeAsTeamMatch(); + } else { + inizializeAsNormalMatch(); } + + super.initState(); } @override @@ -160,165 +145,16 @@ class _MatchResultViewState extends State { // Show player selection if (rulesetSupportsWinnerSelection()) Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { - setState(() { - _selectedPlayer = value; - }); - }, - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomRadioListTile( - text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { - setState(() { - // Check if the already selected player is the same as the newly tapped player. - if (_selectedPlayer == value) { - // If yes deselected the player by setting it to null. - _selectedPlayer = null; - } else { - // If no assign the newly tapped player to the selected player. - (_selectedPlayer = value); - } - }); - }, - ); - }, - ), - ), + child: buildWinnerSelectionWidget(isTeamMatch), ), // Show score entry if (rulesetSupportsScoreEntry()) - Expanded( - child: ListView.separated( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return ScoreListTile( - text: allPlayers[index].name, - controller: controller[index], - ); - }, - separatorBuilder: - (BuildContext context, int index) { - return const Padding( - padding: EdgeInsets.symmetric( - vertical: 8.0, - ), - child: Divider(indent: 20), - ); - }, - ), - ), + Expanded(child: buildScoreEntryWidget(isTeamMatch)), // Show draggable placement list if (rulesetSupportsPlacement()) - Expanded( - child: Row( - children: [ - // Placement indicators - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Column( - children: [ - for ( - int i = 0; - i < allPlayers.length; - i++ - ) - Container( - alignment: Alignment.center, - height: 60, - child: Container( - decoration: BoxDecoration( - color: - CustomTheme.boxBorderColor, - borderRadius: CustomTheme - .standardBorderRadiusAll, - ), - alignment: Alignment.center, - height: 50, - width: 50, - child: Text( - ' #${i + 1} ', - style: const TextStyle( - color: CustomTheme.textColor, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - ), - ], - ), - ), - - // Drag list - Expanded( - child: ReorderableListView.builder( - physics: - const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - proxyDecorator: (child, index, animation) { - return AnimatedBuilder( - animation: animation, - child: child, - builder: (context, child) { - final alpha = - (Curves.easeInOut.transform( - animation.value, - ) * - 40) - .toInt(); - return Stack( - children: [ - child!, - Positioned.fill( - left: 4, - top: 4, - right: 4, - bottom: 4, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.white - .withAlpha(alpha), - borderRadius: CustomTheme - .standardBorderRadiusAll, - ), - ), - ), - ], - ); - }, - ); - }, - onReorder: (int oldIndex, int newIndex) { - setState(() { - if (newIndex > oldIndex) { - newIndex -= 1; - } - final Player item = allPlayers - .removeAt(oldIndex); - allPlayers.insert(newIndex, item); - }); - }, - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return TextIconListTile( - key: ValueKey(allPlayers[index].id), - text: allPlayers[index].name, - icon: Icons.drag_handle, - ); - }, - ), - ), - ], - ), - ), + Expanded(child: buildPlacementWidget(isTeamMatch)), ], ), ), @@ -361,6 +197,63 @@ class _MatchResultViewState extends State { ); } + void initializeAsTeamMatch() { + allTeams = + widget.match.teams ?? + List.generate( + 4, + (index) => Team( + name: 'Team ${index + 1}', + members: [ + Player(name: 'Player ${index + 1}'), + Player(name: 'Player ${index + 2}'), + Player(name: 'Player ${index + 3}'), + Player(name: 'Player ${index + 4}'), + ], + ), + ); + allTeams.sort((a, b) => a.name.compareTo(b.name)); + + controller = List.generate( + allTeams.length, + (index) => TextEditingController()..addListener(() => onTextEnter()), + ); + + // Prefill fields + //TODO + } + + void inizializeAsNormalMatch() { + allPlayers = widget.match.players; + allPlayers.sort((a, b) => a.name.compareTo(b.name)); + + controller = List.generate( + allPlayers.length, + (index) => TextEditingController()..addListener(() => onTextEnter()), + ); + + // Prefill fields + if (widget.match.mvp.isNotEmpty) { + if (rulesetSupportsWinnerSelection()) { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.mvp.first.id, + ); + } else if (rulesetSupportsScoreEntry()) { + for (int i = 0; i < allPlayers.length; i++) { + final scoreList = widget.match.scores[allPlayers[i].id]; + final score = scoreList?.score ?? 0; + controller[i].text = score.toString(); + } + } else if (rulesetSupportsPlacement()) { + allPlayers.sort((a, b) { + final scoreA = widget.match.scores[a.id]?.score ?? 0; + final scoreB = widget.match.scores[b.id]?.score ?? 0; + return scoreB.compareTo(scoreA); + }); + } + } + } + /// Updated [canSave] everytime a text is entered in one of the score entry fields. void onTextEnter() { if (rulesetSupportsScoreEntry()) { @@ -459,4 +352,311 @@ class _MatchResultViewState extends State { bool rulesetSupportsPlacement() { return ruleset == Ruleset.placement; } + + Widget buildTeamTile({required Team team, double? width}) { + return Container( + width: width, + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 2), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: getColorFromGameColor(team.color).withAlpha(30), + border: Border.all(color: getColorFromGameColor(team.color), width: 2), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + team.name, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + for (final member in team.members) + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 4, + ), + decoration: BoxDecoration( + color: CustomTheme.onBoxColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + member.name, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 13, + color: CustomTheme.textColor.withAlpha(180), + ), + ), + ), + ], + ), + ], + ), + ); + } + + Widget buildWinnerSelectionWidget(bool isTeamMatch) { + if (isTeamMatch) { + return RadioGroup( + groupValue: _selectedTeam, + onChanged: (Team? team) async { + setState(() { + _selectedTeam = team; + }); + }, + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allTeams.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + content: buildTeamTile(team: allTeams[index]), + value: allTeams[index], + onContainerTap: (team) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedTeam == team) { + // If yes deselected the player by setting it to null. + _selectedTeam = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedTeam = team); + } + }); + }, + ); + }, + ), + ); + } else { + return RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { + setState(() { + _selectedPlayer = value; + }); + }, + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + content: Text( + allPlayers[index].name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + }, + ); + }, + ), + ); + } + } + + Widget buildScoreEntryWidget(bool isTeamMatch) { + if (isTeamMatch) { + return ListView.separated( + itemCount: allTeams.length, + itemBuilder: (context, index) { + return ScoreListTile( + content: buildTeamTile(team: allTeams[index], width: 220), + horizontalPadding: 0, + controller: controller[index], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(indent: 20), + ); + }, + ); + } else { + return ListView.separated( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return ScoreListTile( + content: Text( + allPlayers[index].name, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500), + ), + controller: controller[index], + ); + }, + separatorBuilder: (BuildContext context, int index) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(indent: 20), + ); + }, + ); + } + } + + Widget buildPlacementWidget(bool isTeamMatch) { + final placementCol = Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Column( + children: [ + for ( + int i = 0; + i < (isTeamMatch ? allTeams.length : allPlayers.length); + i++ + ) + Container( + alignment: Alignment.center, + height: 60, + child: Container( + decoration: BoxDecoration( + color: CustomTheme.boxBorderColor, + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + alignment: Alignment.center, + height: 50, + width: 50, + child: Text( + ' #${i + 1} ', + style: const TextStyle( + color: CustomTheme.textColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + ), + ], + ), + ); + final valueCol = isTeamMatch + ? Expanded( + child: ReorderableListView.builder( + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + child: child, + builder: (context, child) { + final alpha = + (Curves.easeInOut.transform(animation.value) * 40) + .toInt(); + return Stack( + children: [ + child!, + Positioned.fill( + left: 4, + top: 4, + right: 4, + bottom: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withAlpha(alpha), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + ), + ), + ], + ); + }, + ); + }, + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final Team team = allTeams.removeAt(oldIndex); + allTeams.insert(newIndex, team); + }); + }, + itemCount: allTeams.length, + itemBuilder: (context, index) { + return TextIconListTile( + key: ValueKey(allTeams[index].id), + text: allTeams[index].name, + icon: Icons.drag_handle, + color: getColorFromGameColor(allTeams[index].color), + ); + }, + ), + ) + : Expanded( + child: ReorderableListView.builder( + physics: const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + child: child, + builder: (context, child) { + final alpha = + (Curves.easeInOut.transform(animation.value) * 40) + .toInt(); + return Stack( + children: [ + child!, + Positioned.fill( + left: 4, + top: 4, + right: 4, + bottom: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white.withAlpha(alpha), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + ), + ), + ], + ); + }, + ); + }, + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final Player item = allPlayers.removeAt(oldIndex); + allPlayers.insert(newIndex, item); + }); + }, + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return TextIconListTile( + key: ValueKey(allPlayers[index].id), + text: allPlayers[index].name, + icon: Icons.drag_handle, + ); + }, + ), + ); + + return Row(children: [placementCol, valueCol]); + } } diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index c5c7a34..8bb6222 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -16,7 +16,7 @@ class MainMenuButton extends StatefulWidget { }); /// The callback to be invoked when the button is pressed. - final void Function() onPressed; + final void Function()? onPressed; /// The icon of the button. final IconData icon; @@ -31,9 +31,11 @@ class MainMenuButton extends StatefulWidget { } class _MainMenuButtonState extends State - with SingleTickerProviderStateMixin { + with TickerProviderStateMixin { late AnimationController _animationController; + late AnimationController _disabledAnimationController; late Animation _scaleAnimation; + late Animation _disabledScaleAnimation; /// How long the button needs to be pressed to register it as long press Timer? _longPressTimer; @@ -52,37 +54,59 @@ class _MainMenuButtonState extends State vsync: this, ); + _disabledAnimationController = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), ); + + _disabledScaleAnimation = Tween(begin: 1.0, end: 0.98).animate( + CurvedAnimation( + parent: _disabledAnimationController, + curve: Curves.easeInOut, + ), + ); } @override Widget build(BuildContext context) { return ScaleTransition( - scale: _scaleAnimation, + scale: widget.onPressed == null + ? _disabledScaleAnimation + : _scaleAnimation, child: GestureDetector( onTapDown: (_) { - _animationController.forward(); - if (widget.onLongPressed != null) { - _longPressTimer = Timer(const Duration(milliseconds: 400), () { - _isLongPressing = true; - widget.onLongPressed?.call(); - _repeatTimer = Timer.periodic( - const Duration(milliseconds: 250), - (_) => widget.onLongPressed?.call(), - ); - }); + if (widget.onPressed == null) { + _disabledAnimationController.forward(); + } else { + _animationController.forward(); + if (widget.onLongPressed != null) { + _longPressTimer = Timer(const Duration(milliseconds: 400), () { + _isLongPressing = true; + widget.onLongPressed?.call(); + _repeatTimer = Timer.periodic( + const Duration(milliseconds: 250), + (_) => widget.onLongPressed?.call(), + ); + }); + } } }, onTapUp: (_) async { - _cancelTimers(); - if (mounted && !_isLongPressing) { - widget.onPressed(); + if (widget.onPressed == null) { + _disabledAnimationController.reverse(); + } else { + _cancelTimers(); + if (mounted && !_isLongPressing) { + widget.onPressed?.call(); + } + _isLongPressing = false; + await Future.delayed(const Duration(milliseconds: 100)); + await _animationController.reverse(); } - _isLongPressing = false; - await Future.delayed(const Duration(milliseconds: 100)); - await _animationController.reverse(); }, onTapCancel: () { _isLongPressing = false; @@ -91,7 +115,7 @@ class _MainMenuButtonState extends State }, child: Container( decoration: BoxDecoration( - color: Colors.white, + color: widget.onPressed == null ? Colors.grey : Colors.white, borderRadius: BorderRadius.circular(30), ), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), @@ -122,6 +146,7 @@ class _MainMenuButtonState extends State void dispose() { _cancelTimers(); _animationController.dispose(); + _disabledAnimationController.dispose(); super.dispose(); } diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart index b074638..1d56508 100644 --- a/lib/presentation/widgets/text_input/text_input_field.dart +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -57,7 +57,6 @@ class TextInputField extends StatelessWidget { filled: true, fillColor: CustomTheme.boxColor, hintText: hintText, - hintStyle: const TextStyle(fontSize: 18), counterText: showCounterText ? null : '', enabledBorder: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(12)), diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart index 5559d10..1016a14 100644 --- a/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart @@ -8,13 +8,13 @@ class CustomRadioListTile extends StatelessWidget { /// - [onContainerTap]: The callback invoked when the container is tapped. const CustomRadioListTile({ super.key, - required this.text, + required this.content, required this.value, required this.onContainerTap, }); /// The text to display next to the radio button. - final String text; + final Widget content; /// The value associated with the radio button. final T value; @@ -37,16 +37,7 @@ class CustomRadioListTile extends StatelessWidget { child: Row( children: [ Radio(value: value, toggleable: true), - Expanded( - child: Text( - text, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), + Expanded(child: content), ], ), ), diff --git a/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart index e4cfff9..e2970b1 100644 --- a/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart @@ -5,36 +5,34 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; class ScoreListTile extends StatelessWidget { /// A custom list tile widget that has a text field for inputting a score. - /// - [text]: The leading text to be displayed. + /// - [content]: The leading Widget to be displayed. /// - [controller]: The controller for the text field to input the score. const ScoreListTile({ super.key, - required this.text, + required this.content, required this.controller, + this.horizontalPadding = 20, }); - /// The text to display next to the radio button. - final String text; + final Widget content; final TextEditingController controller; + final double horizontalPadding; + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return Container( margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), decoration: const BoxDecoration(color: CustomTheme.boxColor), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - text, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500), - ), + content, SizedBox( width: 100, height: 40, diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index d034763..9274838 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -128,6 +128,12 @@ class _MatchTileState extends State { const SizedBox(height: 12), + Text( + 'team match: ${match.isTeamMatch}', + style: const TextStyle(fontSize: 14, color: Colors.white), + ), + + const SizedBox(height: 12), // Winner / In Progress Info if (match.mvp.isNotEmpty) ...[ Container( diff --git a/lib/presentation/widgets/tiles/team_creation_tile.dart b/lib/presentation/widgets/tiles/team_creation_tile.dart new file mode 100644 index 0000000..fb3e9c5 --- /dev/null +++ b/lib/presentation/widgets/tiles/team_creation_tile.dart @@ -0,0 +1,141 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/constants.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; + +class TeamCreationTile extends StatefulWidget { + const TeamCreationTile({ + super.key, + required this.color, + required this.controller, + required this.players, + required this.hintText, + this.onDelete, + this.onColorSelection, + this.onPlayerTap, + }); + + final GameColor color; + + final List players; + + final TextEditingController controller; + + final String hintText; + + final VoidCallback? onDelete; + + final ValueChanged? onColorSelection; + + final void Function(Player player)? onPlayerTap; + + @override + State createState() => _TeamCreationTileState(); +} + +class _TeamCreationTileState extends State { + @override + Widget build(BuildContext context) { + return Container( + margin: CustomTheme.standardMargin, + padding: const EdgeInsets.all(12), + decoration: CustomTheme.standardBoxDecoration, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextInputField( + controller: widget.controller, + hintText: widget.hintText, + maxLength: Constants.MAX_TEAM_NAME_LENGTH, + ), + ), + const SizedBox(width: 8), + IconButton( + onPressed: () => widget.onDelete?.call(), + icon: const Icon(Icons.delete, size: 24), + ), + ], + ), + const SizedBox(height: 8), + const Text( + 'Color', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: GameColor.values.map((color) { + final isSelected = widget.color == color; + return GestureDetector( + onTap: () { + widget.onColorSelection?.call(color); + }, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: getColorFromGameColor(color), + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.white : Colors.transparent, + width: 3, + ), + ), + child: isSelected + ? const Icon(Icons.check, size: 18, color: Colors.white) + : null, + ), + ); + }).toList(), + ), + const SizedBox(height: 12), + const Text( + 'Players', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + ), + const SizedBox(height: 8), + if (widget.players.isEmpty) + const Text( + 'Keine Spieler:innen zugewiesen', + style: TextStyle(color: CustomTheme.hintColor), + ) + else + Wrap( + spacing: 8, + runSpacing: 8, + children: widget.players + .map( + (player) => GestureDetector( + onTap: () => widget.onPlayerTap?.call(player), + child: TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: widget.onPlayerTap != null, + onIconTap: () => widget.onPlayerTap?.call(player), + ), + ), + ) + .toList(), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart index 04a0803..7684f75 100644 --- a/lib/presentation/widgets/tiles/text_icon_list_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -11,6 +11,7 @@ class TextIconListTile extends StatelessWidget { required this.text, this.suffixText = '', this.icon, + this.color, this.onPressed, }); @@ -23,6 +24,8 @@ class TextIconListTile extends StatelessWidget { /// The icon to display in the tile. final IconData? icon; + final Color? color; + /// The callback to be invoked when the icon is pressed. final VoidCallback? onPressed; @@ -31,7 +34,17 @@ class TextIconListTile extends StatelessWidget { return Container( margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 15), - decoration: CustomTheme.standardBoxDecoration, + decoration: BoxDecoration( + color: + Color.lerp(CustomTheme.onBoxColor, color?.withAlpha(10), 0.1) ?? + CustomTheme.boxColor, + border: Border.all( + color: color ?? CustomTheme.boxBorderColor, + width: color != null ? 2 : 1, + strokeAlign: BorderSide.strokeAlignCenter, + ), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index 541b6ae..08b87cd 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -6,12 +6,14 @@ class TextIconTile extends StatelessWidget { /// - [text]: The text to display in the tile. /// - [iconEnabled]: A boolean to determine if the icon should be displayed. /// - [onIconTap]: The callback to be invoked when the icon is tapped. + /// - [icon]: Optional custom icon. Defaults to [Icons.close]. const TextIconTile({ super.key, required this.text, this.suffixText = '', this.iconEnabled = true, this.onIconTap, + this.icon = Icons.close, }); /// The text to display in the tile. @@ -25,6 +27,9 @@ class TextIconTile extends StatelessWidget { /// The callback to be invoked when the icon is tapped. final VoidCallback? onIconTap; + /// The icon to display. Defaults to [Icons.close]. + final IconData icon; + @override Widget build(BuildContext context) { return Container( @@ -65,10 +70,7 @@ class TextIconTile extends StatelessWidget { ), if (iconEnabled) ...[ const SizedBox(width: 3), - GestureDetector( - onTap: onIconTap, - child: const Icon(Icons.close, size: 20), - ), + GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)), ], ], ), diff --git a/pubspec.yaml b/pubspec.yaml index b4b261e..14c7853 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.30+264 +version: 0.0.30+281 environment: sdk: ^3.8.1 -- 2.49.1 From 2f72b71fdacc1fa8ed032284a1f48729cb48b8f8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 17 May 2026 23:11:18 +0200 Subject: [PATCH 03/51] feat: implemented team score handling --- lib/data/dao/team_dao.dart | 74 +++++++++++ lib/data/db/database.g.dart | 37 +++--- lib/data/db/tables/team_table.dart | 2 +- lib/data/models/match.dart | 10 ++ lib/data/models/team.dart | 4 +- .../match_view/match_result_view.dart | 123 ++++++++++++------ pubspec.yaml | 2 +- 7 files changed, 194 insertions(+), 58 deletions(-) diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index cb61e63..e6a1a0c 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -4,6 +4,7 @@ import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/team.dart'; part 'team_dao.g.dart'; @@ -186,15 +187,42 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /// Updates the score of the team with the given [teamId]. Future updateTeamScore({ required String teamId, + required String matchId, required int score, }) async { + await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + const TeamTableCompanion(score: Value(null)), + ); + await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId); + final rowsAffected = await (update(teamTable)..where((t) => t.id.equals(teamId))).write( TeamTableCompanion(score: Value(score)), ); + + final members = await _getTeamMembers(teamId: teamId); + for (final member in members) { + await db.scoreEntryDao.updateScore( + playerId: member.id, + matchId: matchId, + entry: ScoreEntry(score: score), + ); + } + return rowsAffected > 0; } + Future removeScoreForTeam({ + required String teamId, + required String matchId, + }) async { + await (update(teamTable)..where((t) => t.id.equals(teamId))).write( + const TeamTableCompanion(score: Value(null)), + ); + await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId); + return true; + } + /* Delete */ /// Deletes all teams from the database. @@ -212,4 +240,50 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { final rowsAffected = await query.go(); return rowsAffected > 0; } + + /* Score handling */ + + Future setWinnerTeam({ + required String teamId, + required String matchId, + }) async { + return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1); + } + + Future removeWinnerTeam({ + required String teamId, + required String matchId, + }) async { + return await removeScoreForTeam(teamId: teamId, matchId: matchId); + } + + Future setLoserTeam({ + required String teamId, + required String matchId, + }) async { + return await updateTeamScore(teamId: teamId, matchId: matchId, score: 0); + } + + Future removeLoserTeam({ + required String teamId, + required String matchId, + }) async { + return await removeScoreForTeam(teamId: teamId, matchId: matchId); + } + + Future setTeamPlacements({ + required String teamId, + required String matchId, + required List teams, + }) async { + List success = List.generate(teams.length, (index) => null); + for (int i = 0; i < teams.length; i++) { + success[i] = await updateTeamScore( + matchId: matchId, + teamId: teams[i].id, + score: teams.length - i, + ); + } + return success.every((result) => result == true); + } } diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart index 5827dfb..831aa92 100644 --- a/lib/data/db/database.g.dart +++ b/lib/data/db/database.g.dart @@ -1930,10 +1930,9 @@ class $TeamTableTable extends TeamTable late final GeneratedColumn score = GeneratedColumn( 'score', aliasedName, - false, + true, type: DriftSqlType.int, requiredDuringInsert: false, - defaultValue: const Constant(0), ); @override List get $columns => [id, name, createdAt, color, score]; @@ -2010,7 +2009,7 @@ class $TeamTableTable extends TeamTable score: attachedDatabase.typeMapping.read( DriftSqlType.int, data['${effectivePrefix}score'], - )!, + ), ); } @@ -2025,13 +2024,13 @@ class TeamTableData extends DataClass implements Insertable { final String name; final DateTime createdAt; final String color; - final int score; + final int? score; const TeamTableData({ required this.id, required this.name, required this.createdAt, required this.color, - required this.score, + this.score, }); @override Map toColumns(bool nullToAbsent) { @@ -2040,7 +2039,9 @@ class TeamTableData extends DataClass implements Insertable { map['name'] = Variable(name); map['created_at'] = Variable(createdAt); map['color'] = Variable(color); - map['score'] = Variable(score); + if (!nullToAbsent || score != null) { + map['score'] = Variable(score); + } return map; } @@ -2050,7 +2051,9 @@ class TeamTableData extends DataClass implements Insertable { name: Value(name), createdAt: Value(createdAt), color: Value(color), - score: Value(score), + score: score == null && nullToAbsent + ? const Value.absent() + : Value(score), ); } @@ -2064,7 +2067,7 @@ class TeamTableData extends DataClass implements Insertable { name: serializer.fromJson(json['name']), createdAt: serializer.fromJson(json['createdAt']), color: serializer.fromJson(json['color']), - score: serializer.fromJson(json['score']), + score: serializer.fromJson(json['score']), ); } @override @@ -2075,7 +2078,7 @@ class TeamTableData extends DataClass implements Insertable { 'name': serializer.toJson(name), 'createdAt': serializer.toJson(createdAt), 'color': serializer.toJson(color), - 'score': serializer.toJson(score), + 'score': serializer.toJson(score), }; } @@ -2084,13 +2087,13 @@ class TeamTableData extends DataClass implements Insertable { String? name, DateTime? createdAt, String? color, - int? score, + Value score = const Value.absent(), }) => TeamTableData( id: id ?? this.id, name: name ?? this.name, createdAt: createdAt ?? this.createdAt, color: color ?? this.color, - score: score ?? this.score, + score: score.present ? score.value : this.score, ); TeamTableData copyWithCompanion(TeamTableCompanion data) { return TeamTableData( @@ -2132,7 +2135,7 @@ class TeamTableCompanion extends UpdateCompanion { final Value name; final Value createdAt; final Value color; - final Value score; + final Value score; final Value rowid; const TeamTableCompanion({ this.id = const Value.absent(), @@ -2175,7 +2178,7 @@ class TeamTableCompanion extends UpdateCompanion { Value? name, Value? createdAt, Value? color, - Value? score, + Value? score, Value? rowid, }) { return TeamTableCompanion( @@ -5279,7 +5282,7 @@ typedef $$TeamTableTableCreateCompanionBuilder = required String name, required DateTime createdAt, Value color, - Value score, + Value score, Value rowid, }); typedef $$TeamTableTableUpdateCompanionBuilder = @@ -5288,7 +5291,7 @@ typedef $$TeamTableTableUpdateCompanionBuilder = Value name, Value createdAt, Value color, - Value score, + Value score, Value rowid, }); @@ -5497,7 +5500,7 @@ class $$TeamTableTableTableManager Value name = const Value.absent(), Value createdAt = const Value.absent(), Value color = const Value.absent(), - Value score = const Value.absent(), + Value score = const Value.absent(), Value rowid = const Value.absent(), }) => TeamTableCompanion( id: id, @@ -5513,7 +5516,7 @@ class $$TeamTableTableTableManager required String name, required DateTime createdAt, Value color = const Value.absent(), - Value score = const Value.absent(), + Value score = const Value.absent(), Value rowid = const Value.absent(), }) => TeamTableCompanion.insert( id: id, diff --git a/lib/data/db/tables/team_table.dart b/lib/data/db/tables/team_table.dart index 4f6ce21..e706381 100644 --- a/lib/data/db/tables/team_table.dart +++ b/lib/data/db/tables/team_table.dart @@ -5,7 +5,7 @@ class TeamTable extends Table { TextColumn get name => text()(); DateTimeColumn get createdAt => dateTime()(); TextColumn get color => text().withDefault(const Constant('blue'))(); - IntColumn get score => integer().withDefault(const Constant(0))(); + IntColumn get score => integer().nullable()(); @override Set> get primaryKey => {id}; diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 895070f..f7c81a2 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -231,8 +231,13 @@ class Match { } List _getHighestScoreTeam() { + if (teams!.every((team) => team.score == null)) { + return []; + } + final int highestScore = teams! .map((team) => team.score) + .whereType() .reduce((max, score) => score > max ? score : max); return teams!.where((team) { @@ -241,8 +246,13 @@ class Match { } List _getLowestScoreTeam() { + if (teams!.every((team) => team.score == null)) { + return []; + } + final int lowestScore = teams! .map((team) => team.score) + .whereType() .reduce((min, score) => score < min ? score : min); return teams!.where((team) { diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index ac82425..aa78df7 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -9,7 +9,7 @@ class Team { final String name; final DateTime createdAt; final GameColor color; - final int score; + final int? score; final List members; Team({ @@ -17,7 +17,7 @@ class Team { required this.name, DateTime? createdAt, this.color = GameColor.blue, - this.score = 0, + this.score, required this.members, }) : id = id ?? const Uuid().v4(), createdAt = createdAt ?? clock.now(); diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 8a9ed02..296a9ef 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -198,20 +198,7 @@ class _MatchResultViewState extends State { } void initializeAsTeamMatch() { - allTeams = - widget.match.teams ?? - List.generate( - 4, - (index) => Team( - name: 'Team ${index + 1}', - members: [ - Player(name: 'Player ${index + 1}'), - Player(name: 'Player ${index + 2}'), - Player(name: 'Player ${index + 3}'), - Player(name: 'Player ${index + 4}'), - ], - ), - ); + allTeams = widget.match.teams ?? []; allTeams.sort((a, b) => a.name.compareTo(b.name)); controller = List.generate( @@ -220,7 +207,26 @@ class _MatchResultViewState extends State { ); // Prefill fields - //TODO + if (widget.match.mvt.isNotEmpty) { + if (rulesetSupportsWinnerSelection()) { + _selectedTeam = allTeams.firstWhere( + (p) => p.id == widget.match.mvt.first.id, + ); + } else if (rulesetSupportsScoreEntry()) { + for (int i = 0; i < allTeams.length; i++) { + final score = allTeams[i].score; + controller[i].text = score.toString(); + } + } else if (rulesetSupportsPlacement()) { + allTeams.sort((a, b) { + final scoreA = + allTeams.where((team) => a.id == team.id).first.score ?? 0; + final scoreB = + allTeams.where((team) => b.id == team.id).first.score ?? 0; + return scoreB.compareTo(scoreA); + }); + } + } } void inizializeAsNormalMatch() { @@ -282,41 +288,84 @@ class _MatchResultViewState extends State { /// Handles saving or removing the winner in the database. Future _handleWinner() async { - if (_selectedPlayer == null) { - return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + if (isTeamMatch) { + if (_selectedTeam == null) { + return await db.teamDao.setWinnerTeam( + matchId: widget.match.id, + teamId: _selectedTeam!.id, + ); + } else { + return await db.teamDao.setLoserTeam( + matchId: widget.match.id, + teamId: _selectedTeam!.id, + ); + } } else { - return await db.scoreEntryDao.setWinner( - matchId: widget.match.id, - playerId: _selectedPlayer!.id, - ); + if (_selectedPlayer == null) { + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setWinner( + matchId: widget.match.id, + playerId: _selectedPlayer!.id, + ); + } } } /// Handles saving or removing the loser in the database. Future _handleLoser() async { - if (_selectedPlayer == null) { - return await db.scoreEntryDao.removeLoser(matchId: widget.match.id); + if (isTeamMatch) { + if (_selectedTeam == null) { + return await db.teamDao.removeLoserTeam( + matchId: widget.match.id, + teamId: _selectedTeam!.id, + ); + } else { + return await db.teamDao.setLoserTeam( + matchId: widget.match.id, + teamId: _selectedTeam!.id, + ); + } } else { - return await db.scoreEntryDao.setLoser( - matchId: widget.match.id, - playerId: _selectedPlayer!.id, - ); + if (_selectedPlayer == null) { + return await db.scoreEntryDao.removeLoser(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setLoser( + matchId: widget.match.id, + playerId: _selectedPlayer!.id, + ); + } } } /// Handles saving the scores for each player in the database. Future _handleScores() async { - for (int i = 0; i < allPlayers.length; i++) { - var text = controller[i].text; - if (text.isEmpty) { - text = '0'; + if (isTeamMatch) { + for (int i = 0; i < allTeams.length; i++) { + var text = controller[i].text; + if (text.isEmpty) { + text = '0'; + } + final score = int.parse(text); + await db.teamDao.updateTeamScore( + matchId: widget.match.id, + teamId: allTeams[i].id, + score: score, + ); + } + } else { + for (int i = 0; i < allPlayers.length; i++) { + var text = controller[i].text; + if (text.isEmpty) { + text = '0'; + } + final score = int.parse(text); + await db.scoreEntryDao.addScore( + matchId: widget.match.id, + playerId: allPlayers[i].id, + entry: ScoreEntry(roundNumber: 0, score: score, change: 0), + ); } - final score = int.parse(text); - await db.scoreEntryDao.addScore( - matchId: widget.match.id, - playerId: allPlayers[i].id, - entry: ScoreEntry(roundNumber: 0, score: score, change: 0), - ); } } diff --git a/pubspec.yaml b/pubspec.yaml index 14c7853..91bc4e4 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.30+281 +version: 0.0.30+285 environment: sdk: ^3.8.1 -- 2.49.1 From 2de8cef18dee21340a27a93f11ce6e93767499ff Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 17 May 2026 23:35:02 +0200 Subject: [PATCH 04/51] feat: implemented teams im match tile --- .../widgets/tiles/match_tile.dart | 130 ++++++++++++++++-- pubspec.yaml | 2 +- 2 files changed, 122 insertions(+), 10 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 9274838..8337c59 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -128,14 +128,41 @@ class _MatchTileState extends State { const SizedBox(height: 12), - Text( - 'team match: ${match.isTeamMatch}', - style: const TextStyle(fontSize: 14, color: Colors.white), - ), - - const SizedBox(height: 12), // Winner / In Progress Info - if (match.mvp.isNotEmpty) ...[ + if (match.isTeamMatch && match.mvt.isNotEmpty) ...[ + Container( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 12, + ), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.green.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Row( + children: [ + getMvpIcon(), + const SizedBox(width: 8), + Expanded( + child: Text( + getMvtText(loc), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: CustomTheme.textColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + ] else if (match.mvp.isNotEmpty) ...[ Container( padding: const EdgeInsets.symmetric( vertical: 8, @@ -207,8 +234,69 @@ class _MatchTileState extends State { const SizedBox(height: 12), ], - // Players List - if (players.isNotEmpty && widget.compact == false) ...[ + if (match.teams != null && match.teams!.isNotEmpty) ...[ + const Text( + 'Teams', + style: TextStyle( + fontSize: 13, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var team in match.teams!) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + vertical: 6, + horizontal: 12, + ), + decoration: BoxDecoration( + color: getColorFromGameColor( + team.color, + ).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: getColorFromGameColor( + team.color, + ).withValues(alpha: 0.3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + team.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: CustomTheme.textColor, + ), + ), + Wrap( + spacing: 6, + runSpacing: 6, + children: team.members.map((player) { + return TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: false, + ); + }).toList(), + ), + ], + ), + ), + const SizedBox(height: 12), + ], + ], + ), + const SizedBox(height: 12), + ] else if (players.isNotEmpty && widget.compact == false) ...[ Text( loc.players, style: const TextStyle( @@ -274,6 +362,30 @@ class _MatchTileState extends State { return '${loc.winner}: n.A.'; } + String getMvtText(AppLocalizations loc) { + if (widget.match.mvt.isEmpty) return ''; + final ruleset = widget.match.game.ruleset; + + if (ruleset == Ruleset.singleWinner) { + return '${loc.winner}: ${widget.match.mvt.first.name}'; + } else if (ruleset == Ruleset.singleLoser) { + return '${loc.loser}: ${widget.match.mvt.first.name}'; + } else if (ruleset == Ruleset.highestScore || + ruleset == Ruleset.lowestScore) { + final mvt = widget.match.mvt; + final mvtScore = + widget.match.teams! + .firstWhere((team) => team.id == mvt.first.id) + .score ?? + 0; + final mvtNames = mvt.map((team) => team.name).join(', '); + return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})'; + } else if (ruleset == Ruleset.placement) { + return '${loc.winner}: ${widget.match.mvt.first.name}'; + } + return '${loc.winner}: n.A.'; + } + Icon getMvpIcon() { final icon = getRulesetIcon(widget.match.game.ruleset); diff --git a/pubspec.yaml b/pubspec.yaml index 91bc4e4..33dc417 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.30+285 +version: 0.0.30+287 environment: sdk: ^3.8.1 -- 2.49.1 From d9e0cdf5066777b1bfebf563ba6c30ae6106ca17 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 00:02:59 +0200 Subject: [PATCH 05/51] feat: implemented team card --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 + lib/l10n/generated/app_localizations_de.dart | 3 + lib/l10n/generated/app_localizations_en.dart | 3 + .../match_view/match_detail_view.dart | 55 ++++++--- .../match_view/match_result_view.dart | 5 +- lib/presentation/widgets/cards/team_card.dart | 104 ++++++++++++++++++ .../widgets/tiles/match_tile.dart | 74 ++++--------- pubspec.yaml | 2 +- 10 files changed, 182 insertions(+), 72 deletions(-) create mode 100644 lib/presentation/widgets/cards/team_card.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 5492bab..7bc117f 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -132,6 +132,7 @@ "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "teams": "Teams", "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 7fb944b..48d4eec 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -141,6 +141,7 @@ } } }, + "teams": "Teams", "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", "this_cannot_be_undone": "This can't be undone.", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index bfdb659..f60a19d 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -842,6 +842,12 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); + /// No description provided for @teams. + /// + /// In en, this message translates to: + /// **'Teams'** + String get teams; + /// No description provided for @there_are_no_games_matching_your_search. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 8567ba0..1480887 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -404,6 +404,9 @@ class AppLocalizationsDe extends AppLocalizations { return 'Spieler:in $playerName erfolgreich hinzugefügt'; } + @override + String get teams => 'Teams'; + @override String get there_are_no_games_matching_your_search => 'Es gibt keine Spielvorlagen, die deiner Suche entspricht'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 04e68b4..738bb19 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -404,6 +404,9 @@ class AppLocalizationsEn extends AppLocalizations { return 'Successfully added player $playerName'; } + @override + String get teams => 'Teams'; + @override String get there_are_no_games_matching_your_search => 'There are no games matching your search'; 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 86c26c6..90619dd 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 @@ -12,6 +12,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/cards/team_card.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart'; @@ -153,25 +154,43 @@ class _MatchDetailViewState extends State { const SizedBox(height: 20), ], - // Players - InfoTile( - title: loc.players, - icon: Icons.people, - horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: match.players.map((player) { - return TextIconTile( - text: player.name, - suffixText: getNameCountText(player), - iconEnabled: false, - ); - }).toList(), + if (match.isTeamMatch) ...[ + // Teams + InfoTile( + title: loc.teams, + icon: Icons.scoreboard, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: match.teams!.map((team) { + return TeamCard(team: team); + }).toList(), + ), ), - ), + ] else ...[ + // Players + InfoTile( + title: loc.players, + icon: Icons.people, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: match.players.map((player) { + return TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: false, + ); + }).toList(), + ), + ), + ], const SizedBox(height: 15), // Game diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 296a9ef..27a1e39 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -59,7 +59,6 @@ class _MatchResultViewState extends State { ruleset = widget.match.game.ruleset; canSave = !rulesetSupportsScoreEntry(); isTeamMatch = widget.match.isTeamMatch; - print(widget.match.teams); if (isTeamMatch) { initializeAsTeamMatch(); @@ -290,12 +289,12 @@ class _MatchResultViewState extends State { Future _handleWinner() async { if (isTeamMatch) { if (_selectedTeam == null) { - return await db.teamDao.setWinnerTeam( + return await db.teamDao.removeWinnerTeam( matchId: widget.match.id, teamId: _selectedTeam!.id, ); } else { - return await db.teamDao.setLoserTeam( + return await db.teamDao.setWinnerTeam( matchId: widget.match.id, teamId: _selectedTeam!.id, ); diff --git a/lib/presentation/widgets/cards/team_card.dart b/lib/presentation/widgets/cards/team_card.dart new file mode 100644 index 0000000..43ef842 --- /dev/null +++ b/lib/presentation/widgets/cards/team_card.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/models/team.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; + +class TeamCard extends StatelessWidget { + const TeamCard({ + super.key, + required this.team, + this.compact = false, + this.width = double.infinity, + }); + + final Team team; + + final bool compact; + + final double width; + + @override + Widget build(BuildContext context) { + final teamColor = getColorFromGameColor(team.color); + + if (compact) { + return Container( + width: width, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: teamColor.withAlpha(50), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: teamColor, width: 2), + ), + child: Row( + children: [ + Expanded( + child: Text( + team.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.white, + ), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + Container( + width: 1, + height: 14, + color: Colors.white.withValues(alpha: 0.35), + ), + const SizedBox(width: 8), + const Icon(Icons.people_alt_rounded, size: 14, color: Colors.white), + const SizedBox(width: 4), + Text( + '${team.members.length}', + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ); + } else { + return Container( + width: width, + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + decoration: BoxDecoration( + color: teamColor.withAlpha(50), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: teamColor, width: 2), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 6, + children: [ + Text( + team.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: CustomTheme.textColor, + ), + ), + Wrap( + spacing: 6, + runSpacing: 6, + children: team.members.map((player) { + return TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: false, + ); + }).toList(), + ), + ], + ), + ); + } + } +} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 8337c59..a9ffeb3 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -7,6 +7,7 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/cards/team_card.dart'; import 'package:tallee/presentation/widgets/game_label.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -244,56 +245,29 @@ class _MatchTileState extends State { ), ), const SizedBox(height: 8), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (var team in match.teams!) ...[ - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - vertical: 6, - horizontal: 12, - ), - decoration: BoxDecoration( - color: getColorFromGameColor( - team.color, - ).withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: getColorFromGameColor( - team.color, - ).withValues(alpha: 0.3), - width: 1, - ), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - team.name, - style: const TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - color: CustomTheme.textColor, - ), - ), - Wrap( - spacing: 6, - runSpacing: 6, - children: team.members.map((player) { - return TextIconTile( - text: player.name, - suffixText: getNameCountText(player), - iconEnabled: false, - ); - }).toList(), - ), - ], - ), - ), - const SizedBox(height: 12), - ], - ], + LayoutBuilder( + builder: (context, constraints) { + final useSingleColumn = match.teams!.any( + (team) => team.name.length > 14, + ); + + const spacing = 8.0; + final itemWidth = useSingleColumn + ? constraints.maxWidth + : (constraints.maxWidth - spacing) / 2; + + return Wrap( + spacing: spacing, + runSpacing: spacing, + children: match.teams!.map((team) { + return TeamCard( + team: team, + compact: true, + width: itemWidth, + ); + }).toList(), + ); + }, ), const SizedBox(height: 12), ] else if (players.isNotEmpty && widget.compact == false) ...[ diff --git a/pubspec.yaml b/pubspec.yaml index 33dc417..595d1d7 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.30+287 +version: 0.0.30+294 environment: sdk: ^3.8.1 -- 2.49.1 From 07703037f2e59671c5b5b84ec47a09971ccb70b7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 00:26:49 +0200 Subject: [PATCH 06/51] fix: input var --- lib/data/dao/team_dao.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index e6a1a0c..e85e76f 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -272,7 +272,6 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { } Future setTeamPlacements({ - required String teamId, required String matchId, required List teams, }) async { -- 2.49.1 From 418a6307a00a226ec37ff34413017b1cef9c84d6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 00:26:55 +0200 Subject: [PATCH 07/51] fix: title --- .../main_menu/match_view/create_match/organize_teams_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart index 7169857..b9def83 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart @@ -48,7 +48,7 @@ class _OrganizeTeamsViewState extends State { Widget build(BuildContext context) { return Scaffold( backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: const Text('Teams organisieren')), + appBar: AppBar(title: const Text('Organize Teams')), body: SafeArea( child: Column( children: [ -- 2.49.1 From 092dd5ec0a26f5ae02f1fac681a106f770dbf2a0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 00:27:01 +0200 Subject: [PATCH 08/51] fix: loc --- lib/presentation/widgets/tiles/match_tile.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index a9ffeb3..5dd7d37 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -236,9 +236,9 @@ class _MatchTileState extends State { ], if (match.teams != null && match.teams!.isNotEmpty) ...[ - const Text( - 'Teams', - style: TextStyle( + Text( + loc.teams, + style: const TextStyle( fontSize: 13, color: Colors.grey, fontWeight: FontWeight.w500, -- 2.49.1 From 0a1e14a32d561a4f6dfc8168c53d4ca3b9ce973d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 00:27:14 +0200 Subject: [PATCH 09/51] fix: correct placement implementation --- .../match_view/match_detail_view.dart | 88 +++++++++++-------- .../match_view/match_result_view.dart | 25 +++--- pubspec.yaml | 2 +- 3 files changed, 67 insertions(+), 48 deletions(-) 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 90619dd..6a02bbc 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 @@ -154,6 +154,7 @@ class _MatchDetailViewState extends State { const SizedBox(height: 20), ], + // Teams or Players if (match.isTeamMatch) ...[ // Teams InfoTile( @@ -301,15 +302,21 @@ class _MatchDetailViewState extends State { /// Returns the result row for single winner/loser rulesets or a placeholder /// if no result is entered yet List getSingleResultRow(AppLocalizations loc) { - // Single Winner - if (match.mvp.isNotEmpty && match.game.ruleset == Ruleset.singleWinner) { + final ruleset = match.game.ruleset; + + if (match.mvp.isNotEmpty || match.mvt.isNotEmpty) { + // Single Winner / Loser + final mvpName = match.isTeamMatch + ? match.mvt.first.name + : match.mvp.first.name; + return [ Text( - loc.winner, + ruleset == Ruleset.singleWinner ? loc.winner : loc.loser, style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), ), Text( - match.mvp.first.name, + mvpName, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -317,24 +324,8 @@ class _MatchDetailViewState extends State { ), ), ]; - // Single Loser - } else if (match.game.ruleset == Ruleset.singleLoser) { - return [ - Text( - loc.loser, - style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), - ), - Text( - match.mvp.first.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, - ), - ), - ]; - // No result entered yet } else { + // No result entered yet return [ Text( loc.no_results_entered_yet, @@ -346,40 +337,63 @@ class _MatchDetailViewState extends State { /// Returns the result widget for scores or placement Widget getMultiResultRows(AppLocalizations loc) { - List<(String, int)> playerScores = []; - for (var player in match.players) { - int score = match.scores[player.id]?.score ?? 0; - playerScores.add((player.name, score)); - } - - final ruleset = match.game.ruleset; - - if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { - playerScores.sort((a, b) => b.$2.compareTo(a.$2)); - } else if (ruleset == Ruleset.lowestScore) { - playerScores.sort((a, b) => a.$2.compareTo(b.$2)); - } + List<(String, int)> scores = getSortedScores(); return Column( children: [ - for (var i = 0; i < playerScores.length; i++) + for (var i = 0; i < scores.length; i++) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - playerScores[i].$1, + scores[i].$1, style: const TextStyle( fontSize: 16, color: CustomTheme.textColor, ), ), - getResultValueText(loc, i, playerScores[i].$2), + getResultValueText(loc, i, scores[i].$2), ], ), ], ); } + /// Returns a list of player/team names and their corresponding scores, sorted by score according to the ruleset + List<(String, int)> getSortedScores() { + List<(String, int)> scores = []; + + if (match.isTeamMatch) { + for (var team in match.teams!) { + int score = team.score ?? 0; + scores.add((team.name, score)); + } + + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { + scores.sort((a, b) => b.$2.compareTo(a.$2)); + } else if (ruleset == Ruleset.lowestScore) { + scores.sort((a, b) => a.$2.compareTo(b.$2)); + } + } else { + for (var player in match.players) { + int score = match.scores[player.id]?.score ?? 0; + scores.add((player.name, score)); + } + + final ruleset = match.game.ruleset; + + if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { + scores.sort((a, b) => b.$2.compareTo(a.$2)); + } else if (ruleset == Ruleset.lowestScore) { + scores.sort((a, b) => a.$2.compareTo(b.$2)); + } + } + return scores; + } + + /// Returns the text widget for the score or placement value, styled according to the ruleset Widget getResultValueText(AppLocalizations loc, int index, int score) { final ruleset = match.game.ruleset; diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 27a1e39..4287d5b 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -197,7 +197,7 @@ class _MatchResultViewState extends State { } void initializeAsTeamMatch() { - allTeams = widget.match.teams ?? []; + allTeams = [...(widget.match.teams ?? [])]; allTeams.sort((a, b) => a.name.compareTo(b.name)); controller = List.generate( @@ -218,10 +218,8 @@ class _MatchResultViewState extends State { } } else if (rulesetSupportsPlacement()) { allTeams.sort((a, b) { - final scoreA = - allTeams.where((team) => a.id == team.id).first.score ?? 0; - final scoreB = - allTeams.where((team) => b.id == team.id).first.score ?? 0; + final scoreA = a.score ?? 0; + final scoreB = b.score ?? 0; return scoreB.compareTo(scoreA); }); } @@ -229,7 +227,7 @@ class _MatchResultViewState extends State { } void inizializeAsNormalMatch() { - allPlayers = widget.match.players; + allPlayers = [...widget.match.players]; allPlayers.sort((a, b) => a.name.compareTo(b.name)); controller = List.generate( @@ -370,10 +368,17 @@ class _MatchResultViewState extends State { /// Handles saving the placement for each player in the database. Future _handlePlacement() async { - await db.scoreEntryDao.setPlacements( - matchId: widget.match.id, - players: allPlayers, - ); + if (isTeamMatch) { + await db.teamDao.setTeamPlacements( + matchId: widget.match.id, + teams: allTeams, + ); + } else { + await db.scoreEntryDao.setPlacements( + matchId: widget.match.id, + players: allPlayers, + ); + } } String getTitleForRuleset(AppLocalizations loc) { diff --git a/pubspec.yaml b/pubspec.yaml index 595d1d7..18f210e 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.30+294 +version: 0.0.30+299 environment: sdk: ^3.8.1 -- 2.49.1 From 9bce03d780e3b957d266b2dae63276c0eb11e852 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 00:48:29 +0200 Subject: [PATCH 10/51] fix: updating team score in detail view --- lib/data/dao/team_dao.dart | 18 +++ lib/data/models/match.dart | 2 +- .../match_view/match_detail_view.dart | 105 ++++++++++-------- .../match_view/match_result_view.dart | 2 +- pubspec.yaml | 2 +- 5 files changed, 80 insertions(+), 49 deletions(-) diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index e85e76f..a6f03f0 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -124,6 +124,24 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { ); } + Future> getTeamsByMatchId({required String matchId}) async { + final playerMatchQuery = select(db.playerMatchTable) + ..where((pm) => pm.matchId.equals(matchId)); + final playerMatches = await playerMatchQuery.get(); + + if (playerMatches.isEmpty) return []; + + final teamIds = playerMatches + .map((pm) => pm.teamId) + .whereType() + .toSet(); + + final teams = await Future.wait( + teamIds.map((id) => getTeamById(teamId: id)), + ); + return teams; + } + /// Retrieves a [Team] by its [teamId], including its members. Future getTeamById({required String teamId}) async { final query = select(teamTable)..where((t) => t.id.equals(teamId)); diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index f7c81a2..4b14ff9 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -19,7 +19,7 @@ class Match { final bool isTeamMatch; final List? teams; final String notes; - Map scores; + final Map scores; Match({ required this.name, 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 6a02bbc..e00ebc6 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 @@ -8,6 +8,8 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/score_entry.dart'; +import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; @@ -43,13 +45,19 @@ class MatchDetailView extends StatefulWidget { class _MatchDetailViewState extends State { late final AppDatabase db; - late Match match; + late Match localMatch; + + late List localTeams; + + late Map localScores; @override void initState() { super.initState(); db = Provider.of(context, listen: false); - match = widget.match; + localMatch = widget.match; + localScores = localMatch.scores; + localTeams = localMatch.teams ?? []; } @override @@ -83,7 +91,7 @@ class _MatchDetailViewState extends State { ), ).then((confirmed) async { if (confirmed! && context.mounted) { - await db.matchDao.deleteMatch(matchId: match.id); + await db.matchDao.deleteMatch(matchId: localMatch.id); if (!context.mounted) return; Navigator.pop(context); widget.onMatchUpdate.call(); @@ -117,7 +125,7 @@ class _MatchDetailViewState extends State { // Match Name Text( - match.name, + localMatch.name, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, @@ -129,7 +137,7 @@ class _MatchDetailViewState extends State { // Creation Date Text( - '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(localMatch.createdAt)}', style: const TextStyle( fontSize: 12, color: CustomTheme.textColor, @@ -139,14 +147,14 @@ class _MatchDetailViewState extends State { const SizedBox(height: 10), // Group Name - if (match.group != null) ...[ + if (localMatch.group != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.group), const SizedBox(width: 8), Text( - '${match.group!.name}${getExtraPlayerCount(match)}', + '${localMatch.group!.name}${getExtraPlayerCount(localMatch)}', style: const TextStyle(fontWeight: FontWeight.bold), ), ], @@ -155,7 +163,7 @@ class _MatchDetailViewState extends State { ], // Teams or Players - if (match.isTeamMatch) ...[ + if (localMatch.isTeamMatch) ...[ // Teams InfoTile( title: loc.teams, @@ -166,7 +174,7 @@ class _MatchDetailViewState extends State { crossAxisAlignment: WrapCrossAlignment.start, spacing: 12, runSpacing: 8, - children: match.teams!.map((team) { + children: localMatch.teams!.map((team) { return TeamCard(team: team); }).toList(), ), @@ -182,7 +190,7 @@ class _MatchDetailViewState extends State { crossAxisAlignment: WrapCrossAlignment.start, spacing: 12, runSpacing: 8, - children: match.players.map((player) { + children: localMatch.players.map((player) { return TextIconTile( text: player.name, suffixText: getNameCountText(player), @@ -205,12 +213,12 @@ class _MatchDetailViewState extends State { horizontal: 8, ), child: GameLabel( - title: match.game.name, + title: localMatch.game.name, description: translateRulesetToString( - match.game.ruleset, + localMatch.game.ruleset, context, ), - color: match.game.color, + color: localMatch.game.color, ), ), ), @@ -241,7 +249,7 @@ class _MatchDetailViewState extends State { adaptivePageRoute( fullscreenDialog: true, builder: (context) => CreateMatchView( - matchToEdit: match, + matchToEdit: localMatch, onMatchUpdated: onMatchUpdated, ), ), @@ -257,12 +265,10 @@ class _MatchDetailViewState extends State { adaptivePageRoute( fullscreenDialog: true, builder: (context) => MatchResultView( - match: match, - onWinnerChanged: () { + match: localMatch, + onWinnerChanged: () async { widget.onMatchUpdate.call(); - setState(() { - updateScoresForCurrentMatch(); - }); + await updateScoresForCurrentMatch(); }, ), ), @@ -282,7 +288,7 @@ class _MatchDetailViewState extends State { /// updates the match in this view void onMatchUpdated(Match editedMatch) { setState(() { - match = editedMatch; + localMatch = editedMatch; }); widget.onMatchUpdate.call(); } @@ -302,13 +308,13 @@ class _MatchDetailViewState extends State { /// Returns the result row for single winner/loser rulesets or a placeholder /// if no result is entered yet List getSingleResultRow(AppLocalizations loc) { - final ruleset = match.game.ruleset; + final ruleset = localMatch.game.ruleset; - if (match.mvp.isNotEmpty || match.mvt.isNotEmpty) { + if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) { // Single Winner / Loser - final mvpName = match.isTeamMatch - ? match.mvt.first.name - : match.mvp.first.name; + final mvpName = localMatch.isTeamMatch + ? localMatch.mvt.first.name + : localMatch.mvp.first.name; return [ Text( @@ -361,41 +367,41 @@ class _MatchDetailViewState extends State { /// Returns a list of player/team names and their corresponding scores, sorted by score according to the ruleset List<(String, int)> getSortedScores() { - List<(String, int)> scores = []; + List<(String, int)> namedScores = []; - if (match.isTeamMatch) { - for (var team in match.teams!) { + if (localMatch.isTeamMatch) { + for (var team in localTeams) { int score = team.score ?? 0; - scores.add((team.name, score)); + namedScores.add((team.name, score)); } - final ruleset = match.game.ruleset; + final ruleset = localMatch.game.ruleset; if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { - scores.sort((a, b) => b.$2.compareTo(a.$2)); + namedScores.sort((a, b) => b.$2.compareTo(a.$2)); } else if (ruleset == Ruleset.lowestScore) { - scores.sort((a, b) => a.$2.compareTo(b.$2)); + namedScores.sort((a, b) => a.$2.compareTo(b.$2)); } } else { - for (var player in match.players) { - int score = match.scores[player.id]?.score ?? 0; - scores.add((player.name, score)); + for (var player in localMatch.players) { + int score = localScores[player.id]?.score ?? 0; + namedScores.add((player.name, score)); } - final ruleset = match.game.ruleset; + final ruleset = localMatch.game.ruleset; if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) { - scores.sort((a, b) => b.$2.compareTo(a.$2)); + namedScores.sort((a, b) => b.$2.compareTo(a.$2)); } else if (ruleset == Ruleset.lowestScore) { - scores.sort((a, b) => a.$2.compareTo(b.$2)); + namedScores.sort((a, b) => a.$2.compareTo(b.$2)); } } - return scores; + return namedScores; } /// Returns the text widget for the score or placement value, styled according to the ruleset Widget getResultValueText(AppLocalizations loc, int index, int score) { - final ruleset = match.game.ruleset; + final ruleset = localMatch.game.ruleset; if (ruleset == Ruleset.placement) { return Text( @@ -433,8 +439,8 @@ class _MatchDetailViewState extends State { // Returns if the result can be displayed in a single row bool isSingleRowResult() { - return match.game.ruleset == Ruleset.singleWinner || - match.game.ruleset == Ruleset.singleLoser; + return localMatch.game.ruleset == Ruleset.singleWinner || + localMatch.game.ruleset == Ruleset.singleLoser; } String getPlacementText(BuildContext context, int rank) { @@ -465,9 +471,16 @@ class _MatchDetailViewState extends State { } } - void updateScoresForCurrentMatch() { - db.scoreEntryDao - .getAllMatchScores(matchId: match.id) - .then((scores) => match.scores = scores); + // Die Methode selbst: + Future updateScoresForCurrentMatch() async { + if (widget.match.isTeamMatch) { + final teams = await db.teamDao.getTeamsByMatchId(matchId: localMatch.id); + if (mounted) setState(() => localTeams = teams); + } else { + final scores = await db.scoreEntryDao.getAllMatchScores( + matchId: localMatch.id, + ); + if (mounted) setState(() => localScores = scores); + } } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 4287d5b..fac95bf 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -186,7 +186,7 @@ class _MatchResultViewState extends State { ); await _handleSaving(); if (!context.mounted) return; - Navigator.of(context).pop(_selectedPlayer); + Navigator.pop(context); } : null, ), diff --git a/pubspec.yaml b/pubspec.yaml index 18f210e..be86af2 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.30+299 +version: 0.0.30+304 environment: sdk: ^3.8.1 -- 2.49.1 From 0f621cd799a5b033a6e3a4454463358c43c03727 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 01:32:03 +0200 Subject: [PATCH 11/51] fix: refresh problem --- .../match_view/match_detail_view.dart | 65 ++++++++++--------- .../match_view/match_result_view.dart | 1 + 2 files changed, 36 insertions(+), 30 deletions(-) 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 e38866b..3087e6d 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 @@ -8,8 +8,6 @@ import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/match.dart'; -import 'package:tallee/data/models/score_entry.dart'; -import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; @@ -48,17 +46,11 @@ class _MatchDetailViewState extends State { late Match localMatch; - late List localTeams; - - late Map localScores; - @override void initState() { super.initState(); db = Provider.of(context, listen: false); localMatch = widget.match; - localScores = localMatch.scores; - localTeams = localMatch.teams ?? []; } @override @@ -175,7 +167,7 @@ class _MatchDetailViewState extends State { crossAxisAlignment: WrapCrossAlignment.start, spacing: 12, runSpacing: 8, - children: localMatch.teams!.map((team) { + children: (localMatch.teams ?? []).map((team) { return TeamCard(team: team); }).toList(), ), @@ -313,30 +305,38 @@ class _MatchDetailViewState extends State { final ruleset = localMatch.game.ruleset; if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) { - // Single Winner / Loser - final mvps = localMatch.isTeamMatch - ? localMatch.mvt - : localMatch.mvp; - final mvpName = ruleset == Ruleset.multipleWinners - ? mvps.map((party) => party.name).join(', ') - : mvps.first.name; + // Single winner/loser, multiple winner + final names = localMatch.isTeamMatch + ? localMatch.mvt.map((t) => t.name).toList() + : localMatch.mvp.map((p) => p.name).toList(); + final mvpNames = names.length == 1 ? names.first : names.join(', '); + + final label = ruleset == Ruleset.singleWinner + ? loc.winner + : ruleset == Ruleset.singleLoser + ? loc.loser + : loc.winners; return [ Text( - ruleset == Ruleset.singleWinner ? loc.winner : loc.loser, + label, style: const TextStyle(fontSize: 16, color: CustomTheme.textColor), ), - Text( - mvpName, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.primaryColor, + SizedBox( + width: 200, + child: Text( + mvpNames, + textAlign: TextAlign.end, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: CustomTheme.primaryColor, + ), ), ), ]; } else { - // No result entered yet + // No result yet return [ Text( loc.no_results_entered_yet, @@ -375,7 +375,8 @@ class _MatchDetailViewState extends State { List<(String, int)> namedScores = []; if (localMatch.isTeamMatch) { - for (var team in localTeams) { + final teams = localMatch.teams ?? []; + for (var team in teams) { int score = team.score ?? 0; namedScores.add((team.name, score)); } @@ -388,8 +389,9 @@ class _MatchDetailViewState extends State { namedScores.sort((a, b) => a.$2.compareTo(b.$2)); } } else { + final scores = localMatch.scores; for (var player in localMatch.players) { - int score = localScores[player.id]?.score ?? 0; + int score = scores[player.id]?.score ?? 0; namedScores.add((player.name, score)); } @@ -477,16 +479,19 @@ class _MatchDetailViewState extends State { } } - // Die Methode selbst: Future updateScoresForCurrentMatch() async { - if (widget.match.isTeamMatch) { + if (localMatch.isTeamMatch) { final teams = await db.teamDao.getTeamsByMatchId(matchId: localMatch.id); - if (mounted) setState(() => localTeams = teams); + setState(() { + localMatch = localMatch.copyWith(teams: teams); + }); } else { final scores = await db.scoreEntryDao.getAllMatchScores( matchId: localMatch.id, ); - if (mounted) setState(() => localScores = scores); + setState(() { + localMatch = localMatch.copyWith(scores: scores); + }); } } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 0fe8c1b..63f3346 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -148,6 +148,7 @@ class _MatchResultViewState extends State { // Show player selection if (rulesetSupportsPlayerSelection()) if (ruleset == Ruleset.multipleWinners) + // TODO: Implement view for teams Expanded( child: ListView.builder( physics: const NeverScrollableScrollPhysics(), -- 2.49.1 From 0812f18d77cef116d3ea2a2df69caa46ba1c4979 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:25:49 +0200 Subject: [PATCH 12/51] feat: implemented multiple winners with teams --- lib/core/common.dart | 3 +- lib/data/dao/team_dao.dart | 15 ++ .../match_view/match_result_view.dart | 188 +++++++++++++----- .../widgets/buttons/custom_width_button.dart | 3 + .../custom_checkbox_list_tile.dart | 15 +- .../widgets/tiles/match_tile.dart | 40 ++-- pubspec.yaml | 2 +- 7 files changed, 182 insertions(+), 84 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index 29d2f57..c581858 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -92,11 +92,10 @@ IconData getRulesetIcon(Ruleset ruleset) { case Ruleset.lowestScore: return Icons.arrow_downward; case Ruleset.singleWinner: + case Ruleset.multipleWinners: return Icons.emoji_events; case Ruleset.singleLoser: return Icons.sentiment_dissatisfied; - case Ruleset.multipleWinners: - return Icons.group; case Ruleset.placement: return RpgAwesome.podium; } diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index a6f03f0..4efcc74 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -268,6 +268,21 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1); } + Future setWinnerTeams({ + required List winners, + required String matchId, + }) async { + List success = List.generate(winners.length, (index) => null); + for (int i = 0; i < winners.length; i++) { + success[i] = await updateTeamScore( + teamId: winners[i].id, + matchId: matchId, + score: 1, + ); + } + return success.every((result) => result == true); + } + Future removeWinnerTeam({ required String teamId, required String matchId, diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 63f3346..a0cbc2a 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/common.dart'; @@ -103,20 +105,7 @@ class _MatchResultViewState extends State { Expanded( child: isLiveEditMode // Live Edit Mode - ? ListView.builder( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return LiveEditListTile( - title: allPlayers[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); - }, - ) + ? buildLiveEditWidet(isTeamMatch) // Normal Container : Container( margin: const EdgeInsets.symmetric( @@ -150,35 +139,13 @@ class _MatchResultViewState extends State { if (ruleset == Ruleset.multipleWinners) // TODO: Implement view for teams Expanded( - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomCheckboxListTile( - text: allPlayers[index].name, - value: _selectedPlayers.contains( - allPlayers[index], - ), - onChanged: (bool value) { - setState(() { - if (value) { - _selectedPlayers.add( - allPlayers[index], - ); - } else { - _selectedPlayers.remove( - allPlayers[index], - ); - } - }); - }, - ); - }, + child: buildMultipleWinnerSelectionWidget( + isTeamMatch, ), ) else Expanded( - child: buildWinnerSelectionWidget(isTeamMatch), + child: buildPlayerSelectionWidget(isTeamMatch), ), // Show score entry @@ -363,13 +330,24 @@ class _MatchResultViewState extends State { /// Handles saving the (multiple) winners to the database. Future _handleWinners() async { - if (_selectedPlayers.isEmpty) { - return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + if (isTeamMatch) { + if (_selectedTeams.isEmpty) { + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + } else { + return await db.teamDao.setWinnerTeams( + matchId: widget.match.id, + winners: _selectedTeams.toList(), + ); + } } else { - return await db.scoreEntryDao.setWinners( - matchId: widget.match.id, - winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(), - ); + if (_selectedPlayers.isEmpty) { + return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + } else { + return await db.scoreEntryDao.setWinners( + matchId: widget.match.id, + winners: _selectedPlayers.toList(), + ); + } } } @@ -474,7 +452,11 @@ class _MatchResultViewState extends State { return ruleset == Ruleset.placement; } - Widget buildTeamTile({required Team team, double? width}) { + Widget buildTeamTile({ + required Team team, + double? width, + int showingPlayerAmount = 3, + }) { return Container( width: width, margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 2), @@ -498,7 +480,11 @@ class _MatchResultViewState extends State { spacing: 4, runSpacing: 4, children: [ - for (final member in team.members) + for ( + int i = 0; + i < min(team.members.length, showingPlayerAmount); + i++ + ) Container( padding: const EdgeInsets.symmetric( vertical: 4, @@ -509,7 +495,23 @@ class _MatchResultViewState extends State { borderRadius: BorderRadius.circular(4), ), child: Text( - member.name, + team.members[i].name, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: 13, + color: CustomTheme.textColor.withAlpha(180), + ), + ), + ), + if (team.members.length > 4) + Container( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 4, + ), + child: Text( + '+${team.members.length - showingPlayerAmount}', overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, style: TextStyle( @@ -525,7 +527,7 @@ class _MatchResultViewState extends State { ); } - Widget buildWinnerSelectionWidget(bool isTeamMatch) { + Widget buildPlayerSelectionWidget(bool isTeamMatch) { if (isTeamMatch) { return RadioGroup( groupValue: _selectedTeam, @@ -604,7 +606,11 @@ class _MatchResultViewState extends State { itemCount: allTeams.length, itemBuilder: (context, index) { return ScoreListTile( - content: buildTeamTile(team: allTeams[index], width: 220), + content: buildTeamTile( + team: allTeams[index], + width: 220, + showingPlayerAmount: 2, + ), horizontalPadding: 0, controller: controller[index], ); @@ -780,4 +786,86 @@ class _MatchResultViewState extends State { return Row(children: [placementCol, valueCol]); } + + Widget buildMultipleWinnerSelectionWidget(bool isTeamMatch) { + if (isTeamMatch) { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allTeams.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + content: buildTeamTile(team: allTeams[index]), + value: _selectedTeams.contains(allTeams[index]), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedTeams.add(allTeams[index]); + } else { + _selectedTeams.remove(allTeams[index]); + } + }); + }, + ); + }, + ); + } else { + return ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomCheckboxListTile( + content: Text( + allPlayers[index].name, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500), + ), + value: _selectedPlayers.contains(allPlayers[index]), + onChanged: (bool value) { + setState(() { + if (value) { + _selectedPlayers.add(allPlayers[index]); + } else { + _selectedPlayers.remove(allPlayers[index]); + } + }); + }, + ); + }, + ); + } + } + + Widget buildLiveEditWidet(bool isTeamMatch) { + if (isTeamMatch) { + return ListView.builder( + itemCount: allTeams.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allTeams[index].name, + onChanged: (value) { + setState(() { + controller[index].text = value.toString(); + }); + }, + value: int.tryParse(controller[index].text) ?? 0, + ); + }, + ); + } else { + return ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { + setState(() { + controller[index].text = value.toString(); + }); + }, + value: int.tryParse(controller[index].text) ?? 0, + ); + }, + ); + } + } } diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 556b784..46e7193 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -56,6 +56,7 @@ class CustomWidthButton extends StatelessWidget { onPressed!.call(); }, style: ElevatedButton.styleFrom( + splashFactory: NoSplash.splashFactory, foregroundColor: textcolor, disabledForegroundColor: disabledTextColor, backgroundColor: buttonBackgroundColor, @@ -91,6 +92,7 @@ class CustomWidthButton extends StatelessWidget { onPressed!.call(); }, style: OutlinedButton.styleFrom( + splashFactory: NoSplash.splashFactory, foregroundColor: textcolor, disabledForegroundColor: disabledTextColor, backgroundColor: buttonBackgroundColor, @@ -128,6 +130,7 @@ class CustomWidthButton extends StatelessWidget { onPressed!.call(); }, style: TextButton.styleFrom( + splashFactory: NoSplash.splashFactory, foregroundColor: textcolor, disabledForegroundColor: disabledTextColor, backgroundColor: buttonBackgroundColor, diff --git a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart index bb6c933..23d7be6 100644 --- a/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart @@ -5,12 +5,12 @@ import 'package:tallee/core/custom_theme.dart'; class CustomCheckboxListTile extends StatelessWidget { const CustomCheckboxListTile({ super.key, - required this.text, + required this.content, required this.value, required this.onChanged, }); - final String text; + final Widget content; final bool value; final ValueChanged onChanged; @@ -39,16 +39,7 @@ class CustomCheckboxListTile extends StatelessWidget { onChanged(v); }, ), - Expanded( - child: Text( - text, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), + Expanded(child: content), ], ), ), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 77c3f12..76cc86e 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -347,24 +347,27 @@ class _MatchTileState extends State { if (widget.match.mvt.isEmpty) return ''; final ruleset = widget.match.game.ruleset; - if (ruleset == Ruleset.singleWinner) { - return '${loc.winner}: ${widget.match.mvt.first.name}'; - } else if (ruleset == Ruleset.singleLoser) { - return '${loc.loser}: ${widget.match.mvt.first.name}'; - } else if (ruleset == Ruleset.highestScore || - ruleset == Ruleset.lowestScore) { - final mvt = widget.match.mvt; - final mvtScore = - widget.match.teams! - .firstWhere((team) => team.id == mvt.first.id) - .score ?? - 0; - final mvtNames = mvt.map((team) => team.name).join(', '); - return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})'; - } else if (ruleset == Ruleset.placement) { - return '${loc.winner}: ${widget.match.mvt.first.name}'; + switch (ruleset) { + case Ruleset.singleWinner: + return '${loc.winner}: ${widget.match.mvt.first.name}'; + case Ruleset.singleLoser: + return '${loc.loser}: ${widget.match.mvt.first.name}'; + case Ruleset.highestScore: + case Ruleset.lowestScore: + final mvt = widget.match.mvt; + final mvtScore = + widget.match.teams! + .firstWhere((team) => team.id == mvt.first.id) + .score ?? + 0; + final mvtNames = mvt.map((team) => team.name).join(', '); + return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})'; + case Ruleset.placement: + return '${loc.winner}: ${widget.match.mvt.first.name}'; + case Ruleset.multipleWinners: + final mvtNames = widget.match.mvt.map((team) => team.name).join(', '); + return '${loc.winners}: $mvtNames'; } - return '${loc.winner}: n.A.'; } Icon getMvpIcon() { @@ -372,6 +375,7 @@ class _MatchTileState extends State { switch (widget.match.game.ruleset) { case Ruleset.singleWinner: + case Ruleset.multipleWinners: return Icon(icon, size: 20, color: Colors.amber); case Ruleset.singleLoser: return Icon(icon, size: 20, color: Colors.blue); @@ -379,8 +383,6 @@ class _MatchTileState extends State { return Icon(icon, size: 20, color: Colors.orange); case Ruleset.highestScore: return Icon(icon, size: 20, color: Colors.green); - case Ruleset.multipleWinners: - return Icon(icon, size: 20, color: Colors.amber); case Ruleset.placement: return Icon(icon, size: 20, color: Colors.deepOrangeAccent); } diff --git a/pubspec.yaml b/pubspec.yaml index 95a31f7..1310ab9 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.30+316 +version: 0.0.30+325 environment: sdk: ^3.8.1 -- 2.49.1 From 9a8b93510eaa4f7bb10eaa12b0bca53927647bd7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:29:33 +0200 Subject: [PATCH 13/51] removed compact attribute --- .../widgets/tiles/match_tile.dart | 45 +++++++------------ 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 76cc86e..7c1e801 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -18,13 +18,11 @@ class MatchTile extends StatefulWidget { /// - [match]: The match data to be displayed. /// - [onTap]: The callback invoked when the tile is tapped. /// - [width]: Optional width for the tile. - /// - [compact]: Whether to display the tile in a compact mode const MatchTile({ super.key, required this.match, required this.onTap, this.width, - this.compact = false, }); /// The match data to be displayed. @@ -36,9 +34,6 @@ class MatchTile extends StatefulWidget { /// Optional width for the tile. final double? width; - /// Whether to display the tile in a compact mode - final bool compact; - @override State createState() => _MatchTileState(); } @@ -101,40 +96,25 @@ class _MatchTileState extends State { ], ), const SizedBox(height: 4), - ] else if (widget.compact) ...[ - Row( - children: [ - const Icon(Icons.person, size: 16, color: Colors.grey), - const SizedBox(width: 6), - Expanded( - child: Text( - '${match.players.length} ${loc.players}', - style: const TextStyle(fontSize: 14, color: Colors.grey), - overflow: TextOverflow.ellipsis, - ), - ), - ], - ), - const SizedBox(height: 6), ] else ...[ const SizedBox(height: 8), ], // Game + Ruleset Badge - if (!widget.compact) - GameLabel( - title: match.game.name, - description: translateRulesetToString( - match.game.ruleset, - context, - ), - color: match.game.color, + GameLabel( + title: match.game.name, + description: translateRulesetToString( + match.game.ruleset, + context, ), + color: match.game.color, + ), const SizedBox(height: 12), // Winner / In Progress Info if (match.isTeamMatch && match.mvt.isNotEmpty) ...[ + // MVT Display for team matches Container( padding: const EdgeInsets.symmetric( vertical: 8, @@ -168,6 +148,7 @@ class _MatchTileState extends State { ), const SizedBox(height: 12), ] else if (match.mvp.isNotEmpty) ...[ + // MVP Display for player matches Container( padding: const EdgeInsets.symmetric( vertical: 8, @@ -201,6 +182,7 @@ class _MatchTileState extends State { ), const SizedBox(height: 12), ] else ...[ + // Match in progress display Container( padding: const EdgeInsets.symmetric( vertical: 8, @@ -240,6 +222,7 @@ class _MatchTileState extends State { ], if (match.teams != null && match.teams!.isNotEmpty) ...[ + // Team display Text( loc.teams, style: const TextStyle( @@ -274,7 +257,8 @@ class _MatchTileState extends State { }, ), const SizedBox(height: 12), - ] else if (players.isNotEmpty && widget.compact == false) ...[ + ] else if (players.isNotEmpty) ...[ + // Player display Text( loc.players, style: const TextStyle( @@ -320,6 +304,7 @@ class _MatchTileState extends State { } } + // Returns the appropriate text based on the match's ruleset and MVP. String getMvpText(AppLocalizations loc) { if (widget.match.mvp.isEmpty) return ''; final ruleset = widget.match.game.ruleset; @@ -343,6 +328,7 @@ class _MatchTileState extends State { return '${loc.winner}: n.A.'; } + // Returns the appropriate text based on the match's ruleset and MVT. String getMvtText(AppLocalizations loc) { if (widget.match.mvt.isEmpty) return ''; final ruleset = widget.match.game.ruleset; @@ -370,6 +356,7 @@ class _MatchTileState extends State { } } + // Returns the appropriate icon based on the match's ruleset. Icon getMvpIcon() { final icon = getRulesetIcon(widget.match.game.ruleset); -- 2.49.1 From 9c5e72e6ed0373a3a2d44f8c78e147ddae90bb6c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:39:52 +0200 Subject: [PATCH 14/51] Added missing dao functions --- lib/data/dao/team_dao.dart | 29 +++++++++++++++---- .../match_view/match_result_view.dart | 7 ++--- .../widgets/tiles/match_tile.dart | 2 +- pubspec.yaml | 2 +- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 4efcc74..70ba8b6 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -241,6 +241,14 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return true; } + Future removeAllTeamScores({required String matchId}) async { + await (update( + teamTable, + )).write(const TeamTableCompanion(score: Value(null))); + await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId); + return true; + } + /* Delete */ /// Deletes all teams from the database. @@ -261,6 +269,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /* Score handling */ + /// Sets the team with the given [teamId] as the winner of the match with the given [matchId] by assigning a score of 1. + /// Returns `true` if the score was updated successfully, `false` otherwise. Future setWinnerTeam({ required String teamId, required String matchId, @@ -268,6 +278,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1); } + /// Sets multiple teams as winners of the match with the given [matchId] by assigning a score of 1 to each team. + /// Returns `true` if all scores were updated successfully, `false` otherwise. Future setWinnerTeams({ required List winners, required String matchId, @@ -283,13 +295,14 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return success.every((result) => result == true); } - Future removeWinnerTeam({ - required String teamId, - required String matchId, - }) async { - return await removeScoreForTeam(teamId: teamId, matchId: matchId); + /// Removes the winner status from all Teams with the given [matchId] by setting its score to null. + /// Returns `true` if the score was updated successfully, `false` otherwise. + Future removeWinnerTeam({required String matchId}) async { + return await removeAllTeamScores(matchId: matchId); } + /// Sets the team with the given [teamId] as the loser of the match with the given [matchId] by assigning a score of 0. + /// Returns `true` if the score was updated successfully, `false` otherwise. Future setLoserTeam({ required String teamId, required String matchId, @@ -297,13 +310,17 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return await updateTeamScore(teamId: teamId, matchId: matchId, score: 0); } + /// Removes the loser status from the team with the given [teamId] in the match with the given [matchId] by setting its score to null. + /// Returns `true` if the score was updated successfully, `false` otherwise. Future removeLoserTeam({ required String teamId, required String matchId, }) async { - return await removeScoreForTeam(teamId: teamId, matchId: matchId); + return await removeAllTeamScores(matchId: matchId); } + /// Sets the placements for the teams in the match with the given [matchId] by assigning scores based on their order in the [teams] list. + /// Returns `true` if all scores were updated successfully, `false` otherwise. Future setTeamPlacements({ required String matchId, required List teams, diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index a0cbc2a..4f9ab34 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -306,10 +306,7 @@ class _MatchResultViewState extends State { Future _handleWinner() async { if (isTeamMatch) { if (_selectedTeam == null) { - return await db.teamDao.removeWinnerTeam( - matchId: widget.match.id, - teamId: _selectedTeam!.id, - ); + return await db.teamDao.removeWinnerTeam(matchId: widget.match.id); } else { return await db.teamDao.setWinnerTeam( matchId: widget.match.id, @@ -332,7 +329,7 @@ class _MatchResultViewState extends State { Future _handleWinners() async { if (isTeamMatch) { if (_selectedTeams.isEmpty) { - return await db.scoreEntryDao.removeWinner(matchId: widget.match.id); + return await db.teamDao.removeWinnerTeam(matchId: widget.match.id); } else { return await db.teamDao.setWinnerTeams( matchId: widget.match.id, diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 7c1e801..c3c6b4e 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -235,7 +235,7 @@ class _MatchTileState extends State { LayoutBuilder( builder: (context, constraints) { final useSingleColumn = match.teams!.any( - (team) => team.name.length > 14, + (team) => team.name.length > 10, ); const spacing = 8.0; diff --git a/pubspec.yaml b/pubspec.yaml index 1310ab9..8d9c1b0 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.30+325 +version: 0.0.30+327 environment: sdk: ^3.8.1 -- 2.49.1 From 44c474ae774ddbc730261fb6752c03f09f6261f0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:43:53 +0200 Subject: [PATCH 15/51] fix: setState issue --- lib/presentation/widgets/player_selection.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 00d6c11..ca3566a 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -143,9 +143,9 @@ class _PlayerSelectionState extends State { child: TextIconTile( text: player.name, suffixText: getNameCountText(player), - onIconTap: () { - setState(() async { - await HapticFeedback.selectionClick(); + onIconTap: () async { + await HapticFeedback.selectionClick(); + setState(() { // Removes the player from the selection and notifies the parent. selectedPlayers.remove(player); widget.onChanged([...selectedPlayers]); -- 2.49.1 From f069f62e26dc132487281bb634384680b79f9c20 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:51:52 +0200 Subject: [PATCH 16/51] fix: schema --- assets/schema.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/schema.json b/assets/schema.json index 7f6aebd..8ec904a 100644 --- a/assets/schema.json +++ b/assets/schema.json @@ -166,6 +166,9 @@ "notes": { "type": "string" }, + "isTeamMatch": { + "type": "boolean" + }, "teams": { "type": ["array", "null"] } @@ -177,7 +180,8 @@ "createdAt", "gameId", "playerIds", - "notes" + "notes", + "isTeamMatch" ] } } -- 2.49.1 From 369cabe996ef0f7247268864d76ec77bceab887c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 18 May 2026 22:55:49 +0200 Subject: [PATCH 17/51] fix: export test --- test/services/data_transfer_service_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index ded411d..43e70fb 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -894,6 +894,15 @@ void main() { }, 'createdAt': testMatch.createdAt.toIso8601String(), 'endedAt': null, + 'isTeamMatch': true, + 'teams': [ + { + 'id': testTeam.id, + 'name': testTeam.name, + 'memberIds': [testPlayer1.id, testPlayer2.id], + 'createdAt': testTeam.createdAt.toIso8601String(), + }, + ], }, ], }); -- 2.49.1 From 4c5ce1aba0de57262e9dbea9214bc91502f8d4a8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 20 May 2026 22:44:28 +0200 Subject: [PATCH 18/51] fix: suggested players contained selected players --- lib/presentation/widgets/player_selection.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index ca3566a..ec5ac15 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -252,6 +252,9 @@ class _PlayerSelectionState extends State { ), ) .toList(); + suggestedPlayers = suggestedPlayers + .where((p) => !selectedPlayers.any((sp) => sp.id == p.id)) + .toList(); } } else { // Otherwise, use the loaded players from the database. -- 2.49.1 From ec1182b5604e8975b4fd34799004db65d8cdf2cc Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 00:06:03 +0200 Subject: [PATCH 19/51] feat: added content & disabled state --- .../buttons/animated_dialog_button.dart | 59 ++++++++++++------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/lib/presentation/widgets/buttons/animated_dialog_button.dart b/lib/presentation/widgets/buttons/animated_dialog_button.dart index 8c8765e..674856a 100644 --- a/lib/presentation/widgets/buttons/animated_dialog_button.dart +++ b/lib/presentation/widgets/buttons/animated_dialog_button.dart @@ -15,11 +15,12 @@ class AnimatedDialogButton extends StatefulWidget { this.buttonConstraints, this.buttonType = ButtonType.primary, this.isDescructive = false, + this.content, }); final String buttonText; - final VoidCallback onPressed; + final VoidCallback? onPressed; final BoxConstraints? buttonConstraints; @@ -27,6 +28,8 @@ class AnimatedDialogButton extends StatefulWidget { final bool isDescructive; + final Widget? content; + @override State createState() => _AnimatedDialogButtonState(); } @@ -38,28 +41,40 @@ class _AnimatedDialogButtonState extends State { Widget build(BuildContext context) { final textStyling = _getTextStyling(); final buttonDecoration = _getButtonDecoration(); + final isDisabled = widget.onPressed == null; - return GestureDetector( - onTapDown: (_) => setState(() => _isPressed = true), - onTapUp: (_) => setState(() => _isPressed = false), - onTapCancel: () => setState(() => _isPressed = false), - onTap: widget.onPressed, - child: AnimatedScale( - scale: _isPressed ? 0.95 : 1.0, - duration: const Duration(milliseconds: 100), - child: AnimatedOpacity( - opacity: _isPressed ? 0.6 : 1.0, - duration: const Duration(milliseconds: 100), - child: Center( - child: Container( - constraints: widget.buttonConstraints, - decoration: buttonDecoration, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - margin: const EdgeInsets.symmetric(vertical: 8), - child: Text( - widget.buttonText, - style: textStyling, - textAlign: TextAlign.center, + return IgnorePointer( + ignoring: isDisabled, + child: Opacity( + opacity: isDisabled ? 0.4 : 1.0, + child: GestureDetector( + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) => setState(() => _isPressed = false), + onTapCancel: () => setState(() => _isPressed = false), + onTap: widget.onPressed, + child: AnimatedScale( + scale: _isPressed ? 0.95 : 1.0, + duration: const Duration(milliseconds: 100), + child: AnimatedOpacity( + opacity: _isPressed ? 0.6 : 1.0, + duration: const Duration(milliseconds: 100), + child: Center( + child: Container( + constraints: widget.buttonConstraints, + decoration: buttonDecoration, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + margin: const EdgeInsets.symmetric(vertical: 8), + child: widget.buttonText == '' + ? widget.content! + : Text( + widget.buttonText, + style: textStyling, + textAlign: TextAlign.center, + ), + ), ), ), ), -- 2.49.1 From 3493a74c6f0fb563bfc16f86f5fe34e6796e7245 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 00:06:23 +0200 Subject: [PATCH 20/51] feat: implemented team organsation --- lib/l10n/arb/app_de.arb | 4 + lib/l10n/arb/app_en.arb | 4 + lib/l10n/generated/app_localizations.dart | 24 ++ lib/l10n/generated/app_localizations_de.dart | 12 + lib/l10n/generated/app_localizations_en.dart | 12 + .../create_match/create_match_view.dart | 8 +- .../create_match/organize_teams_view.dart | 263 ----------------- .../team_match/edit_members_view.dart | 57 ++++ .../team_match/organize_teams_view.dart | 272 ++++++++++++++++++ .../match_view/match_result_view.dart | 1 + .../main_menu/match_view/match_view.dart | 1 + .../widgets/tiles/team_creation_tile.dart | 71 +++-- 12 files changed, 436 insertions(+), 293 deletions(-) delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index b7c55fc..679fcc1 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -26,6 +26,7 @@ "create_new_group": "Neue Gruppe erstellen", "create_new_match": "Neues Spiel erstellen", "created_on": "Erstellt am", + "create_teams": "Teams erstellen", "data": "Daten", "data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_exported": "Daten erfolgreich exportiert", @@ -49,6 +50,7 @@ "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", + "edit_members": "Mitglieder bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", @@ -108,6 +110,7 @@ "privacy_policy": "Datenschutzerklärung", "quick_create": "Schnellzugriff", "recent_matches": "Letzte Spiele", + "redistribute": "Neu verteilen", "result": "Ergebnis", "results": "Ergebnisse", "ruleset": "Regelwerk", @@ -133,6 +136,7 @@ "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "team": "Team", "teams": "Teams", "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index be5efa6..b41a6f7 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -27,6 +27,7 @@ "create_new_group": "Create new group", "created_on": "Created on", "create_new_match": "Create new match", + "create_teams": "Create teams", "data": "Data", "data_successfully_deleted": "Data successfully deleted", "data_successfully_exported": "Data successfully exported", @@ -50,6 +51,7 @@ "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", + "edit_members": "Edit Members", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", @@ -109,6 +111,7 @@ "privacy_policy": "Privacy Policy", "quick_create": "Quick Create", "recent_matches": "Recent Matches", + "redistribute": "Redistribute", "results": "Results", "ruleset": "Ruleset", "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", @@ -142,6 +145,7 @@ } } }, + "team": "Team", "teams": "Teams", "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 3f7883d..4ce2c74 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -254,6 +254,12 @@ abstract class AppLocalizations { /// **'Create new match'** String get create_new_match; + /// No description provided for @create_teams. + /// + /// In en, this message translates to: + /// **'Create teams'** + String get create_teams; + /// No description provided for @data. /// /// In en, this message translates to: @@ -350,6 +356,12 @@ abstract class AppLocalizations { /// **'Edit Match'** String get edit_match; + /// No description provided for @edit_members. + /// + /// In en, this message translates to: + /// **'Edit Members'** + String get edit_members; + /// No description provided for @enter_points. /// /// In en, this message translates to: @@ -704,6 +716,12 @@ abstract class AppLocalizations { /// **'Recent Matches'** String get recent_matches; + /// No description provided for @redistribute. + /// + /// In en, this message translates to: + /// **'Redistribute'** + String get redistribute; + /// No description provided for @results. /// /// In en, this message translates to: @@ -848,6 +866,12 @@ abstract class AppLocalizations { /// **'Successfully added player {playerName}'** String successfully_added_player(String playerName); + /// No description provided for @team. + /// + /// In en, this message translates to: + /// **'Team'** + String get team; + /// No description provided for @teams. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index dfbe9f1..c634f55 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -88,6 +88,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_match => 'Neues Spiel erstellen'; + @override + String get create_teams => 'Teams erstellen'; + @override String get data => 'Daten'; @@ -146,6 +149,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get edit_match => 'Gruppe bearbeiten'; + @override + String get edit_members => 'Mitglieder bearbeiten'; + @override String get enter_points => 'Punkte eingeben'; @@ -328,6 +334,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get recent_matches => 'Letzte Spiele'; + @override + String get redistribute => 'Neu verteilen'; + @override String get results => 'Ergebnisse'; @@ -407,6 +416,9 @@ class AppLocalizationsDe extends AppLocalizations { return 'Spieler:in $playerName erfolgreich hinzugefügt'; } + @override + String get team => 'Team'; + @override String get teams => 'Teams'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 93487b3..8e79c77 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -88,6 +88,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_match => 'Create new match'; + @override + String get create_teams => 'Create teams'; + @override String get data => 'Data'; @@ -146,6 +149,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get edit_match => 'Edit Match'; + @override + String get edit_members => 'Edit Members'; + @override String get enter_points => 'Enter points'; @@ -328,6 +334,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get recent_matches => 'Recent Matches'; + @override + String get redistribute => 'Redistribute'; + @override String get results => 'Results'; @@ -407,6 +416,9 @@ class AppLocalizationsEn extends AppLocalizations { return 'Successfully added player $playerName'; } + @override + String get team => 'Team'; + @override String get teams => 'Teams'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 3f4fe36..dbfba47 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -12,7 +12,7 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/player_selection.dart'; @@ -296,7 +296,7 @@ class _CreateMatchViewState extends State { if (isTeamMatch) { if (context.mounted) { - Navigator.pushReplacement( + Navigator.push( context, adaptivePageRoute( fullscreenDialog: !isTeamMatch, @@ -385,7 +385,9 @@ class _CreateMatchViewState extends State { isTeamMatch: isTeamMatch, game: selectedGame!, ); - await db.matchDao.addMatch(match: match); + + // Team matches are saved in OrganizeTeamsView + if (!isTeamMatch) await db.matchDao.addMatch(match: match); return match; } } diff --git a/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart deleted file mode 100644 index b9def83..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/organize_teams_view.dart +++ /dev/null @@ -1,263 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.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/core/enums.dart'; -import 'package:tallee/data/db/database.dart'; -import 'package:tallee/data/models/match.dart'; -import 'package:tallee/data/models/player.dart'; -import 'package:tallee/data/models/team.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; -import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; - -class OrganizeTeamsView extends StatefulWidget { - const OrganizeTeamsView({super.key, required this.match}); - - final Match match; - - @override - State createState() => _OrganizeTeamsViewState(); -} - -class _OrganizeTeamsViewState extends State { - final Random _random = Random(); - late final List<_TeamDraft> _teams; - - List get _players => widget.match.players; - - @override - void initState() { - super.initState(); - _teams = List.generate(2, _createTeamDraft); - _redistributePlayers(); - } - - @override - void dispose() { - for (final team in _teams) { - team.dispose(); - } - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: const Text('Organize Teams')), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.only(top: 12, bottom: 12), - itemCount: _teams.length, - itemBuilder: (context, index) { - return TeamCreationTile( - color: _teams[index].color, - controller: _teams[index].nameController, - players: _teams[index].members, - hintText: 'Team ${index + 1}', - onDelete: () => _removeTeam(index), - onColorSelection: (color) { - setState(() { - _teams[index].color = color; - }); - }, - onPlayerTap: (player) => - _showMovePlayerSheet(player, index), - ); - }, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MainMenuButton( - icon: Icons.cached, - onPressed: () => setState(() { - _redistributePlayers(); - }), - ), - const SizedBox(width: 15), - MainMenuButton( - text: 'Add team', - icon: Icons.emoji_events, - onPressed: _teams.length >= widget.match.players.length - ? null - : _addTeam, - ), - const SizedBox(width: 15), - MainMenuButton( - icon: Icons.check, - onPressed: () async { - final match = await createMatchWithTeams(); - if (context.mounted) { - Navigator.pushReplacement( - context, - adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => MatchResultView(match: match), - ), - ); - } - }, - ), - ], - ), - ], - ), - ), - ); - } - - Future createMatchWithTeams() async { - final teams = _teams - .map( - (team) => Team( - name: team.nameController.text.trim().isNotEmpty - ? team.nameController.text.trim() - : 'Team ${_teams.indexOf(team) + 1}', - color: team.color, - members: team.members, - ), - ) - .toList(); - final db = Provider.of(context, listen: false); - await db.teamDao.addTeamsAsList(teams: teams, matchId: widget.match.id); - return widget.match.copyWith(teams: teams); - } - - _TeamDraft _createTeamDraft(int index) { - return _TeamDraft( - nameController: TextEditingController(text: 'Team ${index + 1}'), - color: getTeamColor(index), - ); - } - - void _addTeam() { - setState(() { - _teams.add(_createTeamDraft(_teams.length)); - _redistributePlayers(); - }); - } - - void _removeTeam(int index) { - setState(() { - final removedTeam = _teams.removeAt(index); - removedTeam.dispose(); - - if (_teams.isEmpty) { - _teams.add(_createTeamDraft(0)); - } - - _redistributePlayers(); - }); - } - - void _movePlayer(Player player, int fromTeamIndex, int toTeamIndex) { - setState(() { - _teams[fromTeamIndex].members.remove(player); - _teams[toTeamIndex].members.add(player); - }); - } - - void _showMovePlayerSheet(Player player, int fromTeamIndex) { - final otherTeams = [ - for (int i = 0; i < _teams.length; i++) - if (i != fromTeamIndex) (index: i, team: _teams[i]), - ]; - - if (otherTeams.isEmpty) return; - - showModalBottomSheet( - context: context, - backgroundColor: CustomTheme.backgroundColor, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (context) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Text( - '${player.name} verschieben in …', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor, - ), - ), - ), - const Divider(), - ...otherTeams.map((entry) { - final teamName = - entry.team.nameController.text.trim().isNotEmpty - ? entry.team.nameController.text.trim() - : 'Team ${entry.index + 1}'; - final teamColor = getColorFromGameColor(entry.team.color); - return ListTile( - leading: CircleAvatar( - radius: 12, - backgroundColor: teamColor, - ), - title: Text( - teamName, - style: const TextStyle(color: CustomTheme.textColor), - ), - onTap: () { - Navigator.pop(context); - _movePlayer(player, fromTeamIndex, entry.index); - }, - ); - }), - ], - ), - ), - ); - }, - ); - } - - void _redistributePlayers() { - for (final team in _teams) { - team.members.clear(); - } - - if (_players.isEmpty || _teams.isEmpty) { - return; - } - - final shuffledPlayers = [..._players]..shuffle(_random); - - for (int i = 0; i < shuffledPlayers.length; i++) { - final teamIndex = i % _teams.length; - _teams[teamIndex].members.add(shuffledPlayers[i]); - } - } -} - -class _TeamDraft { - _TeamDraft({required this.nameController, required this.color}); - - final TextEditingController nameController; - GameColor color; - final List members = []; - - void dispose() { - nameController.dispose(); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart new file mode 100644 index 0000000..12071be --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; +import 'package:tallee/presentation/widgets/player_selection.dart'; + +class EditMembersView extends StatefulWidget { + const EditMembersView({ + super.key, + required this.matchPlayer, + required this.teamMember, + }); + + final List matchPlayer; + + final List teamMember; + + @override + State createState() => _EditMembersViewState(); +} + +class _EditMembersViewState extends State { + List selectedPlayers = []; + List matchPlayer = []; + + @override + void initState() { + selectedPlayers = [...widget.teamMember]; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Scaffold( + appBar: AppBar( + title: Text(loc.edit_members), + leading: HapticIconButton( + onPressed: selectedPlayers.isNotEmpty + ? () => Navigator.pop(context, selectedPlayers) + : null, + icon: const Icon(Icons.arrow_back_ios_new_outlined), + ), + ), + body: PlayerSelection( + initialSelectedPlayers: widget.teamMember, + availablePlayers: widget.matchPlayer, + onChanged: (List newSelectedPlayers) { + setState(() { + selectedPlayers = newSelectedPlayers; + }); + }, + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart new file mode 100644 index 0000000..47f794c --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart @@ -0,0 +1,272 @@ +import 'dart:math'; + +import 'package:flutter/material.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/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/team.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; + +class OrganizeTeamsView extends StatefulWidget { + const OrganizeTeamsView({super.key, required this.match}); + + final Match match; + + @override + State createState() => _OrganizeTeamsViewState(); +} + +class _OrganizeTeamsViewState extends State { + final Random random = Random(); + late List teams; + late List nameController; + + final int initialTeamCount = 2; + List get matchPlayers => widget.match.players; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final loc = AppLocalizations.of(context); + + // Init the teams + teams = List.generate( + initialTeamCount, + (index) => Team( + name: '${loc.team} ${index + 1}', + color: getTeamColor(index), + members: [], + ), + ); + + // Init the controllers + nameController = teams.map(getNewController).toList(); + redistributePlayers(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(loc.create_teams)), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 12, bottom: 12), + itemCount: teams.length, + itemBuilder: (context, index) { + return TeamCreationTile( + color: teams[index].color, + controller: nameController[index], + players: teams[index].members, + hintText: '${loc.team} ${index + 1}', + onEdit: () async { + final newPlayers = await Navigator.push( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => EditMembersView( + matchPlayer: widget.match.players, + teamMember: teams[index].members, + ), + ), + ); + + setState(() { + // Remove the selected players from every team + for (final player in newPlayers) { + for (final team in teams) { + if (team.members.contains(player)) { + team.members.remove(player); + } + } + } + + // Add the selected players to the current team + teams[index] = teams[index].copyWith( + members: newPlayers, + ); + }); + }, + onDelete: teams.length >= 3 + ? () => _removeTeam(index) + : null, + onColorSelection: (color) { + setState(() { + teams[index] = teams[index].copyWith(color: color); + }); + }, + ); + }, + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MainMenuButton( + icon: Icons.cached, + text: loc.redistribute, + onPressed: () => setState(() { + redistributePlayers(); + }), + ), + const SizedBox(width: 15), + MainMenuButton( + icon: Icons.add, + onPressed: teams.length >= widget.match.players.length + ? null + : addTeam, + ), + const SizedBox(width: 15), + MainMenuButton( + icon: Icons.check, + onPressed: teams.every((team) => team.members.isNotEmpty) + ? () async { + final match = await createMatchWithTeams(); + if (context.mounted) { + Navigator.pushAndRemoveUntil( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => + MatchResultView(match: match), + ), + (route) => route.isFirst, + ); + } + } + : null, + ), + ], + ), + ], + ), + ), + ); + } + + /// Adds a new team to the list of teams, creates a corresponding controller, + /// and redistributes the players among all teams. + void addTeam() { + setState(() { + final newTeam = getNewTeam(); + teams.add(newTeam); + nameController.add(getNewController(newTeam)); + redistributePlayers(); + }); + } + + /// Creates a new team with a default name and color based on the current number + Team getNewTeam() { + final loc = AppLocalizations.of(context); + return Team( + name: '${loc.team} ${teams.length + 1}', + color: getTeamColor(teams.length), + members: [], + ); + } + + /// Builds a [TextEditingController] for the given team and sets up a listener + /// to update the team's name whenever the text changes. + TextEditingController getNewController(Team team) { + final textController = TextEditingController(text: team.name); + textController.addListener(() { + final index = teams.indexWhere((t) => t.id == team.id); + if (index == -1) return; + teams[index] = teams[index].copyWith(name: textController.text); + }); + return textController; + } + + /// Removes the team with the given index and redistributes its players to the + /// remaining teams. If there are less than 2 teams the removed team gets + /// replaced with a new one + void _removeTeam(int index) { + final loc = AppLocalizations.of(context); + + setState(() { + final removedTeam = teams.removeAt(index); + final removedController = nameController.removeAt(index); + removedController.dispose(); + if (teams.length < 2) { + final fallbackTeam = getNewTeam(); + teams.add(fallbackTeam); + nameController.add(getNewController(fallbackTeam)); + } + + // Update index-based team names + for (int i = 0; i < nameController.length; i++) { + if (nameController[i].text.contains( + RegExp('^${RegExp.escape(loc.team)} \\d+\$'), + )) { + nameController[i].text = '${loc.team} ${i + 1}'; + } + } + + addToSmallestTeams(removedTeam.members); + }); + } + + /// Adds the given players to the teams with the least amount of members + /// [orphanedPlayers] The players to be added to the teams. + void addToSmallestTeams(List orphanedPlayers) { + if (teams.isEmpty || orphanedPlayers.isEmpty) return; + + for (final player in orphanedPlayers) { + var targetIndex = 0; + for (var i = 1; i < teams.length; i++) { + if (teams[i].members.length < teams[targetIndex].members.length) { + targetIndex = i; + } + } + teams[targetIndex].members.add(player); + } + } + + // Iterates through all teams and redistributes players randomly and + // as evenly as possible. + void redistributePlayers() { + for (final team in teams) { + team.members.clear(); + } + + if (matchPlayers.isEmpty || teams.isEmpty) { + return; + } + + final shuffledPlayers = [...matchPlayers]..shuffle(random); + + for (int i = 0; i < shuffledPlayers.length; i++) { + final teamIndex = i % teams.length; + teams[teamIndex].members.add(shuffledPlayers[i]); + } + } + + /// Saves the teams to the database and returns the updated match with the teams. + Future createMatchWithTeams() async { + final db = Provider.of(context, listen: false); + final match = widget.match.copyWith(teams: teams); + await db.matchDao.addMatch(match: match); + return match; + } + + @override + void dispose() { + for (final c in nameController) { + c.dispose(); + } + super.dispose(); + } +} diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 4f9ab34..39cfef8 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -333,6 +333,7 @@ class _MatchResultViewState extends State { } else { return await db.teamDao.setWinnerTeams( matchId: widget.match.id, + winners: _selectedTeams.toList(), ); } diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index a7f60c6..69b7df7 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -142,6 +142,7 @@ class _MatchViewState extends State { /// Loads the matches from the database and sorts them by creation date. void loadMatches() { + print('Loading matches from database'); isLoading = true; Future.wait([ db.matchDao.getAllMatches(), diff --git a/lib/presentation/widgets/tiles/team_creation_tile.dart b/lib/presentation/widgets/tiles/team_creation_tile.dart index fb3e9c5..8a10150 100644 --- a/lib/presentation/widgets/tiles/team_creation_tile.dart +++ b/lib/presentation/widgets/tiles/team_creation_tile.dart @@ -4,6 +4,8 @@ import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -14,9 +16,9 @@ class TeamCreationTile extends StatefulWidget { required this.controller, required this.players, required this.hintText, + this.onEdit, this.onDelete, this.onColorSelection, - this.onPlayerTap, }); final GameColor color; @@ -27,28 +29,34 @@ class TeamCreationTile extends StatefulWidget { final String hintText; + final VoidCallback? onEdit; + final VoidCallback? onDelete; final ValueChanged? onColorSelection; - final void Function(Player player)? onPlayerTap; - @override State createState() => _TeamCreationTileState(); } class _TeamCreationTileState extends State { + final teamColors = List.generate( + GameColor.values.length, + (index) => getTeamColor(index), + ); @override Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Container( margin: CustomTheme.standardMargin, - padding: const EdgeInsets.all(12), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: CustomTheme.standardBoxDecoration, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: TextInputField( @@ -57,17 +65,19 @@ class _TeamCreationTileState extends State { maxLength: Constants.MAX_TEAM_NAME_LENGTH, ), ), - const SizedBox(width: 8), - IconButton( - onPressed: () => widget.onDelete?.call(), - icon: const Icon(Icons.delete, size: 24), + const SizedBox(width: 12), + AnimatedDialogButton( + content: const Icon(Icons.delete), + isDescructive: true, + onPressed: widget.onDelete, + buttonText: '', ), ], ), const SizedBox(height: 8), - const Text( - 'Color', - style: TextStyle( + Text( + loc.color, + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: CustomTheme.textColor, @@ -77,7 +87,7 @@ class _TeamCreationTileState extends State { Wrap( spacing: 8, runSpacing: 8, - children: GameColor.values.map((color) { + children: teamColors.map((color) { final isSelected = widget.color == color; return GestureDetector( onTap: () { @@ -102,9 +112,9 @@ class _TeamCreationTileState extends State { }).toList(), ), const SizedBox(height: 12), - const Text( - 'Players', - style: TextStyle( + Text( + loc.players, + style: const TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: CustomTheme.textColor, @@ -112,9 +122,9 @@ class _TeamCreationTileState extends State { ), const SizedBox(height: 8), if (widget.players.isEmpty) - const Text( - 'Keine Spieler:innen zugewiesen', - style: TextStyle(color: CustomTheme.hintColor), + Text( + loc.no_players_selected, + style: const TextStyle(color: CustomTheme.hintColor), ) else Wrap( @@ -122,18 +132,25 @@ class _TeamCreationTileState extends State { runSpacing: 8, children: widget.players .map( - (player) => GestureDetector( - onTap: () => widget.onPlayerTap?.call(player), - child: TextIconTile( - text: player.name, - suffixText: getNameCountText(player), - iconEnabled: widget.onPlayerTap != null, - onIconTap: () => widget.onPlayerTap?.call(player), - ), + (player) => TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + iconEnabled: false, ), ) .toList(), ), + if (widget.onEdit != null) + Padding( + padding: const EdgeInsets.only(top: 12), + child: AnimatedDialogButton( + buttonConstraints: const BoxConstraints( + minWidth: double.infinity, + ), + buttonText: loc.edit_members, + onPressed: widget.onEdit!, + ), + ), ], ), ); -- 2.49.1 From e61af14827c485755be5786b19d2b46beb1e8169 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 00:07:09 +0200 Subject: [PATCH 21/51] Refactoring --- .../match_view/create_match/create_match_view.dart | 4 ++-- .../create_teams_view.dart} | 10 +++++----- .../edit_members_view.dart | 0 3 files changed, 7 insertions(+), 7 deletions(-) rename lib/presentation/views/main_menu/match_view/create_match/{team_match/organize_teams_view.dart => create_teams/create_teams_view.dart} (96%) rename lib/presentation/views/main_menu/match_view/create_match/{team_match => create_teams}/edit_members_view.dart (100%) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index dbfba47..c2dfeba 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -12,7 +12,7 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; import 'package:tallee/presentation/widgets/player_selection.dart'; @@ -300,7 +300,7 @@ class _CreateMatchViewState extends State { context, adaptivePageRoute( fullscreenDialog: !isTeamMatch, - builder: (context) => OrganizeTeamsView(match: match), + builder: (context) => CreateTeamsView(match: match), ), ); } diff --git a/lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart similarity index 96% rename from lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart rename to lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index 47f794c..e410834 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/team_match/organize_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -10,21 +10,21 @@ import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; -class OrganizeTeamsView extends StatefulWidget { - const OrganizeTeamsView({super.key, required this.match}); +class CreateTeamsView extends StatefulWidget { + const CreateTeamsView({super.key, required this.match}); final Match match; @override - State createState() => _OrganizeTeamsViewState(); + State createState() => _CreateTeamsViewState(); } -class _OrganizeTeamsViewState extends State { +class _CreateTeamsViewState extends State { final Random random = Random(); late List teams; late List nameController; diff --git a/lib/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart similarity index 100% rename from lib/presentation/views/main_menu/match_view/create_match/team_match/edit_members_view.dart rename to lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart -- 2.49.1 From 0c4035784764456a1fcbbd21d4cfc5e83e0ca2d9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 00:09:51 +0200 Subject: [PATCH 22/51] fix: match view state issue --- .../match_view/create_match/create_match_view.dart | 5 ++++- .../create_match/create_teams/create_teams_view.dart | 9 ++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index c2dfeba..65586a9 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -300,7 +300,10 @@ class _CreateMatchViewState extends State { context, adaptivePageRoute( fullscreenDialog: !isTeamMatch, - builder: (context) => CreateTeamsView(match: match), + builder: (context) => CreateTeamsView( + match: match, + onWinnerChanged: widget.onWinnerChanged, + ), ), ); } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index e410834..295cccd 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -16,9 +16,10 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; class CreateTeamsView extends StatefulWidget { - const CreateTeamsView({super.key, required this.match}); + const CreateTeamsView({super.key, required this.match, this.onWinnerChanged}); final Match match; + final VoidCallback? onWinnerChanged; @override State createState() => _CreateTeamsViewState(); @@ -140,8 +141,10 @@ class _CreateTeamsViewState extends State { context, adaptivePageRoute( fullscreenDialog: true, - builder: (context) => - MatchResultView(match: match), + builder: (context) => MatchResultView( + match: match, + onWinnerChanged: widget.onWinnerChanged, + ), ), (route) => route.isFirst, ); -- 2.49.1 From 8b1a447bd92e98a88b8aba13aeda291a481eccc7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 00:14:15 +0200 Subject: [PATCH 23/51] locs, comments, removed print --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + lib/l10n/generated/app_localizations.dart | 6 ++++++ lib/l10n/generated/app_localizations_de.dart | 3 +++ lib/l10n/generated/app_localizations_en.dart | 3 +++ .../create_match/create_match_view.dart | 2 +- .../create_teams/create_teams_view.dart | 17 ++++++++++++----- .../create_teams/edit_members_view.dart | 1 - .../views/main_menu/match_view/match_view.dart | 1 - 9 files changed, 27 insertions(+), 8 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 679fcc1..5b37a5e 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -137,6 +137,7 @@ "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "team": "Team", + "team_match": "Teamspiel", "teams": "Teams", "there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b41a6f7..e2830e9 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -146,6 +146,7 @@ } }, "team": "Team", + "team_match": "Team Match", "teams": "Teams", "there_are_no_games_matching_your_search": "There are no games matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 4ce2c74..e51aa32 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -872,6 +872,12 @@ abstract class AppLocalizations { /// **'Team'** String get team; + /// No description provided for @team_match. + /// + /// In en, this message translates to: + /// **'Team Match'** + String get team_match; + /// No description provided for @teams. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index c634f55..ccdc989 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -419,6 +419,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get team => 'Team'; + @override + String get team_match => 'Teamspiel'; + @override String get teams => 'Teams'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 8e79c77..feb085a 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -419,6 +419,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get team => 'Team'; + @override + String get team_match => 'Team Match'; + @override String get teams => 'Teams'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 65586a9..fd14508 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -151,7 +151,7 @@ class _CreateMatchViewState extends State { if (!isEditMode()) ChooseTile( - title: 'Team Match', + title: loc.team_match, trailing: Switch.adaptive( activeTrackColor: CustomTheme.primaryColor, padding: const EdgeInsets.symmetric(vertical: -15), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index 295cccd..afee2fc 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -27,11 +27,11 @@ class CreateTeamsView extends StatefulWidget { class _CreateTeamsViewState extends State { final Random random = Random(); + List get matchPlayers => widget.match.players; + late List teams; late List nameController; - final int initialTeamCount = 2; - List get matchPlayers => widget.match.players; @override void didChangeDependencies() { @@ -101,9 +101,9 @@ class _CreateTeamsViewState extends State { ); }); }, - onDelete: teams.length >= 3 - ? () => _removeTeam(index) - : null, + onDelete: teams.length <= 2 + ? null + : () => _removeTeam(index), onColorSelection: (color) { setState(() { teams[index] = teams[index].copyWith(color: color); @@ -113,9 +113,12 @@ class _CreateTeamsViewState extends State { }, ), ), + + // Button row Row( mainAxisAlignment: MainAxisAlignment.center, children: [ + // Redistribute MainMenuButton( icon: Icons.cached, text: loc.redistribute, @@ -124,6 +127,8 @@ class _CreateTeamsViewState extends State { }), ), const SizedBox(width: 15), + + // Add new team MainMenuButton( icon: Icons.add, onPressed: teams.length >= widget.match.players.length @@ -131,6 +136,8 @@ class _CreateTeamsViewState extends State { : addTeam, ), const SizedBox(width: 15), + + // Confirm teams and start match MainMenuButton( icon: Icons.check, onPressed: teams.every((team) => team.members.isNotEmpty) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart index 12071be..78eecef 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart @@ -21,7 +21,6 @@ class EditMembersView extends StatefulWidget { class _EditMembersViewState extends State { List selectedPlayers = []; - List matchPlayer = []; @override void initState() { diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 69b7df7..a7f60c6 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -142,7 +142,6 @@ class _MatchViewState extends State { /// Loads the matches from the database and sorts them by creation date. void loadMatches() { - print('Loading matches from database'); isLoading = true; Future.wait([ db.matchDao.getAllMatches(), -- 2.49.1 From aaeb4bf18c14fe09bf5555fe332f427f5fad2dc7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 00:34:48 +0200 Subject: [PATCH 24/51] fix: async test --- test/services/data_transfer_service_test.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 43e70fb..e88f0ad 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -925,7 +925,12 @@ void main() { expect(jsonString, isNotEmpty); - final isValid = await DataTransferService.validateJsonSchema(jsonString); + // Schema validation requires real async operations (rootBundle, + // HttpClient within json_schema). These must run via + // tester.runAsync, otherwise the test hangs due to a pending timer. + final isValid = await tester.runAsync( + () => DataTransferService.validateJsonSchema(jsonString), + ); expect(isValid, true); }); }); -- 2.49.1 From 63f050b34fa2e3ee420a77e8bfc2a11c602986de Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Thu, 21 May 2026 10:34:54 +0200 Subject: [PATCH 25/51] sort arb files --- lib/l10n/arb/app_de.arb | 18 +++++++++--------- lib/l10n/arb/app_en.arb | 19 +++++++++---------- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 5b37a5e..21a8d25 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -25,8 +25,8 @@ "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", "create_new_match": "Neues Spiel erstellen", - "created_on": "Erstellt am", "create_teams": "Teams erstellen", + "created_on": "Erstellt am", "data": "Daten", "data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_exported": "Daten erfolgreich exportiert", @@ -45,8 +45,8 @@ }, "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", - "drag_to_set_placement": "Ziehen um Platzierung zu setzen", "description": "Beschreibung", + "drag_to_set_placement": "Ziehen um Platzierung zu setzen", "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", @@ -68,6 +68,7 @@ "group_name": "Gruppenname", "group_profile": "Gruppenprofil", "groups": "Gruppen", + "highest_score": "Höchste Punkte", "home": "Startseite", "import_canceled": "Import abgebrochen", "import_data": "Daten importieren", @@ -78,17 +79,20 @@ "legal_notice": "Impressum", "licenses": "Lizenzen", "live_edit_mode": "Live-Bearbeitungsmodus", + "loser": "Verlierer:in", + "lowest_score": "Niedrigste Punkte", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", "matches": "Spiele", "members": "Mitglieder", "most_points": "Höchste Punkte", + "multiple_winners": "Mehrere Gewinner:innen", "no_data_available": "Keine Daten verfügbar", "no_games_created_yet": "Noch keine Spielvorlagen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt", - "no_licenses_found": "Keine Lizenzen gefunden", "no_license_text_available": "Kein Lizenztext verfügbar", + "no_licenses_found": "Keine Lizenzen gefunden", "no_matches_created_yet": "Noch keine Spiele erstellt", "no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", @@ -100,8 +104,8 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", - "placement": "Platzierung", "place": "Platz", + "placement": "Platzierung", "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", @@ -122,17 +126,13 @@ "save_changes": "Änderungen speichern", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", + "select_loser": "Verlierer:in wählen", "select_winner": "Gewinner:in wählen", "select_winners": "Gewinner:innen wählen", - "select_loser": "Verlierer:in wählen", "selected_players": "Ausgewählte Spieler:innen", "settings": "Einstellungen", "single_loser": "Ein:e Verlierer:in", "single_winner": "Ein:e Gewinner:in", - "highest_score": "Höchste Punkte", - "loser": "Verlierer:in", - "lowest_score": "Niedrigste Punkte", - "multiple_winners": "Mehrere Gewinner:innen", "statistics": "Statistiken", "stats": "Statistiken", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index e2830e9..b80b909 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,6 +1,5 @@ { "@@locale": "en", - "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -25,9 +24,9 @@ "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", - "created_on": "Created on", "create_new_match": "Create new match", "create_teams": "Create teams", + "created_on": "Created on", "data": "Data", "data_successfully_deleted": "Data successfully deleted", "data_successfully_exported": "Data successfully exported", @@ -46,8 +45,8 @@ }, "delete_group": "Delete Group", "delete_match": "Delete Match", - "drag_to_set_placement": "Drag to set placement", "description": "Description", + "drag_to_set_placement": "Drag to set placement", "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", @@ -69,6 +68,7 @@ "group_name": "Group name", "group_profile": "Group Profile", "groups": "Groups", + "highest_score": "Highest Score", "home": "Home", "import_canceled": "Import canceled", "import_data": "Import data", @@ -79,17 +79,20 @@ "legal_notice": "Legal Notice", "licenses": "Licenses", "live_edit_mode": "Live Edit Mode", + "loser": "Loser", + "lowest_score": "Lowest Score", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", "matches": "Matches", "members": "Members", "most_points": "Most Points", + "multiple_winners": "Multiple Winners", "no_data_available": "No data available", "no_games_created_yet": "No games created yet", "no_groups_created_yet": "No groups created yet", - "no_licenses_found": "No licenses found", "no_license_text_available": "No license text available", + "no_licenses_found": "No licenses found", "no_matches_created_yet": "No matches created yet", "no_players_created_yet": "No players created yet", "no_players_found_with_that_name": "No players found with that name", @@ -101,8 +104,8 @@ "none": "None", "none_group": "None", "not_available": "Not available", - "placement": "Placement", "place": "place", + "placement": "Placement", "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", @@ -122,17 +125,13 @@ "save_changes": "Save Changes", "search_for_groups": "Search for groups", "search_for_players": "Search for players", + "select_loser": "Select Loser", "select_winner": "Select Winner", "select_winners": "Select Winners", - "select_loser": "Select Loser", "selected_players": "Selected players", "settings": "Settings", "single_loser": "Single Loser", "single_winner": "Single Winner", - "highest_score": "Highest Score", - "loser": "Loser", - "lowest_score": "Lowest Score", - "multiple_winners": "Multiple Winners", "statistics": "Statistics", "stats": "Stats", "successfully_added_player": "Successfully added player {playerName}", -- 2.49.1 From 1f9ba964017ecf9860da5e7eca264c5e3d3a63e5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:27:34 +0200 Subject: [PATCH 26/51] feat: new team member selection --- lib/l10n/arb/app_de.arb | 3 +- lib/l10n/arb/app_en.arb | 3 +- lib/l10n/generated/app_localizations.dart | 126 ++++---- lib/l10n/generated/app_localizations_de.dart | 57 ++-- lib/l10n/generated/app_localizations_en.dart | 57 ++-- .../create_teams/create_teams_view.dart | 74 +---- .../create_teams/edit_members_view.dart | 56 ---- .../create_teams/manage_members_view.dart | 286 ++++++++++++++++++ .../widgets/tiles/team_creation_tile.dart | 47 --- 9 files changed, 422 insertions(+), 287 deletions(-) delete mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart create mode 100644 lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 21a8d25..f97b827 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -1,5 +1,6 @@ { "@@locale": "de", + "add_team": "Team hinzufügen", "all_players": "Alle Spieler:innen", "all_players_selected": "Alle Spieler:innen ausgewählt", "amount_of_matches": "Anzahl der Spiele", @@ -50,7 +51,6 @@ "edit_game": "Spielvorlage bearbeiten", "edit_group": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten", - "edit_members": "Mitglieder bearbeiten", "enter_points": "Punkte eingeben", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", @@ -81,6 +81,7 @@ "live_edit_mode": "Live-Bearbeitungsmodus", "loser": "Verlierer:in", "lowest_score": "Niedrigste Punkte", + "manage_members": "Mitglieder bearbeiten", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b80b909..79883fa 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,5 +1,6 @@ { "@@locale": "en", + "add_team": "Add Team", "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -50,7 +51,6 @@ "edit_game": "Edit Game", "edit_group": "Edit Group", "edit_match": "Edit Match", - "edit_members": "Edit Members", "enter_points": "Enter points", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", @@ -81,6 +81,7 @@ "live_edit_mode": "Live Edit Mode", "loser": "Loser", "lowest_score": "Lowest Score", + "manage_members": "Manage Members", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index e51aa32..2f7970d 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -98,6 +98,12 @@ abstract class AppLocalizations { Locale('en'), ]; + /// No description provided for @add_team. + /// + /// In en, this message translates to: + /// **'Add Team'** + String get add_team; + /// No description provided for @all_players. /// /// In en, this message translates to: @@ -242,12 +248,6 @@ abstract class AppLocalizations { /// **'Create new group'** String get create_new_group; - /// No description provided for @created_on. - /// - /// In en, this message translates to: - /// **'Created on'** - String get created_on; - /// No description provided for @create_new_match. /// /// In en, this message translates to: @@ -260,6 +260,12 @@ abstract class AppLocalizations { /// **'Create teams'** String get create_teams; + /// No description provided for @created_on. + /// + /// In en, this message translates to: + /// **'Created on'** + String get created_on; + /// No description provided for @data. /// /// In en, this message translates to: @@ -326,18 +332,18 @@ abstract class AppLocalizations { /// **'Delete Match'** String get delete_match; - /// No description provided for @drag_to_set_placement. - /// - /// In en, this message translates to: - /// **'Drag to set placement'** - String get drag_to_set_placement; - /// No description provided for @description. /// /// In en, this message translates to: /// **'Description'** String get description; + /// No description provided for @drag_to_set_placement. + /// + /// In en, this message translates to: + /// **'Drag to set placement'** + String get drag_to_set_placement; + /// No description provided for @edit_game. /// /// In en, this message translates to: @@ -356,12 +362,6 @@ abstract class AppLocalizations { /// **'Edit Match'** String get edit_match; - /// No description provided for @edit_members. - /// - /// In en, this message translates to: - /// **'Edit Members'** - String get edit_members; - /// No description provided for @enter_points. /// /// In en, this message translates to: @@ -464,6 +464,12 @@ abstract class AppLocalizations { /// **'Groups'** String get groups; + /// No description provided for @highest_score. + /// + /// In en, this message translates to: + /// **'Highest Score'** + String get highest_score; + /// No description provided for @home. /// /// In en, this message translates to: @@ -524,6 +530,24 @@ abstract class AppLocalizations { /// **'Live Edit Mode'** String get live_edit_mode; + /// No description provided for @loser. + /// + /// In en, this message translates to: + /// **'Loser'** + String get loser; + + /// No description provided for @lowest_score. + /// + /// In en, this message translates to: + /// **'Lowest Score'** + String get lowest_score; + + /// No description provided for @manage_members. + /// + /// In en, this message translates to: + /// **'Manage Members'** + String get manage_members; + /// No description provided for @match_in_progress. /// /// In en, this message translates to: @@ -560,6 +584,12 @@ abstract class AppLocalizations { /// **'Most Points'** String get most_points; + /// No description provided for @multiple_winners. + /// + /// In en, this message translates to: + /// **'Multiple Winners'** + String get multiple_winners; + /// No description provided for @no_data_available. /// /// In en, this message translates to: @@ -578,18 +608,18 @@ abstract class AppLocalizations { /// **'No groups created yet'** String get no_groups_created_yet; - /// No description provided for @no_licenses_found. - /// - /// In en, this message translates to: - /// **'No licenses found'** - String get no_licenses_found; - /// No description provided for @no_license_text_available. /// /// In en, this message translates to: /// **'No license text available'** String get no_license_text_available; + /// No description provided for @no_licenses_found. + /// + /// In en, this message translates to: + /// **'No licenses found'** + String get no_licenses_found; + /// No description provided for @no_matches_created_yet. /// /// In en, this message translates to: @@ -656,18 +686,18 @@ abstract class AppLocalizations { /// **'Not available'** String get not_available; - /// No description provided for @placement. - /// - /// In en, this message translates to: - /// **'Placement'** - String get placement; - /// No description provided for @place. /// /// In en, this message translates to: /// **'place'** String get place; + /// No description provided for @placement. + /// + /// In en, this message translates to: + /// **'Placement'** + String get placement; + /// No description provided for @played_matches. /// /// In en, this message translates to: @@ -782,6 +812,12 @@ abstract class AppLocalizations { /// **'Search for players'** String get search_for_players; + /// No description provided for @select_loser. + /// + /// In en, this message translates to: + /// **'Select Loser'** + String get select_loser; + /// No description provided for @select_winner. /// /// In en, this message translates to: @@ -794,12 +830,6 @@ abstract class AppLocalizations { /// **'Select Winners'** String get select_winners; - /// No description provided for @select_loser. - /// - /// In en, this message translates to: - /// **'Select Loser'** - String get select_loser; - /// No description provided for @selected_players. /// /// In en, this message translates to: @@ -824,30 +854,6 @@ abstract class AppLocalizations { /// **'Single Winner'** String get single_winner; - /// No description provided for @highest_score. - /// - /// In en, this message translates to: - /// **'Highest Score'** - String get highest_score; - - /// No description provided for @loser. - /// - /// In en, this message translates to: - /// **'Loser'** - String get loser; - - /// No description provided for @lowest_score. - /// - /// In en, this message translates to: - /// **'Lowest Score'** - String get lowest_score; - - /// No description provided for @multiple_winners. - /// - /// In en, this message translates to: - /// **'Multiple Winners'** - String get multiple_winners; - /// No description provided for @statistics. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index ccdc989..ce42807 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -8,6 +8,9 @@ import 'app_localizations.dart'; class AppLocalizationsDe extends AppLocalizations { AppLocalizationsDe([String locale = 'de']) : super(locale); + @override + String get add_team => 'Team hinzufügen'; + @override String get all_players => 'Alle Spieler:innen'; @@ -82,15 +85,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_group => 'Neue Gruppe erstellen'; - @override - String get created_on => 'Erstellt am'; - @override String get create_new_match => 'Neues Spiel erstellen'; @override String get create_teams => 'Teams erstellen'; + @override + String get created_on => 'Erstellt am'; + @override String get data => 'Daten'; @@ -135,10 +138,10 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_match => 'Spiel löschen'; @override - String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; + String get description => 'Beschreibung'; @override - String get description => 'Beschreibung'; + String get drag_to_set_placement => 'Ziehen um Platzierung zu setzen'; @override String get edit_game => 'Spielvorlage bearbeiten'; @@ -149,9 +152,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get edit_match => 'Gruppe bearbeiten'; - @override - String get edit_members => 'Mitglieder bearbeiten'; - @override String get enter_points => 'Punkte eingeben'; @@ -207,6 +207,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get groups => 'Gruppen'; + @override + String get highest_score => 'Höchste Punkte'; + @override String get home => 'Startseite'; @@ -237,6 +240,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override + String get loser => 'Verlierer:in'; + + @override + String get lowest_score => 'Niedrigste Punkte'; + + @override + String get manage_members => 'Mitglieder bearbeiten'; + @override String get match_in_progress => 'Spiel läuft...'; @@ -255,6 +267,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get most_points => 'Höchste Punkte'; + @override + String get multiple_winners => 'Mehrere Gewinner:innen'; + @override String get no_data_available => 'Keine Daten verfügbar'; @@ -265,10 +280,10 @@ class AppLocalizationsDe extends AppLocalizations { String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; @override - String get no_licenses_found => 'Keine Lizenzen gefunden'; + String get no_license_text_available => 'Kein Lizenztext verfügbar'; @override - String get no_license_text_available => 'Kein Lizenztext verfügbar'; + String get no_licenses_found => 'Keine Lizenzen gefunden'; @override String get no_matches_created_yet => 'Noch keine Spiele erstellt'; @@ -305,10 +320,10 @@ class AppLocalizationsDe extends AppLocalizations { String get not_available => 'Nicht verfügbar'; @override - String get placement => 'Platzierung'; + String get place => 'Platz'; @override - String get place => 'Platz'; + String get placement => 'Platzierung'; @override String get played_matches => 'Gespielte Spiele'; @@ -372,15 +387,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get search_for_players => 'Nach Spieler:innen suchen'; + @override + String get select_loser => 'Verlierer:in wählen'; + @override String get select_winner => 'Gewinner:in wählen'; @override String get select_winners => 'Gewinner:innen wählen'; - @override - String get select_loser => 'Verlierer:in wählen'; - @override String get selected_players => 'Ausgewählte Spieler:innen'; @@ -393,18 +408,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get single_winner => 'Ein:e Gewinner:in'; - @override - String get highest_score => 'Höchste Punkte'; - - @override - String get loser => 'Verlierer:in'; - - @override - String get lowest_score => 'Niedrigste Punkte'; - - @override - String get multiple_winners => 'Mehrere Gewinner:innen'; - @override String get statistics => 'Statistiken'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index feb085a..ea3234b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -8,6 +8,9 @@ import 'app_localizations.dart'; class AppLocalizationsEn extends AppLocalizations { AppLocalizationsEn([String locale = 'en']) : super(locale); + @override + String get add_team => 'Add Team'; + @override String get all_players => 'All players'; @@ -82,15 +85,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_group => 'Create new group'; - @override - String get created_on => 'Created on'; - @override String get create_new_match => 'Create new match'; @override String get create_teams => 'Create teams'; + @override + String get created_on => 'Created on'; + @override String get data => 'Data'; @@ -135,10 +138,10 @@ class AppLocalizationsEn extends AppLocalizations { String get delete_match => 'Delete Match'; @override - String get drag_to_set_placement => 'Drag to set placement'; + String get description => 'Description'; @override - String get description => 'Description'; + String get drag_to_set_placement => 'Drag to set placement'; @override String get edit_game => 'Edit Game'; @@ -149,9 +152,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get edit_match => 'Edit Match'; - @override - String get edit_members => 'Edit Members'; - @override String get enter_points => 'Enter points'; @@ -207,6 +207,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get groups => 'Groups'; + @override + String get highest_score => 'Highest Score'; + @override String get home => 'Home'; @@ -237,6 +240,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get live_edit_mode => 'Live Edit Mode'; + @override + String get loser => 'Loser'; + + @override + String get lowest_score => 'Lowest Score'; + + @override + String get manage_members => 'Manage Members'; + @override String get match_in_progress => 'Match in progress...'; @@ -255,6 +267,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get most_points => 'Most Points'; + @override + String get multiple_winners => 'Multiple Winners'; + @override String get no_data_available => 'No data available'; @@ -265,10 +280,10 @@ class AppLocalizationsEn extends AppLocalizations { String get no_groups_created_yet => 'No groups created yet'; @override - String get no_licenses_found => 'No licenses found'; + String get no_license_text_available => 'No license text available'; @override - String get no_license_text_available => 'No license text available'; + String get no_licenses_found => 'No licenses found'; @override String get no_matches_created_yet => 'No matches created yet'; @@ -305,10 +320,10 @@ class AppLocalizationsEn extends AppLocalizations { String get not_available => 'Not available'; @override - String get placement => 'Placement'; + String get place => 'place'; @override - String get place => 'place'; + String get placement => 'Placement'; @override String get played_matches => 'Played Matches'; @@ -372,15 +387,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get search_for_players => 'Search for players'; + @override + String get select_loser => 'Select Loser'; + @override String get select_winner => 'Select Winner'; @override String get select_winners => 'Select Winners'; - @override - String get select_loser => 'Select Loser'; - @override String get selected_players => 'Selected players'; @@ -393,18 +408,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get single_winner => 'Single Winner'; - @override - String get highest_score => 'Highest Score'; - - @override - String get loser => 'Loser'; - - @override - String get lowest_score => 'Lowest Score'; - - @override - String get multiple_winners => 'Multiple Winners'; - @override String get statistics => 'Statistics'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index afee2fc..8799109 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -10,8 +10,7 @@ import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart'; @@ -50,7 +49,6 @@ class _CreateTeamsViewState extends State { // Init the controllers nameController = teams.map(getNewController).toList(); - redistributePlayers(); } @override @@ -71,36 +69,7 @@ class _CreateTeamsViewState extends State { return TeamCreationTile( color: teams[index].color, controller: nameController[index], - players: teams[index].members, hintText: '${loc.team} ${index + 1}', - onEdit: () async { - final newPlayers = await Navigator.push( - context, - adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => EditMembersView( - matchPlayer: widget.match.players, - teamMember: teams[index].members, - ), - ), - ); - - setState(() { - // Remove the selected players from every team - for (final player in newPlayers) { - for (final team in teams) { - if (team.members.contains(player)) { - team.members.remove(player); - } - } - } - - // Add the selected players to the current team - teams[index] = teams[index].copyWith( - members: newPlayers, - ); - }); - }, onDelete: teams.length <= 2 ? null : () => _removeTeam(index), @@ -118,19 +87,10 @@ class _CreateTeamsViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - // Redistribute - MainMenuButton( - icon: Icons.cached, - text: loc.redistribute, - onPressed: () => setState(() { - redistributePlayers(); - }), - ), - const SizedBox(width: 15), - // Add new team MainMenuButton( icon: Icons.add, + text: loc.add_team, onPressed: teams.length >= widget.match.players.length ? null : addTeam, @@ -139,21 +99,19 @@ class _CreateTeamsViewState extends State { // Confirm teams and start match MainMenuButton( - icon: Icons.check, - onPressed: teams.every((team) => team.members.isNotEmpty) + icon: Icons.arrow_forward_sharp, + onPressed: teams.length >= 2 ? () async { final match = await createMatchWithTeams(); if (context.mounted) { - Navigator.pushAndRemoveUntil( + Navigator.push( context, adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => MatchResultView( + builder: (context) => ManageMembersView( match: match, onWinnerChanged: widget.onWinnerChanged, ), ), - (route) => route.isFirst, ); } } @@ -174,7 +132,6 @@ class _CreateTeamsViewState extends State { final newTeam = getNewTeam(); teams.add(newTeam); nameController.add(getNewController(newTeam)); - redistributePlayers(); }); } @@ -245,25 +202,6 @@ class _CreateTeamsViewState extends State { } } - // Iterates through all teams and redistributes players randomly and - // as evenly as possible. - void redistributePlayers() { - for (final team in teams) { - team.members.clear(); - } - - if (matchPlayers.isEmpty || teams.isEmpty) { - return; - } - - final shuffledPlayers = [...matchPlayers]..shuffle(random); - - for (int i = 0; i < shuffledPlayers.length; i++) { - final teamIndex = i % teams.length; - teams[teamIndex].members.add(shuffledPlayers[i]); - } - } - /// Saves the teams to the database and returns the updated match with the teams. Future createMatchWithTeams() async { final db = Provider.of(context, listen: false); diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart deleted file mode 100644 index 78eecef..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/edit_members_view.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/data/models/player.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; -import 'package:tallee/presentation/widgets/player_selection.dart'; - -class EditMembersView extends StatefulWidget { - const EditMembersView({ - super.key, - required this.matchPlayer, - required this.teamMember, - }); - - final List matchPlayer; - - final List teamMember; - - @override - State createState() => _EditMembersViewState(); -} - -class _EditMembersViewState extends State { - List selectedPlayers = []; - - @override - void initState() { - selectedPlayers = [...widget.teamMember]; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - - return Scaffold( - appBar: AppBar( - title: Text(loc.edit_members), - leading: HapticIconButton( - onPressed: selectedPlayers.isNotEmpty - ? () => Navigator.pop(context, selectedPlayers) - : null, - icon: const Icon(Icons.arrow_back_ios_new_outlined), - ), - ), - body: PlayerSelection( - initialSelectedPlayers: widget.teamMember, - availablePlayers: widget.matchPlayer, - onChanged: (List newSelectedPlayers) { - setState(() { - selectedPlayers = newSelectedPlayers; - }); - }, - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart new file mode 100644 index 0000000..befd38e --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -0,0 +1,286 @@ +import 'dart:core' hide Match; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; +import 'package:provider/provider.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/match.dart'; +import 'package:tallee/data/models/team.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart'; + +/// Displays the given [teams] as a flat reorderable list where every team is +/// preceded by a header row and followed by its members. Members can be +/// dragged across team boundaries to be reassigned to another team. +class ManageMembersView extends StatefulWidget { + const ManageMembersView({ + super.key, + required this.match, + required this.onWinnerChanged, + }); + + final Match match; + + final VoidCallback? onWinnerChanged; + + @override + State createState() => _ManageMembersViewState(); +} + +class _ManageMembersViewState extends State { + late AppDatabase db; + + late List teams; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + teams = widget.match.teams!; + redistributePlayers(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(loc.manage_members)), + body: SafeArea( + child: Stack( + children: [ + Expanded( + child: ReorderableListView.builder( + padding: const EdgeInsets.symmetric(vertical: 12), + buildDefaultDragHandles: false, + itemCount: allItemsCount, + onReorder: onReorder, + proxyDecorator: (child, index, animation) => + Material(type: MaterialType.transparency, child: child), + itemBuilder: (context, index) { + final teamIndex = teamIndexForFlat(index); + final memberIndex = memberIndexForFlat(index, teamIndex); + final team = teams[teamIndex]; + + if (memberIndex == -1) { + return buildTeamTile(team: team); + } + + final player = team.members[memberIndex]; + return ReorderableDelayedDragStartListener( + key: ValueKey('player_${player.id}'), + index: index, + child: TextIconListTile( + text: player.name, + suffixText: getNameCountText(player), + icon: Icons.drag_handle, + ), + ); + }, + ), + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom, + left: 0, + right: 0, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MainMenuButton( + onPressed: () => setState(() { + redistributePlayers(); + }), + icon: Icons.cached, + ), + const SizedBox(width: 16), + MainMenuButton( + onPressed: allTeamsHaveMembers + ? () async => submitMatch() + : null, + text: loc.create_match, + icon: RpgAwesome.clovers_card, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + void submitMatch() async { + await db.matchDao.addMatch(match: widget.match); + if (mounted) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (_) => MatchResultView( + match: widget.match, + onWinnerChanged: widget.onWinnerChanged, + ), + ), + (route) => route.isFirst, + ); + } + } + + bool get allTeamsHaveMembers { + return teams.every((team) => team.members.isNotEmpty); + } + + // Iterates through all teams and redistributes players randomly and + // as evenly as possible. + void redistributePlayers() { + for (final team in teams) { + team.members.clear(); + } + var matchPlayers = widget.match.players; + Random random = Random(); + + if (matchPlayers.isEmpty || teams.isEmpty) { + return; + } + + final shuffledPlayers = [...matchPlayers]..shuffle(random); + + for (int i = 0; i < shuffledPlayers.length; i++) { + final teamIndex = i % teams.length; + teams[teamIndex].members.add(shuffledPlayers[i]); + } + } + + /// Total players + teams length + int get allItemsCount { + var count = 0; + for (final team in teams) { + count += 1 + team.members.length; + } + return count; + } + + /// Returns the index of the team that owns the flat-list item at [flatIndex]. + int teamIndexForFlat(int flatIndex) { + var remaining = flatIndex; + for (var i = 0; i < teams.length; i++) { + final size = 1 + teams[i].members.length; + if (remaining < size) return i; + remaining -= size; + } + return teams.length - 1; + } + + /// Returns the member index within its team, or `-1` if the item at + /// [flatIndex] is the team header. + int memberIndexForFlat(int flatIndex, int teamIndex) { + var offset = 0; + for (var i = 0; i < teamIndex; i++) { + offset += 1 + teams[i].members.length; + } + // offset now points to the header of [teamIndex]. Anything beyond is a + // member of that team. + final localIndex = flatIndex - offset; + return localIndex == 0 ? -1 : localIndex - 1; + } + + void onReorder(int oldIndex, int newIndex) { + final sourceTeamIndex = teamIndexForFlat(oldIndex); + final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex); + + // Headers themselves can't be reordered. + if (sourceMemberIndex == -1) return; + + // Flutter convention: when moving down, the target index is shifted by 1 + // because the item is removed first. + var targetIndex = newIndex; + if (newIndex > oldIndex) targetIndex -= 1; + targetIndex = targetIndex.clamp(0, allItemsCount - 1); + + // Resolve target location based on the item currently at [targetIndex] + // *before* the move. + int destTeamIndex; + int insertPositionInTeam; + + if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) { + // Dropped at the very end -> append to the last team. + destTeamIndex = teams.length - 1; + insertPositionInTeam = teams[destTeamIndex].members.length; + } else { + destTeamIndex = teamIndexForFlat(targetIndex); + final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex); + + if (anchorMemberIndex == -1) { + // Dropped right before a header -> append to the previous team. + destTeamIndex = destTeamIndex - 1; + if (destTeamIndex < 0) { + // Dropped above the very first header -> stay in team 0 at top. + destTeamIndex = 0; + insertPositionInTeam = 0; + } else { + insertPositionInTeam = teams[destTeamIndex].members.length; + } + } else { + insertPositionInTeam = anchorMemberIndex; + } + } + + setState(() { + final sourceMembers = teams[sourceTeamIndex].members; + final player = sourceMembers.removeAt(sourceMemberIndex); + + // Adjust insert index if we removed from before the insert point in the + // same team. + if (sourceTeamIndex == destTeamIndex && + insertPositionInTeam > sourceMembers.length) { + insertPositionInTeam = sourceMembers.length; + } + + teams[destTeamIndex].members.insert(insertPositionInTeam, player); + }); + } + + Widget buildTeamTile({required Team team}) { + final color = getColorFromGameColor(team.color); + return Padding( + key: ValueKey('header_${team.id}'), + padding: const EdgeInsets.fromLTRB(12, 16, 12, 8), + child: Row( + children: [ + Container( + width: 14, + height: 14, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + team.name, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 17, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + '${team.members.length}', + style: const TextStyle( + color: CustomTheme.hintColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/team_creation_tile.dart b/lib/presentation/widgets/tiles/team_creation_tile.dart index 8a10150..2439597 100644 --- a/lib/presentation/widgets/tiles/team_creation_tile.dart +++ b/lib/presentation/widgets/tiles/team_creation_tile.dart @@ -3,34 +3,26 @@ import 'package:tallee/core/common.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; -import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; -import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class TeamCreationTile extends StatefulWidget { const TeamCreationTile({ super.key, required this.color, required this.controller, - required this.players, required this.hintText, - this.onEdit, this.onDelete, this.onColorSelection, }); final GameColor color; - final List players; - final TextEditingController controller; final String hintText; - final VoidCallback? onEdit; - final VoidCallback? onDelete; final ValueChanged? onColorSelection; @@ -112,45 +104,6 @@ class _TeamCreationTileState extends State { }).toList(), ), const SizedBox(height: 12), - Text( - loc.players, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor, - ), - ), - const SizedBox(height: 8), - if (widget.players.isEmpty) - Text( - loc.no_players_selected, - style: const TextStyle(color: CustomTheme.hintColor), - ) - else - Wrap( - spacing: 8, - runSpacing: 8, - children: widget.players - .map( - (player) => TextIconTile( - text: player.name, - suffixText: getNameCountText(player), - iconEnabled: false, - ), - ) - .toList(), - ), - if (widget.onEdit != null) - Padding( - padding: const EdgeInsets.only(top: 12), - child: AnimatedDialogButton( - buttonConstraints: const BoxConstraints( - minWidth: double.infinity, - ), - buttonText: loc.edit_members, - onPressed: widget.onEdit!, - ), - ), ], ), ); -- 2.49.1 From e761fb14741c924973ca721d0cf47ffefc4f3c31 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:51:15 +0200 Subject: [PATCH 27/51] Inserted custom tiles, comments --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 1 + .../create_teams/manage_members_view.dart | 231 ++++++++++-------- 3 files changed, 127 insertions(+), 106 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index f97b827..de579ff 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -86,6 +86,7 @@ "match_name": "Spieltitel", "match_profile": "Spielprofil", "matches": "Spiele", + "member": "Mitglied", "members": "Mitglieder", "most_points": "Höchste Punkte", "multiple_winners": "Mehrere Gewinner:innen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 79883fa..b77797c 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -86,6 +86,7 @@ "match_name": "Match name", "match_profile": "Match Profile", "matches": "Matches", + "member": "Member", "members": "Members", "most_points": "Most Points", "multiple_winners": "Multiple Winners", diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart index befd38e..49ba9fd 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -2,6 +2,7 @@ import 'dart:core' hide Match; import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:flutter_numeric_text/flutter_numeric_text.dart'; import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/common.dart'; @@ -117,24 +118,58 @@ class _ManageMembersViewState extends State { ); } - void submitMatch() async { - await db.matchDao.addMatch(match: widget.match); - if (mounted) { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (_) => MatchResultView( - match: widget.match, - onWinnerChanged: widget.onWinnerChanged, - ), - ), - (route) => route.isFirst, - ); - } - } + Widget buildTeamTile({required Team team}) { + final color = getColorFromGameColor(team.color); + final loc = AppLocalizations.of(context); + final length = team.members.length; + final memberText = length == 1 ? loc.member : loc.members; - bool get allTeamsHaveMembers { - return teams.every((team) => team.members.isNotEmpty); + return Padding( + key: ValueKey(team.id), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + child: Row( + children: [ + // Color circle + Container( + width: 14, + height: 14, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + + const SizedBox(width: 10), + + // Team name + Expanded( + child: Text( + team.name, + style: const TextStyle( + color: CustomTheme.textColor, + fontSize: 17, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + + // Member length + SizedBox( + width: 150, + child: NumericText( + '$length $memberText', + duration: const Duration(milliseconds: 200), + maxLines: 1, + textAlign: TextAlign.end, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: CustomTheme.hintColor, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); } // Iterates through all teams and redistributes players randomly and @@ -158,6 +193,63 @@ class _ManageMembersViewState extends State { } } + /// Handles moving a member from one team to another + void onReorder(int oldIndex, int newIndex) { + final sourceTeamIndex = teamIndexForFlat(oldIndex); + final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex); + + // Headers themselves can't be reordered. + if (sourceMemberIndex == -1) return; + + // When moving down, the target index is shifted by 1 + // because the item is removed first. + var targetIndex = newIndex; + if (newIndex > oldIndex) targetIndex -= 1; + targetIndex = targetIndex.clamp(0, allItemsCount - 1); + + // Resolve target location based on the item currently at targetIndex + // before the move. + int destTeamIndex; + int insertPositionInTeam; + + if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) { + // Dropped at the very end, append to the last team. + destTeamIndex = teams.length - 1; + insertPositionInTeam = teams[destTeamIndex].members.length; + } else { + destTeamIndex = teamIndexForFlat(targetIndex); + final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex); + + if (anchorMemberIndex == -1) { + // Dropped right before a header, append to the previous team. + destTeamIndex = destTeamIndex - 1; + if (destTeamIndex < 0) { + // Dropped above the very first header, stay in team 0 at top. + destTeamIndex = 0; + insertPositionInTeam = 0; + } else { + insertPositionInTeam = teams[destTeamIndex].members.length; + } + } else { + insertPositionInTeam = anchorMemberIndex; + } + } + + setState(() { + final sourceMembers = teams[sourceTeamIndex].members; + final player = sourceMembers.removeAt(sourceMemberIndex); + + // Adjust insert index if removed from before the insert point in the + // same team. + if (sourceTeamIndex == destTeamIndex && + insertPositionInTeam > sourceMembers.length) { + insertPositionInTeam = sourceMembers.length; + } + + teams[destTeamIndex].members.insert(insertPositionInTeam, player); + }); + } + /// Total players + teams length int get allItemsCount { var count = 0; @@ -191,96 +283,23 @@ class _ManageMembersViewState extends State { return localIndex == 0 ? -1 : localIndex - 1; } - void onReorder(int oldIndex, int newIndex) { - final sourceTeamIndex = teamIndexForFlat(oldIndex); - final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex); + bool get allTeamsHaveMembers => + teams.every((team) => team.members.isNotEmpty); - // Headers themselves can't be reordered. - if (sourceMemberIndex == -1) return; - - // Flutter convention: when moving down, the target index is shifted by 1 - // because the item is removed first. - var targetIndex = newIndex; - if (newIndex > oldIndex) targetIndex -= 1; - targetIndex = targetIndex.clamp(0, allItemsCount - 1); - - // Resolve target location based on the item currently at [targetIndex] - // *before* the move. - int destTeamIndex; - int insertPositionInTeam; - - if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) { - // Dropped at the very end -> append to the last team. - destTeamIndex = teams.length - 1; - insertPositionInTeam = teams[destTeamIndex].members.length; - } else { - destTeamIndex = teamIndexForFlat(targetIndex); - final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex); - - if (anchorMemberIndex == -1) { - // Dropped right before a header -> append to the previous team. - destTeamIndex = destTeamIndex - 1; - if (destTeamIndex < 0) { - // Dropped above the very first header -> stay in team 0 at top. - destTeamIndex = 0; - insertPositionInTeam = 0; - } else { - insertPositionInTeam = teams[destTeamIndex].members.length; - } - } else { - insertPositionInTeam = anchorMemberIndex; - } + void submitMatch() async { + final match = widget.match.copyWith(teams: teams); + await db.matchDao.addMatch(match: match); + if (mounted) { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (_) => MatchResultView( + match: match, + onWinnerChanged: widget.onWinnerChanged, + ), + ), + (route) => route.isFirst, + ); } - - setState(() { - final sourceMembers = teams[sourceTeamIndex].members; - final player = sourceMembers.removeAt(sourceMemberIndex); - - // Adjust insert index if we removed from before the insert point in the - // same team. - if (sourceTeamIndex == destTeamIndex && - insertPositionInTeam > sourceMembers.length) { - insertPositionInTeam = sourceMembers.length; - } - - teams[destTeamIndex].members.insert(insertPositionInTeam, player); - }); - } - - Widget buildTeamTile({required Team team}) { - final color = getColorFromGameColor(team.color); - return Padding( - key: ValueKey('header_${team.id}'), - padding: const EdgeInsets.fromLTRB(12, 16, 12, 8), - child: Row( - children: [ - Container( - width: 14, - height: 14, - decoration: BoxDecoration(color: color, shape: BoxShape.circle), - ), - const SizedBox(width: 10), - Expanded( - child: Text( - team.name, - style: const TextStyle( - color: CustomTheme.textColor, - fontSize: 17, - fontWeight: FontWeight.bold, - ), - overflow: TextOverflow.ellipsis, - ), - ), - Text( - '${team.members.length}', - style: const TextStyle( - color: CustomTheme.hintColor, - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ); } } -- 2.49.1 From 042f44e8efc6ada3f96de91e31f5b6375951066c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:51:31 +0200 Subject: [PATCH 28/51] adjusted team creation --- lib/l10n/generated/app_localizations.dart | 6 ++++ lib/l10n/generated/app_localizations_de.dart | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../create_teams/create_teams_view.dart | 34 ++++++------------- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 2f7970d..defaa4b 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -572,6 +572,12 @@ abstract class AppLocalizations { /// **'Matches'** String get matches; + /// No description provided for @member. + /// + /// In en, this message translates to: + /// **'Member'** + String get member; + /// No description provided for @members. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index ce42807..5f2f606 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -261,6 +261,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get matches => 'Spiele'; + @override + String get member => 'Mitglied'; + @override String get members => 'Mitglieder'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index ea3234b..c0f9fcc 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -261,6 +261,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get matches => 'Matches'; + @override + String get member => 'Member'; + @override String get members => 'Members'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index 8799109..d960e76 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -1,11 +1,9 @@ import 'dart:math'; import 'package:flutter/material.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/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/team.dart'; @@ -97,23 +95,21 @@ class _CreateTeamsViewState extends State { ), const SizedBox(width: 15), - // Confirm teams and start match + // Confirm teams and continue with member assignment MainMenuButton( icon: Icons.arrow_forward_sharp, onPressed: teams.length >= 2 - ? () async { - final match = await createMatchWithTeams(); - if (context.mounted) { - Navigator.push( - context, - adaptivePageRoute( - builder: (context) => ManageMembersView( - match: match, - onWinnerChanged: widget.onWinnerChanged, - ), + ? () { + final match = widget.match.copyWith(teams: teams); + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => ManageMembersView( + match: match, + onWinnerChanged: widget.onWinnerChanged, ), - ); - } + ), + ); } : null, ), @@ -202,14 +198,6 @@ class _CreateTeamsViewState extends State { } } - /// Saves the teams to the database and returns the updated match with the teams. - Future createMatchWithTeams() async { - final db = Provider.of(context, listen: false); - final match = widget.match.copyWith(teams: teams); - await db.matchDao.addMatch(match: match); - return match; - } - @override void dispose() { for (final c in nameController) { -- 2.49.1 From 32d6eb1d18a1221be4537f816710ad53ac6c55b3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:58:48 +0200 Subject: [PATCH 29/51] feat: added color reset on delete --- .../create_teams/create_teams_view.dart | 38 +++++-------------- 1 file changed, 10 insertions(+), 28 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index d960e76..ed5fcb2 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -153,51 +153,33 @@ class _CreateTeamsViewState extends State { return textController; } - /// Removes the team with the given index and redistributes its players to the - /// remaining teams. If there are less than 2 teams the removed team gets - /// replaced with a new one + /// Removes the team with the given index. If there are less than 2 teams the + /// removed team gets replaced with a new one void _removeTeam(int index) { final loc = AppLocalizations.of(context); setState(() { - final removedTeam = teams.removeAt(index); + teams.removeAt(index); final removedController = nameController.removeAt(index); removedController.dispose(); - if (teams.length < 2) { - final fallbackTeam = getNewTeam(); - teams.add(fallbackTeam); - nameController.add(getNewController(fallbackTeam)); - } - // Update index-based team names + // Update index-based team names and default colors for (int i = 0; i < nameController.length; i++) { if (nameController[i].text.contains( RegExp('^${RegExp.escape(loc.team)} \\d+\$'), )) { nameController[i].text = '${loc.team} ${i + 1}'; + + // Reset color to default if it was based on the index + final previousIndex = i < index ? i : i + 1; + if (teams[i].color == getTeamColor(previousIndex)) { + teams[i] = teams[i].copyWith(color: getTeamColor(i)); + } } } - - addToSmallestTeams(removedTeam.members); }); } - /// Adds the given players to the teams with the least amount of members - /// [orphanedPlayers] The players to be added to the teams. - void addToSmallestTeams(List orphanedPlayers) { - if (teams.isEmpty || orphanedPlayers.isEmpty) return; - - for (final player in orphanedPlayers) { - var targetIndex = 0; - for (var i = 1; i < teams.length; i++) { - if (teams[i].members.length < teams[targetIndex].members.length) { - targetIndex = i; - } - } - teams[targetIndex].members.add(player); - } - } - @override void dispose() { for (final c in nameController) { -- 2.49.1 From 60d746ede2ea3e6b1a1a87308470a57f6afdc68b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:58:56 +0200 Subject: [PATCH 30/51] fix: removed local var --- .../create_match/create_teams/manage_members_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart index 49ba9fd..1192b04 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -36,13 +36,12 @@ class ManageMembersView extends StatefulWidget { class _ManageMembersViewState extends State { late AppDatabase db; - late List teams; + List get teams => widget.match.teams!; @override void initState() { super.initState(); db = Provider.of(context, listen: false); - teams = widget.match.teams!; redistributePlayers(); } @@ -287,7 +286,8 @@ class _ManageMembersViewState extends State { teams.every((team) => team.members.isNotEmpty); void submitMatch() async { - final match = widget.match.copyWith(teams: teams); + final match = widget.match; + print('teams: ${match.teams}'); await db.matchDao.addMatch(match: match); if (mounted) { Navigator.pushAndRemoveUntil( -- 2.49.1 From f3f7d449948244e0ade636702f37934cb36b3f47 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 17:59:26 +0200 Subject: [PATCH 31/51] refactoring --- .../create_teams/create_teams_view.dart | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index ed5fcb2..a8f3899 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -70,7 +70,7 @@ class _CreateTeamsViewState extends State { hintText: '${loc.team} ${index + 1}', onDelete: teams.length <= 2 ? null - : () => _removeTeam(index), + : () => removeTeam(index), onColorSelection: (color) { setState(() { teams[index] = teams[index].copyWith(color: color); @@ -121,16 +121,6 @@ class _CreateTeamsViewState extends State { ); } - /// Adds a new team to the list of teams, creates a corresponding controller, - /// and redistributes the players among all teams. - void addTeam() { - setState(() { - final newTeam = getNewTeam(); - teams.add(newTeam); - nameController.add(getNewController(newTeam)); - }); - } - /// Creates a new team with a default name and color based on the current number Team getNewTeam() { final loc = AppLocalizations.of(context); @@ -153,9 +143,19 @@ class _CreateTeamsViewState extends State { return textController; } + /// Adds a new team to the list of teams, creates a corresponding controller, + /// and redistributes the players among all teams. + void addTeam() { + setState(() { + final newTeam = getNewTeam(); + teams.add(newTeam); + nameController.add(getNewController(newTeam)); + }); + } + /// Removes the team with the given index. If there are less than 2 teams the /// removed team gets replaced with a new one - void _removeTeam(int index) { + void removeTeam(int index) { final loc = AppLocalizations.of(context); setState(() { -- 2.49.1 From 32a8a6090a25b07368ab3c324b16369154971260 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 18:16:40 +0200 Subject: [PATCH 32/51] Reworked team creation tile --- .../widgets/tiles/team_creation_tile.dart | 153 +++++++++++------- 1 file changed, 93 insertions(+), 60 deletions(-) diff --git a/lib/presentation/widgets/tiles/team_creation_tile.dart b/lib/presentation/widgets/tiles/team_creation_tile.dart index 2439597..06b349f 100644 --- a/lib/presentation/widgets/tiles/team_creation_tile.dart +++ b/lib/presentation/widgets/tiles/team_creation_tile.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:fluttericon/font_awesome_icons.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; class TeamCreationTile extends StatefulWidget { @@ -36,75 +37,107 @@ class _TeamCreationTileState extends State { GameColor.values.length, (index) => getTeamColor(index), ); + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return Container( margin: CustomTheme.standardMargin, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: CustomTheme.standardBoxDecoration, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: TextInputField( - controller: widget.controller, - hintText: widget.hintText, - maxLength: Constants.MAX_TEAM_NAME_LENGTH, + clipBehavior: Clip.antiAlias, + child: IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 6, ), - ), - const SizedBox(width: 12), - AnimatedDialogButton( - content: const Icon(Icons.delete), - isDescructive: true, - onPressed: widget.onDelete, - buttonText: '', - ), - ], - ), - const SizedBox(height: 8), - Text( - loc.color, - style: const TextStyle( - fontSize: 15, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor, - ), - ), - const SizedBox(height: 8), - Wrap( - spacing: 8, - runSpacing: 8, - children: teamColors.map((color) { - final isSelected = widget.color == color; - return GestureDetector( - onTap: () { - widget.onColorSelection?.call(color); - }, - child: Container( - width: 34, - height: 34, - decoration: BoxDecoration( - color: getColorFromGameColor(color), - shape: BoxShape.circle, - border: Border.all( - color: isSelected ? Colors.white : Colors.transparent, - width: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name input + delete icon + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: TextInputField( + controller: widget.controller, + hintText: widget.hintText, + maxLength: Constants.MAX_TEAM_NAME_LENGTH, + ), + ), + HapticIconButton( + icon: const Icon(FontAwesome.trash), + color: CustomTheme.textColor, + iconSize: 25, + onPressed: widget.onDelete, + ), + ], ), - ), - child: isSelected - ? const Icon(Icons.check, size: 18, color: Colors.white) - : null, + const SizedBox(height: 12), + + // Color label + Padding( + padding: const EdgeInsets.only(left: 8), + child: Text( + loc.color, + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + ), + ), + const SizedBox(height: 8), + + // Color picker + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: teamColors.map((color) { + final isSelected = widget.color == color; + return GestureDetector( + onTap: () { + widget.onColorSelection?.call(color); + }, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: getColorFromGameColor(color), + shape: BoxShape.circle, + border: Border.all( + color: isSelected + ? Colors.white + : Colors.transparent, + width: 3, + ), + ), + child: isSelected + ? const Icon( + Icons.check, + size: 18, + color: Colors.white, + ) + : null, + ), + ); + }).toList(), + ), + ), + ], ), - ); - }).toList(), - ), - const SizedBox(height: 12), - ], + ), + ), + ], + ), ), ); } -- 2.49.1 From 021a5464798a959dc3ed30c442855c0c068b3b84 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 18:28:11 +0200 Subject: [PATCH 33/51] Updated stacks & buttons --- .../group_view/create_group_view.dart | 31 ++--- .../create_match/create_match_view.dart | 26 +++-- .../create_teams/create_teams_view.dart | 56 ++++----- .../create_teams/manage_members_view.dart | 107 +++++++++--------- 4 files changed, 113 insertions(+), 107 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 84efbe1..96092ca 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -8,7 +8,7 @@ import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; @@ -96,19 +96,24 @@ class _CreateGroupViewState extends State { }, ), ), - CustomWidthButton( - text: widget.groupToEdit == null - ? loc.create_group - : loc.edit_group, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.primary, - onPressed: - (_groupNameController.text.isEmpty || - (selectedPlayers.length < 2)) - ? null - : _saveGroup, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: AnimatedDialogButton( + buttonConstraints: const BoxConstraints( + minWidth: double.infinity, + minHeight: 50, + ), + buttonText: widget.groupToEdit == null + ? loc.create_group + : loc.edit_group, + buttonType: ButtonType.primary, + onPressed: + (_groupNameController.text.isEmpty || + (selectedPlayers.length < 2)) + ? null + : _saveGroup, + ), ), - const SizedBox(height: 20), ], ), ), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index fd14508..7fd9fa0 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -14,7 +14,7 @@ import 'package:tallee/presentation/views/main_menu/match_view/create_match/choo import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/text_input/text_input_field.dart'; import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; @@ -175,15 +175,21 @@ class _CreateMatchViewState extends State { ), // Create or save button. - CustomWidthButton( - text: buttonText, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.primary, - onPressed: isSubmitButtonEnabled() - ? () { - submitButtonNavigation(context); - } - : null, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: AnimatedDialogButton( + buttonConstraints: const BoxConstraints( + minWidth: double.infinity, + minHeight: 50, + ), + buttonType: ButtonType.primary, + onPressed: isSubmitButtonEnabled() + ? () { + submitButtonNavigation(context); + } + : null, + buttonText: buttonText, + ), ), ], ), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index a8f3899..31ed6d6 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -56,33 +56,33 @@ class _CreateTeamsViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_teams)), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.only(top: 12, bottom: 12), - itemCount: teams.length, - itemBuilder: (context, index) { - return TeamCreationTile( - color: teams[index].color, - controller: nameController[index], - hintText: '${loc.team} ${index + 1}', - onDelete: teams.length <= 2 - ? null - : () => removeTeam(index), - onColorSelection: (color) { - setState(() { - teams[index] = teams[index].copyWith(color: color); - }); - }, - ); - }, - ), + body: Stack( + alignment: Alignment.center, + children: [ + Expanded( + child: ListView.builder( + padding: const EdgeInsets.only(top: 12, bottom: 12), + itemCount: teams.length, + itemBuilder: (context, index) { + return TeamCreationTile( + color: teams[index].color, + controller: nameController[index], + hintText: '${loc.team} ${index + 1}', + onDelete: teams.length <= 2 ? null : () => removeTeam(index), + onColorSelection: (color) { + setState(() { + teams[index] = teams[index].copyWith(color: color); + }); + }, + ); + }, ), + ), - // Button row - Row( + // Button row + Positioned( + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // Add new team @@ -95,7 +95,7 @@ class _CreateTeamsViewState extends State { ), const SizedBox(width: 15), - // Confirm teams and continue with member assignment + // Confirm teams MainMenuButton( icon: Icons.arrow_forward_sharp, onPressed: teams.length >= 2 @@ -115,8 +115,8 @@ class _CreateTeamsViewState extends State { ), ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart index 1192b04..a668f33 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -52,67 +52,62 @@ class _ManageMembersViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.manage_members)), - body: SafeArea( - child: Stack( - children: [ - Expanded( - child: ReorderableListView.builder( - padding: const EdgeInsets.symmetric(vertical: 12), - buildDefaultDragHandles: false, - itemCount: allItemsCount, - onReorder: onReorder, - proxyDecorator: (child, index, animation) => - Material(type: MaterialType.transparency, child: child), - itemBuilder: (context, index) { - final teamIndex = teamIndexForFlat(index); - final memberIndex = memberIndexForFlat(index, teamIndex); - final team = teams[teamIndex]; + body: Stack( + alignment: AlignmentDirectional.center, + children: [ + Expanded( + child: ReorderableListView.builder( + padding: const EdgeInsets.symmetric(vertical: 12), + buildDefaultDragHandles: false, + itemCount: allItemsCount, + onReorder: onReorder, + proxyDecorator: (child, index, animation) => + Material(type: MaterialType.transparency, child: child), + itemBuilder: (context, index) { + final teamIndex = teamIndexForFlat(index); + final memberIndex = memberIndexForFlat(index, teamIndex); + final team = teams[teamIndex]; - if (memberIndex == -1) { - return buildTeamTile(team: team); - } + if (memberIndex == -1) { + return buildTeamTile(team: team); + } - final player = team.members[memberIndex]; - return ReorderableDelayedDragStartListener( - key: ValueKey('player_${player.id}'), - index: index, - child: TextIconListTile( - text: player.name, - suffixText: getNameCountText(player), - icon: Icons.drag_handle, - ), - ); - }, - ), + final player = team.members[memberIndex]; + return ReorderableDelayedDragStartListener( + key: ValueKey('player_${player.id}'), + index: index, + child: TextIconListTile( + text: player.name, + suffixText: getNameCountText(player), + icon: Icons.drag_handle, + ), + ); + }, ), - Positioned( - bottom: MediaQuery.of(context).padding.bottom, - left: 0, - right: 0, - child: Center( - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - MainMenuButton( - onPressed: () => setState(() { - redistributePlayers(); - }), - icon: Icons.cached, - ), - const SizedBox(width: 16), - MainMenuButton( - onPressed: allTeamsHaveMembers - ? () async => submitMatch() - : null, - text: loc.create_match, - icon: RpgAwesome.clovers_card, - ), - ], + ), + Positioned( + bottom: MediaQuery.of(context).padding.bottom + 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + MainMenuButton( + onPressed: () => setState(() { + redistributePlayers(); + }), + icon: Icons.cached, ), - ), + const SizedBox(width: 16), + MainMenuButton( + onPressed: allTeamsHaveMembers + ? () async => submitMatch() + : null, + text: loc.create_match, + icon: RpgAwesome.clovers_card, + ), + ], ), - ], - ), + ), + ], ), ); } -- 2.49.1 From df8e060707b8e0f667950874aaa3c9651e5e5c95 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 18:31:15 +0200 Subject: [PATCH 34/51] Renamed GameColor to AppColor --- lib/core/common.dart | 62 ++++++++++--------- lib/core/enums.dart | 4 +- lib/data/dao/game_dao.dart | 6 +- lib/data/dao/team_dao.dart | 6 +- lib/data/models/game.dart | 8 +-- lib/data/models/match.dart | 2 +- lib/data/models/team.dart | 8 +-- .../create_match/create_game_view.dart | 10 +-- .../main_menu/match_view/match_view.dart | 2 +- lib/presentation/widgets/game_label.dart | 2 +- lib/presentation/widgets/tiles/game_tile.dart | 2 +- .../widgets/tiles/team_creation_tile.dart | 6 +- lib/services/data_transfer_service.dart | 2 +- test/db_tests/aggregates/match_test.dart | 2 +- test/db_tests/aggregates/team_test.dart | 2 +- test/db_tests/entities/game_test.dart | 16 ++--- .../relationships/player_match_test.dart | 2 +- test/db_tests/values/score_entry_test.dart | 2 +- test/services/data_transfer_service_test.dart | 14 ++--- 19 files changed, 80 insertions(+), 78 deletions(-) diff --git a/lib/core/common.dart b/lib/core/common.dart index c581858..764094e 100644 --- a/lib/core/common.dart +++ b/lib/core/common.dart @@ -24,62 +24,62 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) { } } -// Returns a [GameColor] enum value based on the provided team [index]. -GameColor getTeamColor(int index) { +// Returns a AppColor enum value based on the provided team [index]. +AppColor getTeamColor(int index) { final colors = [ - GameColor.red, - GameColor.blue, - GameColor.green, - GameColor.yellow, - GameColor.purple, - GameColor.orange, - GameColor.pink, - GameColor.teal, + AppColor.red, + AppColor.blue, + AppColor.green, + AppColor.yellow, + AppColor.purple, + AppColor.orange, + AppColor.pink, + AppColor.teal, ]; return colors[index % colors.length]; } -/// Translates a [GameColor] enum value to its corresponding localized string. -String translateGameColorToString(GameColor color, BuildContext context) { +/// Translates a [AppColor] enum value to its corresponding localized string. +String translateGameColorToString(AppColor color, BuildContext context) { final loc = AppLocalizations.of(context); switch (color) { - case GameColor.red: + case AppColor.red: return loc.color_red; - case GameColor.blue: + case AppColor.blue: return loc.color_blue; - case GameColor.green: + case AppColor.green: return loc.color_green; - case GameColor.yellow: + case AppColor.yellow: return loc.color_yellow; - case GameColor.purple: + case AppColor.purple: return loc.color_purple; - case GameColor.orange: + case AppColor.orange: return loc.color_orange; - case GameColor.pink: + case AppColor.pink: return loc.color_pink; - case GameColor.teal: + case AppColor.teal: return loc.color_teal; } } -/// Returns the [Color] object corresponding to a [GameColor] enum value. -Color getColorFromGameColor(GameColor color) { +/// Returns the [Color] object corresponding to a [AppColor] enum value. +Color getColorFromGameColor(AppColor color) { switch (color) { - case GameColor.red: + case AppColor.red: return Colors.red; - case GameColor.blue: + case AppColor.blue: return Colors.blue; - case GameColor.green: + case AppColor.green: return Colors.green; - case GameColor.yellow: + case AppColor.yellow: return const Color(0xFFF7CA28); - case GameColor.purple: + case AppColor.purple: return Colors.purple; - case GameColor.orange: + case AppColor.orange: return const Color(0xFFef681f); - case GameColor.pink: + case AppColor.pink: return Colors.pink; - case GameColor.teal: + case AppColor.teal: return Colors.teal; } } @@ -127,6 +127,7 @@ String getExtraPlayerCount(Match match) { return ' + ${count.toString()}'; } +/// Returns the player name count if greater 0 in the format " #2", otherwise an empty string String getNameCountText(Player player) { if (player.nameCount >= 1) { return ' #${player.nameCount}'; @@ -134,6 +135,7 @@ String getNameCountText(Player player) { return ''; } +/// Returns the correct singular or plural form of "point(s)" based on the [points] value. String getPointLabel(AppLocalizations loc, int points) { if (points == 1) { return '$points ${loc.point}'; diff --git a/lib/core/enums.dart b/lib/core/enums.dart index 99141e4..f1ef571 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -42,5 +42,5 @@ enum Ruleset { singleLoser, } -/// Different colors for highlighting games -enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } +/// Different colors for highlighting content +enum AppColor { red, orange, yellow, green, teal, blue, purple, pink } diff --git a/lib/data/dao/game_dao.dart b/lib/data/dao/game_dao.dart index a4c2300..1adfc9e 100644 --- a/lib/data/dao/game_dao.dart +++ b/lib/data/dao/game_dao.dart @@ -92,7 +92,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { name: row.name, ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), description: row.description, - color: GameColor.values.firstWhere((e) => e.name == row.color), + color: AppColor.values.firstWhere((e) => e.name == row.color), icon: row.icon, createdAt: row.createdAt, ), @@ -109,7 +109,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { name: result.name, ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), description: result.description, - color: GameColor.values.firstWhere((e) => e.name == result.color), + color: AppColor.values.firstWhere((e) => e.name == result.color), icon: result.icon, createdAt: result.createdAt, ); @@ -156,7 +156,7 @@ class GameDao extends DatabaseAccessor with _$GameDaoMixin { /// Updates the color of the game with the given [gameId]. Future updateGameColor({ required String gameId, - required GameColor color, + required AppColor color, }) async { final rowsAffected = await (update(gameTable)..where((g) => g.id.equals(gameId))).write( diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 70ba8b6..5b1da89 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -116,7 +116,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { id: row.id, name: row.name, createdAt: row.createdAt, - color: GameColor.values.byName(row.color), + color: AppColor.values.byName(row.color), score: row.score, members: members, ); @@ -151,7 +151,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { id: result.id, name: result.name, createdAt: result.createdAt, - color: GameColor.values.byName(result.color), + color: AppColor.values.byName(result.color), score: result.score, members: members, ); @@ -193,7 +193,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { /// Updates the color of the team with the given [teamId]. Future updateTeamColor({ required String teamId, - required GameColor color, + required AppColor color, }) async { final rowsAffected = await (update(teamTable)..where((t) => t.id.equals(teamId))).write( diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index 89bbd30..ec69204 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -8,13 +8,13 @@ class Game { final String name; final Ruleset ruleset; final String description; - final GameColor color; + final AppColor color; final String icon; Game({ required this.name, required this.ruleset, - this.color = GameColor.orange, + this.color = AppColor.orange, this.description = '', this.icon = '', String? id, @@ -33,7 +33,7 @@ class Game { String? name, Ruleset? ruleset, String? description, - GameColor? color, + AppColor? color, String? icon, }) { return Game( @@ -73,7 +73,7 @@ class Game { orElse: () => Ruleset.singleWinner, ), description = json['description'], - color = GameColor.values.firstWhere((e) => e.name == json['color']), + color = AppColor.values.firstWhere((e) => e.name == json['color']), icon = json['icon']; Map toJson() => { diff --git a/lib/data/models/match.dart b/lib/data/models/match.dart index 4db294f..deedea5 100644 --- a/lib/data/models/match.dart +++ b/lib/data/models/match.dart @@ -113,7 +113,7 @@ class Match { name: '', ruleset: Ruleset.singleWinner, description: '', - color: GameColor.blue, + color: AppColor.blue, icon: '', ), group = null, diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index aa78df7..56576c8 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -8,7 +8,7 @@ class Team { final String id; final String name; final DateTime createdAt; - final GameColor color; + final AppColor color; final int? score; final List members; @@ -16,7 +16,7 @@ class Team { String? id, required this.name, DateTime? createdAt, - this.color = GameColor.blue, + this.color = AppColor.blue, this.score, required this.members, }) : id = id ?? const Uuid().v4(), @@ -31,7 +31,7 @@ class Team { String? id, String? name, DateTime? createdAt, - GameColor? color, + AppColor? color, int? score, List? members, }) { @@ -71,7 +71,7 @@ class Team { : id = json['id'], name = json['name'], createdAt = DateTime.parse(json['createdAt']), - color = GameColor.values.byName(json['color'] ?? GameColor.blue.name), + color = AppColor.values.byName(json['color'] ?? AppColor.blue.name), score = json['score'] ?? 0, members = []; // Populated during import via DataTransferService diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart index 998f4e1..21849bf 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_game_view.dart @@ -49,10 +49,10 @@ class _CreateGameViewState extends State { late final AppDatabase db; late List<(Ruleset, String)> _rulesets; - late List<(GameColor, String)> _colors; + late List<(AppColor, String)> _colors; Ruleset? selectedRuleset = Ruleset.singleWinner; - GameColor? selectedColor = GameColor.orange; + AppColor? selectedColor = AppColor.orange; /// Controller for the game name input field. final _gameNameController = TextEditingController(); @@ -87,10 +87,10 @@ class _CreateGameViewState extends State { ), ); _colors = List.generate( - GameColor.values.length, + AppColor.values.length, (index) => ( - GameColor.values[index], - translateGameColorToString(GameColor.values[index], context), + AppColor.values[index], + translateGameColorToString(AppColor.values[index], context), ), ); diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index a7f60c6..dc94365 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -39,7 +39,7 @@ class _MatchViewState extends State { game: Game( name: 'Game name', ruleset: Ruleset.singleWinner, - color: GameColor.blue, + color: AppColor.blue, icon: '', ), group: Group( diff --git a/lib/presentation/widgets/game_label.dart b/lib/presentation/widgets/game_label.dart index 553e637..3eae9b1 100644 --- a/lib/presentation/widgets/game_label.dart +++ b/lib/presentation/widgets/game_label.dart @@ -12,7 +12,7 @@ class GameLabel extends StatelessWidget { final String title; final String description; - final GameColor color; + final AppColor color; @override Widget build(BuildContext context) { diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index ee5acf0..11d96d8 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -51,7 +51,7 @@ class GameTile extends StatelessWidget { ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) : Colors.white; - final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); + final gameColor = badgeColor ?? getColorFromGameColor(AppColor.orange); return GestureDetector( onTap: () async { diff --git a/lib/presentation/widgets/tiles/team_creation_tile.dart b/lib/presentation/widgets/tiles/team_creation_tile.dart index 06b349f..c6d999b 100644 --- a/lib/presentation/widgets/tiles/team_creation_tile.dart +++ b/lib/presentation/widgets/tiles/team_creation_tile.dart @@ -18,7 +18,7 @@ class TeamCreationTile extends StatefulWidget { this.onColorSelection, }); - final GameColor color; + final AppColor color; final TextEditingController controller; @@ -26,7 +26,7 @@ class TeamCreationTile extends StatefulWidget { final VoidCallback? onDelete; - final ValueChanged? onColorSelection; + final ValueChanged? onColorSelection; @override State createState() => _TeamCreationTileState(); @@ -34,7 +34,7 @@ class TeamCreationTile extends StatefulWidget { class _TeamCreationTileState extends State { final teamColors = List.generate( - GameColor.values.length, + AppColor.values.length, (index) => getTeamColor(index), ); diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 29199f8..0f2f8fa 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -278,7 +278,7 @@ class DataTransferService { name: 'Unknown', ruleset: Ruleset.singleWinner, description: '', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); } diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 37c1cd0..916d5d4 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -56,7 +56,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index fefdcc5..381d22b 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -49,7 +49,7 @@ void main() { testGame = Game( name: 'Test Game', ruleset: Ruleset.highestScore, - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/entities/game_test.dart b/test/db_tests/entities/game_test.dart index 778d43b..f7e7dcd 100644 --- a/test/db_tests/entities/game_test.dart +++ b/test/db_tests/entities/game_test.dart @@ -28,7 +28,7 @@ void main() { name: 'Chess', ruleset: Ruleset.singleWinner, description: 'A classic strategy game', - color: GameColor.blue, + color: AppColor.blue, icon: 'chess_icon', ); testGame2 = Game( @@ -36,7 +36,7 @@ void main() { name: 'Poker', ruleset: Ruleset.multipleWinners, description: 'Card game with multiple winners', - color: GameColor.red, + color: AppColor.red, icon: 'poker_icon', ); testGame3 = Game( @@ -44,7 +44,7 @@ void main() { name: 'Monopoly', ruleset: Ruleset.highestScore, description: 'A board game about real estate', - color: GameColor.orange, + color: AppColor.orange, icon: '', ); }); @@ -124,7 +124,7 @@ void main() { name: 'Game\'s & "Special" ', ruleset: Ruleset.multipleWinners, description: 'Description with émojis 🎮🎲', - color: GameColor.purple, + color: AppColor.purple, icon: '', ); await database.gameDao.addGame(game: specialGame); @@ -280,19 +280,19 @@ void main() { await database.gameDao.updateGameColor( gameId: testGame1.id, - color: GameColor.green, + color: AppColor.green, ); final updatedGame = await database.gameDao.getGameById( gameId: testGame1.id, ); - expect(updatedGame.color, GameColor.green); + expect(updatedGame.color, AppColor.green); }); test('updateGameColor() does nothing for non-existent game', () async { final updated = await database.gameDao.updateGameColor( gameId: 'non-existent-id', - color: GameColor.green, + color: AppColor.green, ); expect(updated, isFalse); @@ -336,7 +336,7 @@ void main() { name: newName, ); - const newGameColor = GameColor.teal; + const newGameColor = AppColor.teal; await database.gameDao.updateGameColor( gameId: testGame1.id, color: newGameColor, diff --git a/test/db_tests/relationships/player_match_test.dart b/test/db_tests/relationships/player_match_test.dart index 6d879c3..fa7ec21 100644 --- a/test/db_tests/relationships/player_match_test.dart +++ b/test/db_tests/relationships/player_match_test.dart @@ -42,7 +42,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/db_tests/values/score_entry_test.dart b/test/db_tests/values/score_entry_test.dart index f6cc292..593d194 100644 --- a/test/db_tests/values/score_entry_test.dart +++ b/test/db_tests/values/score_entry_test.dart @@ -40,7 +40,7 @@ void main() { name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', - color: GameColor.blue, + color: AppColor.blue, icon: '', ); testMatch1 = Match( diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index e88f0ad..a7594c7 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -45,7 +45,7 @@ void main() { name: 'Chess', ruleset: Ruleset.singleWinner, description: 'Strategic board game', - color: GameColor.blue, + color: AppColor.blue, icon: 'chess_icon', ); @@ -445,19 +445,19 @@ void main() { Game( name: 'Red Game', ruleset: Ruleset.singleWinner, - color: GameColor.red, + color: AppColor.red, icon: 'icon', ), Game( name: 'Blue Game', ruleset: Ruleset.singleWinner, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), Game( name: 'Green Game', ruleset: Ruleset.singleWinner, - color: GameColor.green, + color: AppColor.green, icon: 'icon', ), ]; @@ -481,19 +481,19 @@ void main() { Game( name: 'Highest Score Game', ruleset: Ruleset.highestScore, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), Game( name: 'Lowest Score Game', ruleset: Ruleset.lowestScore, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), Game( name: 'Single Winner', ruleset: Ruleset.singleWinner, - color: GameColor.blue, + color: AppColor.blue, icon: 'icon', ), ]; -- 2.49.1 From a497ae872b17150361446aa813bf9f9ed90193cc Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 18:44:30 +0200 Subject: [PATCH 35/51] Updated match result view --- .../match_view/match_result_view.dart | 205 ++++++++++-------- 1 file changed, 110 insertions(+), 95 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 39cfef8..37386b4 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -11,7 +11,7 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; @@ -90,109 +90,124 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( + automaticallyImplyLeading: true, leading: HapticIconButton( - icon: const Icon(Icons.close), - onPressed: () { - widget.onWinnerChanged?.call(); - Navigator.pop(context); - }, + icon: isLiveEditMode + ? const Icon(Icons.arrow_back_ios) + : const Icon(Icons.close), + onPressed: isLiveEditMode + ? () => setState(() { + isLiveEditMode = false; + }) + : () => {widget.onWinnerChanged?.call(), Navigator.pop(context)}, ), title: Text(widget.match.name), ), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: isLiveEditMode - // Live Edit Mode - ? buildLiveEditWidet(isTeamMatch) - // Normal Container - : Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 10, - ), - decoration: BoxDecoration( - color: CustomTheme.boxColor, - border: Border.all(color: CustomTheme.boxBorderColor), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - getTitleForRuleset(loc), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - - // Show player selection - if (rulesetSupportsPlayerSelection()) - if (ruleset == Ruleset.multipleWinners) - // TODO: Implement view for teams - Expanded( - child: buildMultipleWinnerSelectionWidget( - isTeamMatch, - ), - ) - else - Expanded( - child: buildPlayerSelectionWidget(isTeamMatch), - ), - - // Show score entry - if (rulesetSupportsScoreEntry()) - Expanded(child: buildScoreEntryWidget(isTeamMatch)), - - // Show draggable placement list - if (rulesetSupportsDragBehaviour()) - Expanded(child: buildPlacementWidget(isTeamMatch)), - ], - ), + body: Column( + children: [ + Expanded( + child: isLiveEditMode + // Live Edit Mode + ? buildLiveEditWidet(isTeamMatch) + // Normal Container + : Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, ), - ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getTitleForRuleset(loc), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), - if (rulesetSupportsScoreEntry()) - // Button to switch to live edit mode - ...[ - CustomWidthButton( - text: isLiveEditMode ? loc.exit_view : loc.live_edit_mode, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.secondary, - onPressed: () => setState(() { - isLiveEditMode = !isLiveEditMode; - }), + // Show player selection + if (rulesetSupportsPlayerSelection()) + if (ruleset == Ruleset.multipleWinners) + Expanded( + child: buildMultipleWinnerSelectionWidget( + isTeamMatch, + ), + ) + else + Expanded( + child: buildPlayerSelectionWidget(isTeamMatch), + ), + + // Show score entry + if (rulesetSupportsScoreEntry()) + Expanded(child: buildScoreEntryWidget(isTeamMatch)), + + // Show draggable placement list + if (rulesetSupportsDragBehaviour()) + Expanded(child: buildPlacementWidget(isTeamMatch)), + ], + ), + ), + ), + + if (!isLiveEditMode) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (rulesetSupportsScoreEntry()) ...[ + // Button to switch to live edit mode + AnimatedDialogButton( + buttonConstraints: const BoxConstraints( + minWidth: double.infinity, + minHeight: 50, + ), + buttonText: loc.live_edit_mode, + buttonType: ButtonType.secondary, + onPressed: () => setState(() { + isLiveEditMode = !isLiveEditMode; + }), + ), + ], + + // Save Changes Button + AnimatedDialogButton( + buttonConstraints: const BoxConstraints( + minWidth: double.infinity, + minHeight: 50, + ), + buttonText: loc.save_changes, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.pop(context); + } + : null, + ), + ], ), - const SizedBox(height: 10), - ], - - // Save Changes Button - CustomWidthButton( - text: loc.save_changes, - sizeRelativeToWidth: 0.95, - onPressed: canSave - ? () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.pop(context); - } - : null, ), ], - ), + ], ), ); } -- 2.49.1 From a803dc36d7e2270badfa6f9f457432e6106e0457 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 19:54:46 +0200 Subject: [PATCH 36/51] fix: sorted games --- .../create_match/choose_game_view.dart | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 3c51cab..34f9be0 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -51,6 +51,9 @@ class _ChooseGameViewState extends State { /// Games filtered according to the current search query late List filteredGames; + List get games => + widget.games..sort((a, b) => a.name.compareTo(b.name)); + @override void initState() { db = Provider.of(context, listen: false); @@ -59,7 +62,7 @@ class _ChooseGameViewState extends State { selectedGameId = widget.initialGameId; // Start with all games visible - filteredGames = List.from(widget.games); + filteredGames = List.from(games); super.initState(); } @@ -77,9 +80,7 @@ class _ChooseGameViewState extends State { Navigator.of(context).pop( selectedGameId == '' ? null - : widget.games.firstWhere( - (game) => game.id == selectedGameId, - ), + : games.firstWhere((game) => game.id == selectedGameId), ); }, ), @@ -99,7 +100,7 @@ class _ChooseGameViewState extends State { ); if (result != null && result.game != null) { setState(() { - widget.games.insert(0, result.game); + games.insert(0, result.game); }); _refreshFromSource(); } @@ -139,7 +140,7 @@ class _ChooseGameViewState extends State { child: Visibility( visible: filteredGames.isNotEmpty, replacement: Visibility( - visible: widget.games.isNotEmpty, + visible: games.isNotEmpty, replacement: TopCenteredMessage( icon: Icons.info, title: loc.info, @@ -190,7 +191,7 @@ class _ChooseGameViewState extends State { ); if (result != null && result.game != null) { // Find the index in the original list to mutate - final originalIndex = widget.games.indexWhere( + final originalIndex = games.indexWhere( (g) => g.id == game.id, ); if (originalIndex == -1) { @@ -202,12 +203,12 @@ class _ChooseGameViewState extends State { if (selectedGameId == game.id) { selectedGameId = ''; } - widget.games.removeAt(originalIndex); + games.removeAt(originalIndex); widget.onGamesUpdated?.call(); }); } else { setState(() { - widget.games[originalIndex] = result.game; + games[originalIndex] = result.game; }); } _refreshFromSource(); @@ -229,13 +230,13 @@ class _ChooseGameViewState extends State { final q = query.toLowerCase().trim(); if (q.isEmpty) { setState(() { - filteredGames = List.from(widget.games); + filteredGames = List.from(games); }); return; } setState(() { - filteredGames = widget.games.where((game) { + filteredGames = games.where((game) { final name = game.name.toLowerCase(); final description = game.description.toLowerCase(); return name.contains(q) || description.contains(q); -- 2.49.1 From 12b7bcdc6c93e115d7694f3bd0d0195160dcc301 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 19:54:53 +0200 Subject: [PATCH 37/51] fix: team import --- lib/services/data_transfer_service.dart | 6 +++++- pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index 0f2f8fa..c4cbb0a 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -204,8 +204,10 @@ class DataTransferService { return Team( id: map['id'] as String, name: map['name'] as String, - members: members, createdAt: DateTime.parse(map['createdAt'] as String), + color: AppColor.values.byName(map['color'] ?? AppColor.blue), + members: members, + score: map['score'] as int?, ); }).toList(); } @@ -231,6 +233,7 @@ class DataTransferService { final endedAt = map['endedAt'] != null ? DateTime.parse(map['endedAt'] as String) : null; + final isTeamMatch = map['isTeamMatch'] as bool; final notes = map['notes'] as String? ?? ''; final scoresJson = map['scores'] as Map? ?? {}; final scores = scoresJson.map( @@ -262,6 +265,7 @@ class DataTransferService { game: game, group: group, players: players, + isTeamMatch: isTeamMatch, teams: teams.isEmpty ? null : teams, createdAt: createdAt, endedAt: endedAt, diff --git a/pubspec.yaml b/pubspec.yaml index 8d9c1b0..876ee0c 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.30+327 +version: 0.0.30+330 environment: sdk: ^3.8.1 -- 2.49.1 From 46134a4f5c779c59284097a0f82d2a5350420e14 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 21:10:07 +0200 Subject: [PATCH 38/51] feat: Implemented LiveEditView --- .../match_result/live_edit_view.dart | 89 +++++++ .../match_view/match_result_view.dart | 228 ++++++++---------- pubspec.yaml | 2 +- 3 files changed, 184 insertions(+), 135 deletions(-) create mode 100644 lib/presentation/views/main_menu/match_view/create_match/match_result/live_edit_view.dart diff --git a/lib/presentation/views/main_menu/match_view/create_match/match_result/live_edit_view.dart b/lib/presentation/views/main_menu/match_view/create_match/match_result/live_edit_view.dart new file mode 100644 index 0000000..5b960c6 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/match_result/live_edit_view.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/team.dart'; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; + +class LiveEditView extends StatefulWidget { + const LiveEditView({super.key, required this.match}); + final Match match; + + @override + State createState() => _LiveEditViewState(); +} + +class _LiveEditViewState extends State { + List get allTeams => + (widget.match.teams ?? [])..sort((a, b) => a.name.compareTo(b.name)); + List get allPlayers => + widget.match.players..sort((a, b) => a.name.compareTo(b.name)); + List scores = []; + + @override + void initState() { + super.initState(); + + if (widget.match.isTeamMatch) { + scores = List.generate( + allTeams.length, + (index) => allTeams[index].score ?? 0, + ); + } else { + scores = List.generate( + allPlayers.length, + (index) => widget.match.scores[allPlayers[index].id]?.score ?? 0, + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.match.name), + leading: HapticIconButton( + onPressed: () => Navigator.pop(context, scores), + icon: const Icon(Icons.close), + ), + ), + body: Column( + children: [ + Expanded(child: buildLiveEditWidget(widget.match.isTeamMatch)), + ], + ), + ); + } + + Widget buildLiveEditWidget(bool isTeamMatch) { + if (isTeamMatch) { + return ListView.builder( + itemCount: allTeams.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allTeams[index].name, + onChanged: (value) { + scores[index] = value; + }, + value: scores[index], + ); + }, + ); + } else { + return ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { + setState(() { + scores[index] = value; + }); + }, + value: scores[index], + ); + }, + ); + } + } +} diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 37386b4..64dd8ad 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.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/core/enums.dart'; @@ -11,11 +12,11 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/data/models/team.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/match_result/live_edit_view.dart'; import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart'; @@ -38,8 +39,6 @@ class MatchResultView extends StatefulWidget { class _MatchResultViewState extends State { late final AppDatabase db; - bool isLiveEditMode = false; - late final Ruleset ruleset; late final List allPlayers; @@ -92,121 +91,116 @@ class _MatchResultViewState extends State { appBar: AppBar( automaticallyImplyLeading: true, leading: HapticIconButton( - icon: isLiveEditMode - ? const Icon(Icons.arrow_back_ios) - : const Icon(Icons.close), - onPressed: isLiveEditMode - ? () => setState(() { - isLiveEditMode = false; - }) - : () => {widget.onWinnerChanged?.call(), Navigator.pop(context)}, + icon: const Icon(Icons.close), + onPressed: () => { + widget.onWinnerChanged?.call(), + Navigator.pop(context), + }, ), title: Text(widget.match.name), ), body: Column( children: [ Expanded( - child: isLiveEditMode - // Live Edit Mode - ? buildLiveEditWidet(isTeamMatch) - // Normal Container - : Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 10, - ), - decoration: BoxDecoration( - color: CustomTheme.boxColor, - border: Border.all(color: CustomTheme.boxBorderColor), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - getTitleForRuleset(loc), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - - // Show player selection - if (rulesetSupportsPlayerSelection()) - if (ruleset == Ruleset.multipleWinners) - Expanded( - child: buildMultipleWinnerSelectionWidget( - isTeamMatch, - ), - ) - else - Expanded( - child: buildPlayerSelectionWidget(isTeamMatch), - ), - - // Show score entry - if (rulesetSupportsScoreEntry()) - Expanded(child: buildScoreEntryWidget(isTeamMatch)), - - // Show draggable placement list - if (rulesetSupportsDragBehaviour()) - Expanded(child: buildPlacementWidget(isTeamMatch)), - ], + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getTitleForRuleset(loc), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, ), ), + const SizedBox(height: 10), + + // Show player selection + if (rulesetSupportsPlayerSelection()) + if (ruleset == Ruleset.multipleWinners) + Expanded( + child: buildMultipleWinnerSelectionWidget(isTeamMatch), + ) + else + Expanded(child: buildPlayerSelectionWidget(isTeamMatch)), + + // Show score entry + if (rulesetSupportsScoreEntry()) + Expanded(child: buildScoreEntryWidget(isTeamMatch)), + + // Show draggable placement list + if (rulesetSupportsDragBehaviour()) + Expanded(child: buildPlacementWidget(isTeamMatch)), + ], + ), + ), ), - if (!isLiveEditMode) ...[ - Padding( - padding: const EdgeInsets.fromLTRB(12, 0, 12, 20), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (rulesetSupportsScoreEntry()) ...[ - // Button to switch to live edit mode - AnimatedDialogButton( - buttonConstraints: const BoxConstraints( - minWidth: double.infinity, - minHeight: 50, - ), - buttonText: loc.live_edit_mode, - buttonType: ButtonType.secondary, - onPressed: () => setState(() { - isLiveEditMode = !isLiveEditMode; - }), - ), - ], - - // Save Changes Button + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Live Edit Mode Button + if (rulesetSupportsScoreEntry()) ...[ AnimatedDialogButton( buttonConstraints: const BoxConstraints( minWidth: double.infinity, minHeight: 50, ), - buttonText: loc.save_changes, - onPressed: canSave - ? () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.pop(context); - } - : null, + buttonText: loc.live_edit_mode, + buttonType: ButtonType.secondary, + onPressed: () => + Navigator.push( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => + LiveEditView(match: widget.match), + ), + ).then( + (scores) => { + if (scores != null) + { + for (int i = 0; i < scores.length; i++) + {controller[i].text = scores[i].toString()}, + }, + }, + ), ), ], - ), + + // Save Changes Button + AnimatedDialogButton( + buttonConstraints: const BoxConstraints( + minWidth: double.infinity, + minHeight: 50, + ), + buttonText: loc.save_changes, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.pop(context); + } + : null, + ), + ], ), - ], + ), ], ), ); @@ -847,38 +841,4 @@ class _MatchResultViewState extends State { ); } } - - Widget buildLiveEditWidet(bool isTeamMatch) { - if (isTeamMatch) { - return ListView.builder( - itemCount: allTeams.length, - itemBuilder: (context, index) { - return LiveEditListTile( - title: allTeams[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); - }, - ); - } else { - return ListView.builder( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return LiveEditListTile( - title: allPlayers[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); - }, - ); - } - } } diff --git a/pubspec.yaml b/pubspec.yaml index 876ee0c..9b94141 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.30+330 +version: 0.0.30+331 environment: sdk: ^3.8.1 -- 2.49.1 From 14eb77e241e65777a87950d7ba9c20af2c708e56 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 21:22:12 +0200 Subject: [PATCH 39/51] fix: team json tests --- lib/services/data_transfer_service.dart | 10 ++------ test/services/data_transfer_service_test.dart | 23 ++++++++++++++++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart index c4cbb0a..6bc5cda 100644 --- a/lib/services/data_transfer_service.dart +++ b/lib/services/data_transfer_service.dart @@ -200,15 +200,9 @@ class DataTransferService { .map((id) => playerById[id]) .whereType() .toList(); + final team = Team.fromJson(map); - return Team( - id: map['id'] as String, - name: map['name'] as String, - createdAt: DateTime.parse(map['createdAt'] as String), - color: AppColor.values.byName(map['color'] ?? AppColor.blue), - members: members, - score: map['score'] as int?, - ); + return team.copyWith(members: members); }).toList(); } diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index a7594c7..73a412a 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -55,7 +55,12 @@ void main() { members: [testPlayer1, testPlayer2], ); - testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]); + testTeam = Team( + name: 'Test Team', + color: AppColor.yellow, + score: 5, + members: [testPlayer1, testPlayer2], + ); testMatch = Match( name: 'Test Match', @@ -666,6 +671,8 @@ void main() { 'name': testTeam.name, 'memberIds': [testPlayer1.id], 'createdAt': testTeam.createdAt.toIso8601String(), + 'color': testTeam.color.toString(), + 'score': testTeam.score, }, ]; @@ -679,6 +686,8 @@ void main() { expect(teams[0].name, testTeam.name); expect(teams[0].members.length, 1); expect(teams[0].members[0].id, testPlayer1.id); + expect(teams[0].color, testTeam.color); + expect(teams[0].score, testTeam.score); }); test('parseTeamsFromJson() empty list', () { @@ -715,6 +724,9 @@ void main() { 'gameId': testGame.id, 'groupId': testGroup.id, 'playerIds': [testPlayer1.id, testPlayer2.id], + 'isTeamMatch': false, + 'teams': null, + 'scores': null, 'notes': testMatch.notes, 'createdAt': testMatch.createdAt.toIso8601String(), }, @@ -770,6 +782,9 @@ void main() { 'name': testMatch.name, 'gameId': 'non-existent-game-id', 'playerIds': [testPlayer1.id], + 'isTeamMatch': false, + 'teams': null, + 'scores': null, 'notes': '', 'createdAt': testMatch.createdAt.toIso8601String(), }, @@ -801,6 +816,9 @@ void main() { 'gameId': testGame.id, 'groupId': null, 'playerIds': [testPlayer1.id], + 'isTeamMatch': false, + 'teams': null, + 'scores': null, 'notes': '', 'createdAt': testMatch.createdAt.toIso8601String(), }, @@ -831,6 +849,9 @@ void main() { 'name': testMatch.name, 'gameId': testGame.id, 'playerIds': [testPlayer1.id], + 'isTeamMatch': false, + 'teams': null, + 'scores': null, 'notes': '', 'createdAt': testMatch.createdAt.toIso8601String(), 'endedAt': endedDate.toIso8601String(), -- 2.49.1 From a7d36787ce283627b3c13e93f38283c47523c6a8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 21:28:36 +0200 Subject: [PATCH 40/51] fix: enum fromJson --- lib/data/models/game.dart | 6 +++++- lib/data/models/team.dart | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index ec69204..d5bd3e8 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -73,7 +73,11 @@ class Game { orElse: () => Ruleset.singleWinner, ), description = json['description'], - color = AppColor.values.firstWhere((e) => e.name == json['color']), + color = AppColor.values.firstWhere( + (value) => + value.name == json['color'] || value.toString() == json['color'], + orElse: () => AppColor.blue, + ), icon = json['icon']; Map toJson() => { diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index 56576c8..0069a1f 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -71,7 +71,11 @@ class Team { : id = json['id'], name = json['name'], createdAt = DateTime.parse(json['createdAt']), - color = AppColor.values.byName(json['color'] ?? AppColor.blue.name), + color = AppColor.values.firstWhere( + (value) => + value.name == json['color'] || value.toString() == json['color'], + orElse: () => AppColor.blue, + ), score = json['score'] ?? 0, members = []; // Populated during import via DataTransferService -- 2.49.1 From a6deba42380ed583d97665b13e70eb9ddfab7b8d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 21 May 2026 21:32:57 +0200 Subject: [PATCH 41/51] updated tests --- lib/data/models/game.dart | 5 ++--- lib/data/models/team.dart | 5 ++--- test/services/data_transfer_service_test.dart | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/data/models/game.dart b/lib/data/models/game.dart index d5bd3e8..44c7321 100644 --- a/lib/data/models/game.dart +++ b/lib/data/models/game.dart @@ -74,9 +74,8 @@ class Game { ), description = json['description'], color = AppColor.values.firstWhere( - (value) => - value.name == json['color'] || value.toString() == json['color'], - orElse: () => AppColor.blue, + (e) => e.name == json['color'], + orElse: () => AppColor.orange, ), icon = json['icon']; diff --git a/lib/data/models/team.dart b/lib/data/models/team.dart index 0069a1f..408e2a5 100644 --- a/lib/data/models/team.dart +++ b/lib/data/models/team.dart @@ -72,9 +72,8 @@ class Team { name = json['name'], createdAt = DateTime.parse(json['createdAt']), color = AppColor.values.firstWhere( - (value) => - value.name == json['color'] || value.toString() == json['color'], - orElse: () => AppColor.blue, + (e) => e.name == json['color'], + orElse: () => AppColor.orange, ), score = json['score'] ?? 0, members = []; // Populated during import via DataTransferService diff --git a/test/services/data_transfer_service_test.dart b/test/services/data_transfer_service_test.dart index 73a412a..0ea8b83 100644 --- a/test/services/data_transfer_service_test.dart +++ b/test/services/data_transfer_service_test.dart @@ -671,7 +671,7 @@ void main() { 'name': testTeam.name, 'memberIds': [testPlayer1.id], 'createdAt': testTeam.createdAt.toIso8601String(), - 'color': testTeam.color.toString(), + 'color': testTeam.color.name, 'score': testTeam.score, }, ]; -- 2.49.1 From df64ef4b93533d5186c9a1a8422000cabe8cd6ea Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 00:34:55 +0200 Subject: [PATCH 42/51] fix: removeAllTeamScores --- lib/data/dao/team_dao.dart | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 5b1da89..793cce8 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -203,6 +203,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { } /// Updates the score of the team with the given [teamId]. + /// Updates the member scores correspondingly Future updateTeamScore({ required String teamId, required String matchId, @@ -220,7 +221,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { final members = await _getTeamMembers(teamId: teamId); for (final member in members) { - await db.scoreEntryDao.updateScore( + await db.scoreEntryDao.addScore( playerId: member.id, matchId: matchId, entry: ScoreEntry(score: score), @@ -241,12 +242,27 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return true; } + /// Removes the scores for all teams in the match with the given [matchId] by setting their scores to null. Future removeAllTeamScores({required String matchId}) async { - await (update( - teamTable, - )).write(const TeamTableCompanion(score: Value(null))); + // collect all teamIds for the given matchId from playerMatchTable + final teamIds = + await (selectOnly(playerMatchTable) + ..addColumns([playerMatchTable.teamId]) + ..where(playerMatchTable.matchId.equals(matchId))) + .map((row) => row.read(playerMatchTable.teamId)) + .get(); + + // filter null or duplicates + final filteredTeamIds = teamIds.whereType().toSet().toList(); + + var rowsAffected = 0; + if (filteredTeamIds.isNotEmpty) { + rowsAffected = + await (update(teamTable)..where((t) => t.id.isIn(filteredTeamIds))) + .write(const TeamTableCompanion(score: Value(null))); + } await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId); - return true; + return rowsAffected > 0; } /* Delete */ @@ -285,6 +301,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { required String matchId, }) async { List success = List.generate(winners.length, (index) => null); + for (int i = 0; i < winners.length; i++) { success[i] = await updateTeamScore( teamId: winners[i].id, @@ -310,12 +327,9 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { return await updateTeamScore(teamId: teamId, matchId: matchId, score: 0); } - /// Removes the loser status from the team with the given [teamId] in the match with the given [matchId] by setting its score to null. + /// Removes the loser from the match with the given [matchId] by setting its score to null. /// Returns `true` if the score was updated successfully, `false` otherwise. - Future removeLoserTeam({ - required String teamId, - required String matchId, - }) async { + Future removeLoserTeam({required String matchId}) async { return await removeAllTeamScores(matchId: matchId); } -- 2.49.1 From 6fb4a8996cdb9a9d12752f8032ca5c26cd36777d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 00:48:21 +0200 Subject: [PATCH 43/51] updated team tests --- lib/data/dao/score_entry_dao.dart | 6 +- lib/data/dao/team_dao.dart | 43 +++- .../match_view/match_result_view.dart | 5 +- pubspec.yaml | 2 +- test/db_tests/aggregates/match_test.dart | 46 ++-- test/db_tests/aggregates/team_test.dart | 197 +++++++++++++++++- 6 files changed, 258 insertions(+), 41 deletions(-) diff --git a/lib/data/dao/score_entry_dao.dart b/lib/data/dao/score_entry_dao.dart index 830135d..9e41524 100644 --- a/lib/data/dao/score_entry_dao.dart +++ b/lib/data/dao/score_entry_dao.dart @@ -16,12 +16,12 @@ class ScoreEntryDao extends DatabaseAccessor /* Create */ /// Adds a score entry to the database. - Future addScore({ + Future addScore({ required String playerId, required String matchId, required ScoreEntry entry, }) async { - await into(scoreEntryTable).insert( + final rowsAffected = await into(scoreEntryTable).insert( ScoreEntryTableCompanion.insert( playerId: playerId, matchId: matchId, @@ -31,6 +31,8 @@ class ScoreEntryDao extends DatabaseAccessor ), mode: InsertMode.insertOrReplace, ); + + return rowsAffected > 0; } Future addScoresAsList({ diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 793cce8..32e5f07 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -212,7 +212,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { await (update(teamTable)..where((t) => t.id.equals(teamId))).write( const TeamTableCompanion(score: Value(null)), ); - await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId); + await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId); final rowsAffected = await (update(teamTable)..where((t) => t.id.equals(teamId))).write( @@ -238,7 +238,7 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { await (update(teamTable)..where((t) => t.id.equals(teamId))).write( const TeamTableCompanion(score: Value(null)), ); - await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId); + await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId); return true; } @@ -300,16 +300,17 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { required List winners, required String matchId, }) async { - List success = List.generate(winners.length, (index) => null); - - for (int i = 0; i < winners.length; i++) { - success[i] = await updateTeamScore( - teamId: winners[i].id, - matchId: matchId, - score: 1, - ); + // Reset all team scores . + await removeAllTeamScores(matchId: matchId); + // Reset all score entries + for (final team in winners) { + await _deleteAllScoresForMembersOfTeam(teamId: team.id, matchId: matchId); } - return success.every((result) => result == true); + + for (final team in winners) { + await updateTeamScore(teamId: team.id, matchId: matchId, score: 1); + } + return true; } /// Removes the winner status from all Teams with the given [matchId] by setting its score to null. @@ -349,4 +350,24 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { } return success.every((result) => result == true); } + + /// Helper method to delete all scores for members of a team in a specific match. + Future _deleteAllScoresForMembersOfTeam({ + required String teamId, + required String matchId, + }) async { + final playerMatchQuery = select(db.playerMatchTable) + ..where((pm) => pm.teamId.equals(teamId) & pm.matchId.equals(matchId)); + final playerMatches = await playerMatchQuery.get(); + + if (playerMatches.isEmpty) return false; + + for (final pm in playerMatches) { + await db.scoreEntryDao.deleteAllScoresForPlayerInMatch( + playerId: pm.playerId, + matchId: matchId, + ); + } + return true; + } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 64dd8ad..8708bfe 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -362,10 +362,7 @@ class _MatchResultViewState extends State { Future _handleLoser() async { if (isTeamMatch) { if (_selectedTeam == null) { - return await db.teamDao.removeLoserTeam( - matchId: widget.match.id, - teamId: _selectedTeam!.id, - ); + return await db.teamDao.removeLoserTeam(matchId: widget.match.id); } else { return await db.teamDao.setLoserTeam( matchId: widget.match.id, diff --git a/pubspec.yaml b/pubspec.yaml index 9b94141..5e57448 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.30+331 +version: 0.0.30+332 environment: sdk: ^3.8.1 diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 916d5d4..3f76a23 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -507,34 +507,36 @@ void main() { deleted = await database.matchDao.deleteAllMatches(); expect(deleted, isFalse); }); - }); - test('deleteMatchesByGame() deletes all matches for a game', () async { - await database.matchDao.addMatch(match: testMatch1); - await database.matchDao.addMatch(match: testMatch2); + test('deleteMatchesByGame() deletes all matches for a game', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: testMatch2); - var count = await database.matchDao.getMatchCountByGame( - gameId: testGame.id, - ); - expect(count, 2); + var count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 2); - final deletedCount = await database.matchDao.deleteMatchesByGame( - gameId: testGame.id, - ); - expect(deletedCount, 2); + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: testGame.id, + ); + expect(deletedCount, 2); - count = await database.matchDao.getMatchCountByGame(gameId: testGame.id); - expect(count, 0); + count = await database.matchDao.getMatchCountByGame( + gameId: testGame.id, + ); + expect(count, 0); - final allMatches = await database.matchDao.getAllMatches(); - expect(allMatches, isEmpty); - }); + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches, isEmpty); + }); - test('deleteMatchesByGame() returns 0 for non-existent game', () async { - final deletedCount = await database.matchDao.deleteMatchesByGame( - gameId: 'non-existent-game-id', - ); - expect(deletedCount, 0); + test('deleteMatchesByGame() returns 0 for non-existent game', () async { + final deletedCount = await database.matchDao.deleteMatchesByGame( + gameId: 'non-existent-game-id', + ); + expect(deletedCount, 0); + }); }); }); } diff --git a/test/db_tests/aggregates/team_test.dart b/test/db_tests/aggregates/team_test.dart index 381d22b..832dcb8 100644 --- a/test/db_tests/aggregates/team_test.dart +++ b/test/db_tests/aggregates/team_test.dart @@ -1,7 +1,7 @@ import 'dart:core' hide Match; import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:tallee/core/enums.dart'; @@ -327,5 +327,200 @@ void main() { expect(deleted, isFalse); }); }); + + group('SCORE', () { + test('updateTeamScore() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final updated = await database.teamDao.updateTeamScore( + teamId: testTeam1.id, + matchId: testMatch1.id, + score: 5, + ); + expect(updated, isTrue); + final team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, 5); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 5); + } + }); + + test('set-/removeWinnerTeam() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final set = await database.teamDao.setWinnerTeam( + teamId: testTeam1.id, + matchId: testMatch1.id, + ); + expect(set, isTrue); + + var team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, 1); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 1); + } + + final removed = await database.teamDao.removeWinnerTeam( + matchId: testMatch1.id, + ); + expect(removed, isTrue); + + team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, isNull); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNull); + } + }); + + test('set-/removeLoserTeam() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final set = await database.teamDao.setLoserTeam( + teamId: testTeam1.id, + matchId: testMatch1.id, + ); + expect(set, isTrue); + + var team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, 0); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 0); + } + + final removed = await database.teamDao.removeLoserTeam( + matchId: testMatch1.id, + ); + expect(removed, isTrue); + + team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, isNull); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNull); + } + }); + + test('set-/removeWinnerTeams() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final set = await database.teamDao.setWinnerTeams( + winners: [testTeam1, testTeam2], + matchId: testMatch1.id, + ); + expect(set, isTrue); + + // check both teams got the winner score + var team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, 1); + team = await database.teamDao.getTeamById(teamId: testTeam2.id); + expect(team.score, 1); + + // check all members of both teams got the winner score + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 1); + } + + for (final member in testTeam2.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 1); + } + + final removed = await database.teamDao.removeWinnerTeam( + matchId: testMatch1.id, + ); + expect(removed, isTrue); + + team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, isNull); + + team = await database.teamDao.getTeamById(teamId: testTeam2.id); + expect(team.score, isNull); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNull); + } + + for (final member in testTeam2.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNull); + } + }); + + test('setTeamPlacements() works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final set = await database.teamDao.setTeamPlacements( + teams: [testTeam1, testTeam2], + matchId: testMatch1.id, + ); + expect(set, isTrue); + + var team = await database.teamDao.getTeamById(teamId: testTeam1.id); + expect(team.score, 2); + team = await database.teamDao.getTeamById(teamId: testTeam2.id); + expect(team.score, 1); + + for (final member in testTeam1.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 2); + } + + for (final member in testTeam2.members) { + final entry = await database.scoreEntryDao.getScore( + playerId: member.id, + matchId: testMatch1.id, + ); + expect(entry, isNotNull); + expect(entry!.score, 1); + } + }); + }); }); } -- 2.49.1 From a0897d4966aceff9c83c96eb9c315b3f304786b6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 14:44:16 +0200 Subject: [PATCH 44/51] fix: incorrect user of paraent data widget --- .../create_match/create_teams/create_teams_view.dart | 4 ++-- .../create_match/create_teams/manage_members_view.dart | 5 ++--- pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart index 31ed6d6..f33eea5 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart @@ -59,9 +59,9 @@ class _CreateTeamsViewState extends State { body: Stack( alignment: Alignment.center, children: [ - Expanded( + Positioned.fill( child: ListView.builder( - padding: const EdgeInsets.only(top: 12, bottom: 12), + padding: const EdgeInsets.only(top: 12, bottom: 96), itemCount: teams.length, itemBuilder: (context, index) { return TeamCreationTile( diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart index a668f33..a952d5d 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -55,9 +55,9 @@ class _ManageMembersViewState extends State { body: Stack( alignment: AlignmentDirectional.center, children: [ - Expanded( + Positioned.fill( child: ReorderableListView.builder( - padding: const EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.fromLTRB(0, 12, 0, 96), buildDefaultDragHandles: false, itemCount: allItemsCount, onReorder: onReorder, @@ -282,7 +282,6 @@ class _ManageMembersViewState extends State { void submitMatch() async { final match = widget.match; - print('teams: ${match.teams}'); await db.matchDao.addMatch(match: match); if (mounted) { Navigator.pushAndRemoveUntil( diff --git a/pubspec.yaml b/pubspec.yaml index 5e57448..18cc5bc 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.30+332 +version: 0.0.30+333 environment: sdk: ^3.8.1 -- 2.49.1 From 96037e606218f2be9288dd09022d95377b43cf55 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 14:52:30 +0200 Subject: [PATCH 45/51] fix: placing user at header 2 --- .../create_teams/manage_members_view.dart | 25 ++++++++++++------- pubspec.yaml | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart index a952d5d..f24da72 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -201,13 +201,13 @@ class _ManageMembersViewState extends State { if (newIndex > oldIndex) targetIndex -= 1; targetIndex = targetIndex.clamp(0, allItemsCount - 1); - // Resolve target location based on the item currently at targetIndex - // before the move. + // Resolve target location based on the item currently + // at targetIndex before the move. int destTeamIndex; int insertPositionInTeam; if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) { - // Dropped at the very end, append to the last team. + // dropped at the very end, append to the last team. destTeamIndex = teams.length - 1; insertPositionInTeam = teams[destTeamIndex].members.length; } else { @@ -215,14 +215,21 @@ class _ManageMembersViewState extends State { final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex); if (anchorMemberIndex == -1) { - // Dropped right before a header, append to the previous team. - destTeamIndex = destTeamIndex - 1; - if (destTeamIndex < 0) { - // Dropped above the very first header, stay in team 0 at top. - destTeamIndex = 0; + // dropped on a header, direction decides which team the player gets added + // if moving down, insert as first member of that team. + // if moving UP, append to the previous team. + final isMovingDown = newIndex > oldIndex; + if (isMovingDown) { insertPositionInTeam = 0; } else { - insertPositionInTeam = teams[destTeamIndex].members.length; + final previousTeamIndex = destTeamIndex - 1; + if (previousTeamIndex < 0) { + // above the very first header, stay at top of team 0. + insertPositionInTeam = 0; + } else { + destTeamIndex = previousTeamIndex; + insertPositionInTeam = teams[destTeamIndex].members.length; + } } } else { insertPositionInTeam = anchorMemberIndex; diff --git a/pubspec.yaml b/pubspec.yaml index 18cc5bc..54ce5a8 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.30+333 +version: 0.0.30+334 environment: sdk: ^3.8.1 -- 2.49.1 From 47829a695542525717d4267c7bc9c1372f336cb3 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 22 May 2026 15:05:43 +0200 Subject: [PATCH 46/51] fix: sorting problems --- lib/data/dao/match_dao.dart | 3 ++- lib/data/dao/player_match_dao.dart | 3 ++- lib/data/dao/team_dao.dart | 3 ++- .../views/main_menu/match_view/match_result_view.dart | 3 +-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 3a77147..39e0990 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -406,7 +406,8 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)), ); - return teams; + return teams + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } /* Update */ diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index d119468..5ddc72e 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -74,7 +74,8 @@ class PlayerMatchDao extends DatabaseAccessor (row) => db.playerDao.getPlayerById(playerId: row.playerId), ); final players = await Future.wait(futures); - return players; + return players + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } /// Retrieves a list of [Player]s associated with a specific team in a match. diff --git a/lib/data/dao/team_dao.dart b/lib/data/dao/team_dao.dart index 32e5f07..213d24e 100644 --- a/lib/data/dao/team_dao.dart +++ b/lib/data/dao/team_dao.dart @@ -173,7 +173,8 @@ class TeamDao extends DatabaseAccessor with _$TeamDaoMixin { final players = await Future.wait( playerIds.map((id) => db.playerDao.getPlayerById(playerId: id)), ); - return players; + return players + ..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); } /* Update */ diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 8708bfe..d0a9ad6 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -208,7 +208,6 @@ class _MatchResultViewState extends State { void initializeAsTeamMatch() { allTeams = [...(widget.match.teams ?? [])]; - allTeams.sort((a, b) => a.name.compareTo(b.name)); controller = List.generate( allTeams.length, @@ -508,7 +507,7 @@ class _MatchResultViewState extends State { ), ), ), - if (team.members.length > 4) + if (team.members.length > showingPlayerAmount) Container( padding: const EdgeInsets.symmetric( vertical: 4, -- 2.49.1 From 4b3067312547b34156f22889e72505ca07864b80 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 23 May 2026 00:41:36 +0200 Subject: [PATCH 47/51] Updated deprecated attribute --- .../create_match/create_teams/manage_members_view.dart | 4 ++-- .../views/main_menu/match_view/match_result_view.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart index f24da72..7d2f2d7 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart @@ -60,7 +60,7 @@ class _ManageMembersViewState extends State { padding: const EdgeInsets.fromLTRB(0, 12, 0, 96), buildDefaultDragHandles: false, itemCount: allItemsCount, - onReorder: onReorder, + onReorderItem: onReorderItem, proxyDecorator: (child, index, animation) => Material(type: MaterialType.transparency, child: child), itemBuilder: (context, index) { @@ -188,7 +188,7 @@ class _ManageMembersViewState extends State { } /// Handles moving a member from one team to another - void onReorder(int oldIndex, int newIndex) { + void onReorderItem(int oldIndex, int newIndex) { final sourceTeamIndex = teamIndexForFlat(oldIndex); final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex); diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index d0a9ad6..0c16191 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -715,7 +715,7 @@ class _MatchResultViewState extends State { }, ); }, - onReorder: (int oldIndex, int newIndex) { + onReorderItem: (int oldIndex, int newIndex) { setState(() { if (newIndex > oldIndex) { newIndex -= 1; @@ -767,7 +767,7 @@ class _MatchResultViewState extends State { }, ); }, - onReorder: (int oldIndex, int newIndex) { + onReorderItem: (int oldIndex, int newIndex) { setState(() { if (newIndex > oldIndex) { newIndex -= 1; -- 2.49.1 From a1b2a1d722612a0894b010e8ffe025f94812e9db Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 23 May 2026 01:08:29 +0200 Subject: [PATCH 48/51] Updated highlightedBoxDecoration --- lib/core/custom_theme.dart | 6 +++++- lib/l10n/generated/app_localizations.dart | 12 ++++++------ lib/l10n/generated/app_localizations_de.dart | 6 +++--- lib/l10n/generated/app_localizations_en.dart | 6 +++--- lib/presentation/widgets/tiles/game_tile.dart | 3 ++- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index bb6d4b4..585e951 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -65,7 +65,11 @@ class CustomTheme { static BoxDecoration highlightedBoxDecoration = BoxDecoration( color: boxColor, - border: Border.all(color: textColor, width: 2), + border: Border.all( + color: textColor, + width: 2, + strokeAlign: BorderSide.strokeAlignCenter, + ), borderRadius: standardBorderRadiusAll, ); diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index ff45d67..defaa4b 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -530,12 +530,6 @@ abstract class AppLocalizations { /// **'Live Edit Mode'** String get live_edit_mode; - /// No description provided for @manage_members. - /// - /// In en, this message translates to: - /// **'Manage Members'** - String get manage_members; - /// No description provided for @loser. /// /// In en, this message translates to: @@ -548,6 +542,12 @@ abstract class AppLocalizations { /// **'Lowest Score'** String get lowest_score; + /// No description provided for @manage_members. + /// + /// In en, this message translates to: + /// **'Manage Members'** + String get manage_members; + /// No description provided for @match_in_progress. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index d6bd7ae..5f2f606 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -240,15 +240,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get live_edit_mode => 'Live-Bearbeitungsmodus'; - @override - String get manage_members => 'Mitglieder bearbeiten'; - @override String get loser => 'Verlierer:in'; @override String get lowest_score => 'Niedrigste Punkte'; + @override + String get manage_members => 'Mitglieder bearbeiten'; + @override String get match_in_progress => 'Spiel läuft...'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 830b515..c0f9fcc 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -240,15 +240,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get live_edit_mode => 'Live Edit Mode'; - @override - String get manage_members => 'Manage Members'; - @override String get loser => 'Loser'; @override String get lowest_score => 'Lowest Score'; + @override + String get manage_members => 'Manage Members'; + @override String get match_in_progress => 'Match in progress...'; diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index 11d96d8..9c274e9 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -67,13 +67,14 @@ class GameTile extends StatelessWidget { } }, child: AnimatedContainer( - margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), decoration: !isHighlighted ? CustomTheme.standardBoxDecoration : CustomTheme.highlightedBoxDecoration.copyWith( border: Border.all( color: gameColor.withValues(alpha: 0.9), width: 2, + strokeAlign: BorderSide.strokeAlignCenter, ), ), duration: const Duration(milliseconds: 200), -- 2.49.1 From 3c9d115d08a85debb728f3cf9d0c15009944a4e0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 23 May 2026 01:18:16 +0200 Subject: [PATCH 49/51] Updated game tile --- .../create_match/choose_game_view.dart | 5 +--- lib/presentation/widgets/tiles/game_tile.dart | 26 ++++++++++++++----- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 34f9be0..a4a7f2e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -161,10 +161,7 @@ class _ChooseGameViewState extends State { return GameTile( title: game.name, description: game.description, - badgeText: translateRulesetToString( - game.ruleset, - context, - ), + subtitle: translateRulesetToString(game.ruleset, context), badgeColor: getColorFromGameColor(game.color), isHighlighted: selectedGameId == game.id, onTap: () async { diff --git a/lib/presentation/widgets/tiles/game_tile.dart b/lib/presentation/widgets/tiles/game_tile.dart index 9c274e9..2417408 100644 --- a/lib/presentation/widgets/tiles/game_tile.dart +++ b/lib/presentation/widgets/tiles/game_tile.dart @@ -7,6 +7,7 @@ import 'package:tallee/core/enums.dart'; class GameTile extends StatelessWidget { /// A list tile widget that displays a title and description, with optional highlighting and badge. /// - [title]: The title text displayed on the tile. + /// - [subtitle]: An optional subtitle displayed under the title. /// - [description]: The description text displayed below the title. /// - [onTap]: The callback invoked when the tile is tapped. /// - [onLongPress]: The callback invoked when the tile is tapped. @@ -17,6 +18,7 @@ class GameTile extends StatelessWidget { super.key, required this.title, required this.description, + this.subtitle, this.onTap, this.onLongPress, this.isHighlighted = false, @@ -24,25 +26,20 @@ class GameTile extends StatelessWidget { this.badgeColor, }); - /// The title text displayed on the tile. final String title; - /// The description text displayed below the title. + final String? subtitle; + final String description; - /// The callback invoked when the tile is tapped. final VoidCallback? onTap; - /// The callback invoked when the tile is long-pressed. final VoidCallback? onLongPress; - /// A boolean to determine if the tile should be highlighted. final bool isHighlighted; - /// Optional text to display in a badge on the right side of the title. final String? badgeText; - /// Optional color for the badge background. final Color? badgeColor; @override @@ -119,6 +116,21 @@ class GameTile extends StatelessWidget { ), ), + // Title + if (subtitle != null && subtitle!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + subtitle!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.hintColor, + ), + ), + ], + // Badge if (badgeText != null) ...[ const SizedBox(height: 5), -- 2.49.1 From 6d17539af2a680691ad6d1058b58926b80692ec8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 23 May 2026 16:12:44 +0200 Subject: [PATCH 50/51] Updated text icon tile --- .../views/main_menu/group_view/group_detail_view.dart | 1 - lib/presentation/widgets/cards/team_card.dart | 1 - lib/presentation/widgets/tiles/group_tile.dart | 1 - lib/presentation/widgets/tiles/text_icon_tile.dart | 7 ++----- 4 files changed, 2 insertions(+), 8 deletions(-) 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 c417ec4..47f4b3f 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 @@ -150,7 +150,6 @@ class _GroupDetailViewState extends State { return TextIconTile( text: member.name, suffixText: getNameCountText(member), - iconEnabled: false, ); }).toList(), ), diff --git a/lib/presentation/widgets/cards/team_card.dart b/lib/presentation/widgets/cards/team_card.dart index 43ef842..9121805 100644 --- a/lib/presentation/widgets/cards/team_card.dart +++ b/lib/presentation/widgets/cards/team_card.dart @@ -92,7 +92,6 @@ class TeamCard extends StatelessWidget { return TextIconTile( text: player.name, suffixText: getNameCountText(player), - iconEnabled: false, ); }).toList(), ), diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index f6c406e..7744296 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -91,7 +91,6 @@ class _GroupTileState extends State { TextIconTile( text: member.name, suffixText: getNameCountText(member), - iconEnabled: false, ), ], ), diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart index 08b87cd..499cbbb 100644 --- a/lib/presentation/widgets/tiles/text_icon_tile.dart +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -4,14 +4,12 @@ import 'package:tallee/core/custom_theme.dart'; class TextIconTile extends StatelessWidget { /// A tile widget that displays text with an optional icon that can be tapped. /// - [text]: The text to display in the tile. - /// - [iconEnabled]: A boolean to determine if the icon should be displayed. /// - [onIconTap]: The callback to be invoked when the icon is tapped. /// - [icon]: Optional custom icon. Defaults to [Icons.close]. const TextIconTile({ super.key, required this.text, this.suffixText = '', - this.iconEnabled = true, this.onIconTap, this.icon = Icons.close, }); @@ -21,9 +19,6 @@ class TextIconTile extends StatelessWidget { final String suffixText; - /// A boolean to determine if the icon should be displayed. - final bool iconEnabled; - /// The callback to be invoked when the icon is tapped. final VoidCallback? onIconTap; @@ -32,6 +27,8 @@ class TextIconTile extends StatelessWidget { @override Widget build(BuildContext context) { + final iconEnabled = onIconTap != null; + return Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( -- 2.49.1 From 99c3c3c257e81a99291e7d0851812ba67836143b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 23 May 2026 16:12:58 +0200 Subject: [PATCH 51/51] Added empty build for teams & player in match tile / detail view --- lib/l10n/arb/app_de.arb | 2 + lib/l10n/arb/app_en.arb | 2 + lib/l10n/generated/app_localizations.dart | 12 ++++ lib/l10n/generated/app_localizations_de.dart | 6 ++ lib/l10n/generated/app_localizations_en.dart | 6 ++ .../match_view/match_detail_view.dart | 60 ++++++++++++------- .../widgets/tiles/match_tile.dart | 13 +++- 7 files changed, 77 insertions(+), 24 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index de579ff..95acced 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -96,6 +96,7 @@ "no_license_text_available": "Kein Lizenztext verfügbar", "no_licenses_found": "Keine Lizenzen gefunden", "no_matches_created_yet": "Noch keine Spiele erstellt", + "no_players_available": "Keine Spieler:innen verfügbar", "no_players_created_yet": "Noch keine Spieler:in erstellt", "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_selected": "Keine Spieler:innen ausgewählt", @@ -103,6 +104,7 @@ "no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_second_match_available": "Kein zweites Spiel verfügbar", "no_statistics_available": "Keine Statistiken verfügbar", + "no_teams_available": "Keine Teams verfügbar", "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b77797c..a5effcd 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -96,6 +96,7 @@ "no_license_text_available": "No license text available", "no_licenses_found": "No licenses found", "no_matches_created_yet": "No matches created yet", + "no_players_available": "No players available", "no_players_created_yet": "No players created yet", "no_players_found_with_that_name": "No players found with that name", "no_players_selected": "No players selected", @@ -103,6 +104,7 @@ "no_results_entered_yet": "No results entered yet", "no_second_match_available": "No second match available", "no_statistics_available": "No statistics available", + "no_teams_available": "No teams available", "none": "None", "none_group": "None", "not_available": "Not available", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index defaa4b..27e9ea5 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -632,6 +632,12 @@ abstract class AppLocalizations { /// **'No matches created yet'** String get no_matches_created_yet; + /// No description provided for @no_players_available. + /// + /// In en, this message translates to: + /// **'No players available'** + String get no_players_available; + /// No description provided for @no_players_created_yet. /// /// In en, this message translates to: @@ -674,6 +680,12 @@ abstract class AppLocalizations { /// **'No statistics available'** String get no_statistics_available; + /// No description provided for @no_teams_available. + /// + /// In en, this message translates to: + /// **'No teams available'** + String get no_teams_available; + /// No description provided for @none. /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index 5f2f606..ff95f6c 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -291,6 +291,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_matches_created_yet => 'Noch keine Spiele erstellt'; + @override + String get no_players_available => 'Keine Spieler:innen verfügbar'; + @override String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; @@ -313,6 +316,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_statistics_available => 'Keine Statistiken verfügbar'; + @override + String get no_teams_available => 'Keine Teams verfügbar'; + @override String get none => 'Kein'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index c0f9fcc..1060a9d 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -291,6 +291,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_matches_created_yet => 'No matches created yet'; + @override + String get no_players_available => 'No players available'; + @override String get no_players_created_yet => 'No players created yet'; @@ -313,6 +316,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_statistics_available => 'No statistics available'; + @override + String get no_teams_available => 'No teams available'; + @override String get none => 'None'; 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 3087e6d..c6272b4 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 @@ -162,15 +162,24 @@ class _MatchDetailViewState extends State { title: loc.teams, icon: Icons.scoreboard, horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: (localMatch.teams ?? []).map((team) { - return TeamCard(team: team); - }).toList(), - ), + content: + localMatch.teams != null && localMatch.teams!.isNotEmpty + ? Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: (localMatch.teams ?? []).map((team) { + return TeamCard(team: team); + }).toList(), + ) + : Text( + loc.no_teams_available, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.textColor, + ), + ), ), ] else ...[ // Players @@ -178,19 +187,26 @@ class _MatchDetailViewState extends State { title: loc.players, icon: Icons.people, horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: localMatch.players.map((player) { - return TextIconTile( - text: player.name, - suffixText: getNameCountText(player), - iconEnabled: false, - ); - }).toList(), - ), + content: localMatch.players.isNotEmpty + ? Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: localMatch.players.map((player) { + return TextIconTile( + text: player.name, + suffixText: getNameCountText(player), + ); + }).toList(), + ) + : Text( + loc.no_players_available, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.textColor, + ), + ), ), ], const SizedBox(height: 15), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index c3c6b4e..b3d326b 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -221,7 +221,9 @@ class _MatchTileState extends State { const SizedBox(height: 12), ], - if (match.teams != null && match.teams!.isNotEmpty) ...[ + if (match.teams != null && + match.teams!.isNotEmpty && + match.isTeamMatch) ...[ // Team display Text( loc.teams, @@ -275,10 +277,17 @@ class _MatchTileState extends State { return TextIconTile( text: player.name, suffixText: getNameCountText(player), - iconEnabled: false, ); }).toList(), ), + ] else ...[ + Text( + loc.no_players_available, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.hintColor, + ), + ), ], ], ), -- 2.49.1