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