feat: basic integration of teams

This commit is contained in:
2026-05-17 21:29:16 +02:00
parent badf5ea311
commit a957408c7e
20 changed files with 1325 additions and 325 deletions

View File

@@ -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. /// Translates a [GameColor] enum value to its corresponding localized string.
String translateGameColorToString(GameColor color, BuildContext context) { String translateGameColorToString(GameColor color, BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);

View File

@@ -30,6 +30,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
gameId: match.game.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: match.name, name: match.name,
isTeamMatch: Value(match.isTeamMatch),
notes: match.notes, notes: match.notes,
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt), endedAt: Value(match.endedAt),
@@ -142,6 +143,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
gameId: match.game.id, gameId: match.game.id,
groupId: Value(match.group?.id), groupId: Value(match.group?.id),
name: match.name, name: match.name,
isTeamMatch: Value(match.isTeamMatch),
notes: match.notes, notes: match.notes,
createdAt: match.createdAt, createdAt: match.createdAt,
endedAt: Value(match.endedAt), endedAt: Value(match.endedAt),
@@ -300,6 +302,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
isTeamMatch: row.isTeamMatch,
notes: row.notes, notes: row.notes,
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,
@@ -334,6 +337,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
isTeamMatch: result.isTeamMatch,
notes: result.notes, notes: result.notes,
createdAt: result.createdAt, createdAt: result.createdAt,
endedAt: result.endedAt, endedAt: result.endedAt,
@@ -373,6 +377,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
isTeamMatch: row.isTeamMatch,
notes: row.notes, notes: row.notes,
createdAt: row.createdAt, createdAt: row.createdAt,
endedAt: row.endedAt, endedAt: row.endedAt,

View File

@@ -1,4 +1,5 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_match_table.dart'; import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/team_table.dart'; import 'package:tallee/data/db/tables/team_table.dart';
@@ -22,6 +23,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: team.id, id: team.id,
name: team.name, name: team.name,
createdAt: team.createdAt, createdAt: team.createdAt,
color: Value(team.color.name),
score: Value(team.score),
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrReplace,
); );
@@ -56,6 +59,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: team.id, id: team.id,
name: team.name, name: team.name,
createdAt: team.createdAt, createdAt: team.createdAt,
color: Value(team.color.name),
score: Value(team.score),
), ),
) )
.toList(), .toList(),
@@ -110,6 +115,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: row.id, id: row.id,
name: row.name, name: row.name,
createdAt: row.createdAt, createdAt: row.createdAt,
color: GameColor.values.byName(row.color),
score: row.score,
members: members, members: members,
); );
}), }),
@@ -125,6 +132,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: result.id, id: result.id,
name: result.name, name: result.name,
createdAt: result.createdAt, createdAt: result.createdAt,
color: GameColor.values.byName(result.color),
score: result.score,
members: members, members: members,
); );
} }
@@ -162,6 +171,30 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return rowsAffected > 0; return rowsAffected > 0;
} }
/// Updates the color of the team with the given [teamId].
Future<bool> 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<bool> 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 */ /* Delete */
/// Deletes all teams from the database. /// Deletes all teams from the database.

View File

@@ -1185,6 +1185,21 @@ class $MatchTableTable extends MatchTable
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: true, requiredDuringInsert: true,
); );
static const VerificationMeta _isTeamMatchMeta = const VerificationMeta(
'isTeamMatch',
);
@override
late final GeneratedColumn<bool> isTeamMatch = GeneratedColumn<bool>(
'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'); static const VerificationMeta _notesMeta = const VerificationMeta('notes');
@override @override
late final GeneratedColumn<String> notes = GeneratedColumn<String>( late final GeneratedColumn<String> notes = GeneratedColumn<String>(
@@ -1222,6 +1237,7 @@ class $MatchTableTable extends MatchTable
gameId, gameId,
groupId, groupId,
name, name,
isTeamMatch,
notes, notes,
createdAt, createdAt,
endedAt, endedAt,
@@ -1265,6 +1281,15 @@ class $MatchTableTable extends MatchTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_nameMeta); context.missing(_nameMeta);
} }
if (data.containsKey('is_team_match')) {
context.handle(
_isTeamMatchMeta,
isTeamMatch.isAcceptableOrUnknown(
data['is_team_match']!,
_isTeamMatchMeta,
),
);
}
if (data.containsKey('notes')) { if (data.containsKey('notes')) {
context.handle( context.handle(
_notesMeta, _notesMeta,
@@ -1312,6 +1337,10 @@ class $MatchTableTable extends MatchTable
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}name'], data['${effectivePrefix}name'],
)!, )!,
isTeamMatch: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}is_team_match'],
)!,
notes: attachedDatabase.typeMapping.read( notes: attachedDatabase.typeMapping.read(
DriftSqlType.string, DriftSqlType.string,
data['${effectivePrefix}notes'], data['${effectivePrefix}notes'],
@@ -1338,6 +1367,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
final String gameId; final String gameId;
final String? groupId; final String? groupId;
final String name; final String name;
final bool isTeamMatch;
final String notes; final String notes;
final DateTime createdAt; final DateTime createdAt;
final DateTime? endedAt; final DateTime? endedAt;
@@ -1346,6 +1376,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
required this.gameId, required this.gameId,
this.groupId, this.groupId,
required this.name, required this.name,
required this.isTeamMatch,
required this.notes, required this.notes,
required this.createdAt, required this.createdAt,
this.endedAt, this.endedAt,
@@ -1359,6 +1390,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
map['group_id'] = Variable<String>(groupId); map['group_id'] = Variable<String>(groupId);
} }
map['name'] = Variable<String>(name); map['name'] = Variable<String>(name);
map['is_team_match'] = Variable<bool>(isTeamMatch);
map['notes'] = Variable<String>(notes); map['notes'] = Variable<String>(notes);
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
if (!nullToAbsent || endedAt != null) { if (!nullToAbsent || endedAt != null) {
@@ -1375,6 +1407,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
? const Value.absent() ? const Value.absent()
: Value(groupId), : Value(groupId),
name: Value(name), name: Value(name),
isTeamMatch: Value(isTeamMatch),
notes: Value(notes), notes: Value(notes),
createdAt: Value(createdAt), createdAt: Value(createdAt),
endedAt: endedAt == null && nullToAbsent endedAt: endedAt == null && nullToAbsent
@@ -1393,6 +1426,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: serializer.fromJson<String>(json['gameId']), gameId: serializer.fromJson<String>(json['gameId']),
groupId: serializer.fromJson<String?>(json['groupId']), groupId: serializer.fromJson<String?>(json['groupId']),
name: serializer.fromJson<String>(json['name']), name: serializer.fromJson<String>(json['name']),
isTeamMatch: serializer.fromJson<bool>(json['isTeamMatch']),
notes: serializer.fromJson<String>(json['notes']), notes: serializer.fromJson<String>(json['notes']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
endedAt: serializer.fromJson<DateTime?>(json['endedAt']), endedAt: serializer.fromJson<DateTime?>(json['endedAt']),
@@ -1406,6 +1440,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
'gameId': serializer.toJson<String>(gameId), 'gameId': serializer.toJson<String>(gameId),
'groupId': serializer.toJson<String?>(groupId), 'groupId': serializer.toJson<String?>(groupId),
'name': serializer.toJson<String>(name), 'name': serializer.toJson<String>(name),
'isTeamMatch': serializer.toJson<bool>(isTeamMatch),
'notes': serializer.toJson<String>(notes), 'notes': serializer.toJson<String>(notes),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'endedAt': serializer.toJson<DateTime?>(endedAt), 'endedAt': serializer.toJson<DateTime?>(endedAt),
@@ -1417,6 +1452,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
String? gameId, String? gameId,
Value<String?> groupId = const Value.absent(), Value<String?> groupId = const Value.absent(),
String? name, String? name,
bool? isTeamMatch,
String? notes, String? notes,
DateTime? createdAt, DateTime? createdAt,
Value<DateTime?> endedAt = const Value.absent(), Value<DateTime?> endedAt = const Value.absent(),
@@ -1425,6 +1461,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: gameId ?? this.gameId, gameId: gameId ?? this.gameId,
groupId: groupId.present ? groupId.value : this.groupId, groupId: groupId.present ? groupId.value : this.groupId,
name: name ?? this.name, name: name ?? this.name,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
notes: notes ?? this.notes, notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
endedAt: endedAt.present ? endedAt.value : this.endedAt, endedAt: endedAt.present ? endedAt.value : this.endedAt,
@@ -1435,6 +1472,9 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: data.gameId.present ? data.gameId.value : this.gameId, gameId: data.gameId.present ? data.gameId.value : this.gameId,
groupId: data.groupId.present ? data.groupId.value : this.groupId, groupId: data.groupId.present ? data.groupId.value : this.groupId,
name: data.name.present ? data.name.value : this.name, 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, notes: data.notes.present ? data.notes.value : this.notes,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
endedAt: data.endedAt.present ? data.endedAt.value : this.endedAt, endedAt: data.endedAt.present ? data.endedAt.value : this.endedAt,
@@ -1448,6 +1488,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
..write('gameId: $gameId, ') ..write('gameId: $gameId, ')
..write('groupId: $groupId, ') ..write('groupId: $groupId, ')
..write('name: $name, ') ..write('name: $name, ')
..write('isTeamMatch: $isTeamMatch, ')
..write('notes: $notes, ') ..write('notes: $notes, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('endedAt: $endedAt') ..write('endedAt: $endedAt')
@@ -1456,8 +1497,16 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
} }
@override @override
int get hashCode => int get hashCode => Object.hash(
Object.hash(id, gameId, groupId, name, notes, createdAt, endedAt); id,
gameId,
groupId,
name,
isTeamMatch,
notes,
createdAt,
endedAt,
);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
@@ -1466,6 +1515,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
other.gameId == this.gameId && other.gameId == this.gameId &&
other.groupId == this.groupId && other.groupId == this.groupId &&
other.name == this.name && other.name == this.name &&
other.isTeamMatch == this.isTeamMatch &&
other.notes == this.notes && other.notes == this.notes &&
other.createdAt == this.createdAt && other.createdAt == this.createdAt &&
other.endedAt == this.endedAt); other.endedAt == this.endedAt);
@@ -1476,6 +1526,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
final Value<String> gameId; final Value<String> gameId;
final Value<String?> groupId; final Value<String?> groupId;
final Value<String> name; final Value<String> name;
final Value<bool> isTeamMatch;
final Value<String> notes; final Value<String> notes;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<DateTime?> endedAt; final Value<DateTime?> endedAt;
@@ -1485,6 +1536,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
this.gameId = const Value.absent(), this.gameId = const Value.absent(),
this.groupId = const Value.absent(), this.groupId = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
this.isTeamMatch = const Value.absent(),
this.notes = const Value.absent(), this.notes = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.endedAt = const Value.absent(), this.endedAt = const Value.absent(),
@@ -1495,6 +1547,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
required String gameId, required String gameId,
this.groupId = const Value.absent(), this.groupId = const Value.absent(),
required String name, required String name,
this.isTeamMatch = const Value.absent(),
required String notes, required String notes,
required DateTime createdAt, required DateTime createdAt,
this.endedAt = const Value.absent(), this.endedAt = const Value.absent(),
@@ -1509,6 +1562,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
Expression<String>? gameId, Expression<String>? gameId,
Expression<String>? groupId, Expression<String>? groupId,
Expression<String>? name, Expression<String>? name,
Expression<bool>? isTeamMatch,
Expression<String>? notes, Expression<String>? notes,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<DateTime>? endedAt, Expression<DateTime>? endedAt,
@@ -1519,6 +1573,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
if (gameId != null) 'game_id': gameId, if (gameId != null) 'game_id': gameId,
if (groupId != null) 'group_id': groupId, if (groupId != null) 'group_id': groupId,
if (name != null) 'name': name, if (name != null) 'name': name,
if (isTeamMatch != null) 'is_team_match': isTeamMatch,
if (notes != null) 'notes': notes, if (notes != null) 'notes': notes,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (endedAt != null) 'ended_at': endedAt, if (endedAt != null) 'ended_at': endedAt,
@@ -1531,6 +1586,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
Value<String>? gameId, Value<String>? gameId,
Value<String?>? groupId, Value<String?>? groupId,
Value<String>? name, Value<String>? name,
Value<bool>? isTeamMatch,
Value<String>? notes, Value<String>? notes,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<DateTime?>? endedAt, Value<DateTime?>? endedAt,
@@ -1541,6 +1597,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
gameId: gameId ?? this.gameId, gameId: gameId ?? this.gameId,
groupId: groupId ?? this.groupId, groupId: groupId ?? this.groupId,
name: name ?? this.name, name: name ?? this.name,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
notes: notes ?? this.notes, notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
endedAt: endedAt ?? this.endedAt, endedAt: endedAt ?? this.endedAt,
@@ -1563,6 +1620,9 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
if (name.present) { if (name.present) {
map['name'] = Variable<String>(name.value); map['name'] = Variable<String>(name.value);
} }
if (isTeamMatch.present) {
map['is_team_match'] = Variable<bool>(isTeamMatch.value);
}
if (notes.present) { if (notes.present) {
map['notes'] = Variable<String>(notes.value); map['notes'] = Variable<String>(notes.value);
} }
@@ -1585,6 +1645,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
..write('gameId: $gameId, ') ..write('gameId: $gameId, ')
..write('groupId: $groupId, ') ..write('groupId: $groupId, ')
..write('name: $name, ') ..write('name: $name, ')
..write('isTeamMatch: $isTeamMatch, ')
..write('notes: $notes, ') ..write('notes: $notes, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('endedAt: $endedAt, ') ..write('endedAt: $endedAt, ')
@@ -1854,8 +1915,28 @@ class $TeamTableTable extends TeamTable
type: DriftSqlType.dateTime, type: DriftSqlType.dateTime,
requiredDuringInsert: true, requiredDuringInsert: true,
); );
static const VerificationMeta _colorMeta = const VerificationMeta('color');
@override @override
List<GeneratedColumn> get $columns => [id, name, createdAt]; late final GeneratedColumn<String> color = GeneratedColumn<String>(
'color',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant('blue'),
);
static const VerificationMeta _scoreMeta = const VerificationMeta('score');
@override
late final GeneratedColumn<int> score = GeneratedColumn<int>(
'score',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0),
);
@override
List<GeneratedColumn> get $columns => [id, name, createdAt, color, score];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@@ -1889,6 +1970,18 @@ class $TeamTableTable extends TeamTable
} else if (isInserting) { } else if (isInserting) {
context.missing(_createdAtMeta); 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; return context;
} }
@@ -1910,6 +2003,14 @@ class $TeamTableTable extends TeamTable
DriftSqlType.dateTime, DriftSqlType.dateTime,
data['${effectivePrefix}created_at'], 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<TeamTableData> {
final String id; final String id;
final String name; final String name;
final DateTime createdAt; final DateTime createdAt;
final String color;
final int score;
const TeamTableData({ const TeamTableData({
required this.id, required this.id,
required this.name, required this.name,
required this.createdAt, required this.createdAt,
required this.color,
required this.score,
}); });
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -1934,6 +2039,8 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
map['id'] = Variable<String>(id); map['id'] = Variable<String>(id);
map['name'] = Variable<String>(name); map['name'] = Variable<String>(name);
map['created_at'] = Variable<DateTime>(createdAt); map['created_at'] = Variable<DateTime>(createdAt);
map['color'] = Variable<String>(color);
map['score'] = Variable<int>(score);
return map; return map;
} }
@@ -1942,6 +2049,8 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
id: Value(id), id: Value(id),
name: Value(name), name: Value(name),
createdAt: Value(createdAt), createdAt: Value(createdAt),
color: Value(color),
score: Value(score),
); );
} }
@@ -1954,6 +2063,8 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
id: serializer.fromJson<String>(json['id']), id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']), name: serializer.fromJson<String>(json['name']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']), createdAt: serializer.fromJson<DateTime>(json['createdAt']),
color: serializer.fromJson<String>(json['color']),
score: serializer.fromJson<int>(json['score']),
); );
} }
@override @override
@@ -1963,20 +2074,31 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
'id': serializer.toJson<String>(id), 'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name), 'name': serializer.toJson<String>(name),
'createdAt': serializer.toJson<DateTime>(createdAt), 'createdAt': serializer.toJson<DateTime>(createdAt),
'color': serializer.toJson<String>(color),
'score': serializer.toJson<int>(score),
}; };
} }
TeamTableData copyWith({String? id, String? name, DateTime? createdAt}) => TeamTableData copyWith({
TeamTableData( String? id,
id: id ?? this.id, String? name,
name: name ?? this.name, DateTime? createdAt,
createdAt: createdAt ?? this.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) { TeamTableData copyWithCompanion(TeamTableCompanion data) {
return TeamTableData( return TeamTableData(
id: data.id.present ? data.id.value : this.id, id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name, name: data.name.present ? data.name.value : this.name,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, 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<TeamTableData> {
return (StringBuffer('TeamTableData(') return (StringBuffer('TeamTableData(')
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('createdAt: $createdAt') ..write('createdAt: $createdAt, ')
..write('color: $color, ')
..write('score: $score')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(id, name, createdAt); int get hashCode => Object.hash(id, name, createdAt, color, score);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is TeamTableData && (other is TeamTableData &&
other.id == this.id && other.id == this.id &&
other.name == this.name && other.name == this.name &&
other.createdAt == this.createdAt); other.createdAt == this.createdAt &&
other.color == this.color &&
other.score == this.score);
} }
class TeamTableCompanion extends UpdateCompanion<TeamTableData> { class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
final Value<String> id; final Value<String> id;
final Value<String> name; final Value<String> name;
final Value<DateTime> createdAt; final Value<DateTime> createdAt;
final Value<String> color;
final Value<int> score;
final Value<int> rowid; final Value<int> rowid;
const TeamTableCompanion({ const TeamTableCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
this.createdAt = const Value.absent(), this.createdAt = const Value.absent(),
this.color = const Value.absent(),
this.score = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
TeamTableCompanion.insert({ TeamTableCompanion.insert({
required String id, required String id,
required String name, required String name,
required DateTime createdAt, required DateTime createdAt,
this.color = const Value.absent(),
this.score = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : id = Value(id), }) : id = Value(id),
name = Value(name), name = Value(name),
@@ -2024,12 +2156,16 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
Expression<String>? id, Expression<String>? id,
Expression<String>? name, Expression<String>? name,
Expression<DateTime>? createdAt, Expression<DateTime>? createdAt,
Expression<String>? color,
Expression<int>? score,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (name != null) 'name': name, if (name != null) 'name': name,
if (createdAt != null) 'created_at': createdAt, if (createdAt != null) 'created_at': createdAt,
if (color != null) 'color': color,
if (score != null) 'score': score,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
@@ -2038,12 +2174,16 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
Value<String>? id, Value<String>? id,
Value<String>? name, Value<String>? name,
Value<DateTime>? createdAt, Value<DateTime>? createdAt,
Value<String>? color,
Value<int>? score,
Value<int>? rowid, Value<int>? rowid,
}) { }) {
return TeamTableCompanion( return TeamTableCompanion(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score ?? this.score,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@@ -2060,6 +2200,12 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
if (createdAt.present) { if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value); map['created_at'] = Variable<DateTime>(createdAt.value);
} }
if (color.present) {
map['color'] = Variable<String>(color.value);
}
if (score.present) {
map['score'] = Variable<int>(score.value);
}
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
} }
@@ -2072,6 +2218,8 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
..write('id: $id, ') ..write('id: $id, ')
..write('name: $name, ') ..write('name: $name, ')
..write('createdAt: $createdAt, ') ..write('createdAt: $createdAt, ')
..write('color: $color, ')
..write('score: $score, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@@ -4092,6 +4240,7 @@ typedef $$MatchTableTableCreateCompanionBuilder =
required String gameId, required String gameId,
Value<String?> groupId, Value<String?> groupId,
required String name, required String name,
Value<bool> isTeamMatch,
required String notes, required String notes,
required DateTime createdAt, required DateTime createdAt,
Value<DateTime?> endedAt, Value<DateTime?> endedAt,
@@ -4103,6 +4252,7 @@ typedef $$MatchTableTableUpdateCompanionBuilder =
Value<String> gameId, Value<String> gameId,
Value<String?> groupId, Value<String?> groupId,
Value<String> name, Value<String> name,
Value<bool> isTeamMatch,
Value<String> notes, Value<String> notes,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<DateTime?> endedAt, Value<DateTime?> endedAt,
@@ -4215,6 +4365,11 @@ class $$MatchTableTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get notes => $composableBuilder( ColumnFilters<String> get notes => $composableBuilder(
column: $table.notes, column: $table.notes,
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
@@ -4346,6 +4501,11 @@ class $$MatchTableTableOrderingComposer
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get notes => $composableBuilder( ColumnOrderings<String> get notes => $composableBuilder(
column: $table.notes, column: $table.notes,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
@@ -4423,6 +4583,11 @@ class $$MatchTableTableAnnotationComposer
GeneratedColumn<String> get name => GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column); $composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumn<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => column,
);
GeneratedColumn<String> get notes => GeneratedColumn<String> get notes =>
$composableBuilder(column: $table.notes, builder: (column) => column); $composableBuilder(column: $table.notes, builder: (column) => column);
@@ -4566,6 +4731,7 @@ class $$MatchTableTableTableManager
Value<String> gameId = const Value.absent(), Value<String> gameId = const Value.absent(),
Value<String?> groupId = const Value.absent(), Value<String?> groupId = const Value.absent(),
Value<String> name = const Value.absent(), Value<String> name = const Value.absent(),
Value<bool> isTeamMatch = const Value.absent(),
Value<String> notes = const Value.absent(), Value<String> notes = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<DateTime?> endedAt = const Value.absent(), Value<DateTime?> endedAt = const Value.absent(),
@@ -4575,6 +4741,7 @@ class $$MatchTableTableTableManager
gameId: gameId, gameId: gameId,
groupId: groupId, groupId: groupId,
name: name, name: name,
isTeamMatch: isTeamMatch,
notes: notes, notes: notes,
createdAt: createdAt, createdAt: createdAt,
endedAt: endedAt, endedAt: endedAt,
@@ -4586,6 +4753,7 @@ class $$MatchTableTableTableManager
required String gameId, required String gameId,
Value<String?> groupId = const Value.absent(), Value<String?> groupId = const Value.absent(),
required String name, required String name,
Value<bool> isTeamMatch = const Value.absent(),
required String notes, required String notes,
required DateTime createdAt, required DateTime createdAt,
Value<DateTime?> endedAt = const Value.absent(), Value<DateTime?> endedAt = const Value.absent(),
@@ -4595,6 +4763,7 @@ class $$MatchTableTableTableManager
gameId: gameId, gameId: gameId,
groupId: groupId, groupId: groupId,
name: name, name: name,
isTeamMatch: isTeamMatch,
notes: notes, notes: notes,
createdAt: createdAt, createdAt: createdAt,
endedAt: endedAt, endedAt: endedAt,
@@ -5109,6 +5278,8 @@ typedef $$TeamTableTableCreateCompanionBuilder =
required String id, required String id,
required String name, required String name,
required DateTime createdAt, required DateTime createdAt,
Value<String> color,
Value<int> score,
Value<int> rowid, Value<int> rowid,
}); });
typedef $$TeamTableTableUpdateCompanionBuilder = typedef $$TeamTableTableUpdateCompanionBuilder =
@@ -5116,6 +5287,8 @@ typedef $$TeamTableTableUpdateCompanionBuilder =
Value<String> id, Value<String> id,
Value<String> name, Value<String> name,
Value<DateTime> createdAt, Value<DateTime> createdAt,
Value<String> color,
Value<int> score,
Value<int> rowid, Value<int> rowid,
}); });
@@ -5171,6 +5344,16 @@ class $$TeamTableTableFilterComposer
builder: (column) => ColumnFilters(column), builder: (column) => ColumnFilters(column),
); );
ColumnFilters<String> get color => $composableBuilder(
column: $table.color,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get score => $composableBuilder(
column: $table.score,
builder: (column) => ColumnFilters(column),
);
Expression<bool> playerMatchTableRefs( Expression<bool> playerMatchTableRefs(
Expression<bool> Function($$PlayerMatchTableTableFilterComposer f) f, Expression<bool> Function($$PlayerMatchTableTableFilterComposer f) f,
) { ) {
@@ -5220,6 +5403,16 @@ class $$TeamTableTableOrderingComposer
column: $table.createdAt, column: $table.createdAt,
builder: (column) => ColumnOrderings(column), builder: (column) => ColumnOrderings(column),
); );
ColumnOrderings<String> get color => $composableBuilder(
column: $table.color,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get score => $composableBuilder(
column: $table.score,
builder: (column) => ColumnOrderings(column),
);
} }
class $$TeamTableTableAnnotationComposer class $$TeamTableTableAnnotationComposer
@@ -5240,6 +5433,12 @@ class $$TeamTableTableAnnotationComposer
GeneratedColumn<DateTime> get createdAt => GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column); $composableBuilder(column: $table.createdAt, builder: (column) => column);
GeneratedColumn<String> get color =>
$composableBuilder(column: $table.color, builder: (column) => column);
GeneratedColumn<int> get score =>
$composableBuilder(column: $table.score, builder: (column) => column);
Expression<T> playerMatchTableRefs<T extends Object>( Expression<T> playerMatchTableRefs<T extends Object>(
Expression<T> Function($$PlayerMatchTableTableAnnotationComposer a) f, Expression<T> Function($$PlayerMatchTableTableAnnotationComposer a) f,
) { ) {
@@ -5297,11 +5496,15 @@ class $$TeamTableTableTableManager
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
Value<String> name = const Value.absent(), Value<String> name = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(), Value<DateTime> createdAt = const Value.absent(),
Value<String> color = const Value.absent(),
Value<int> score = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => TeamTableCompanion( }) => TeamTableCompanion(
id: id, id: id,
name: name, name: name,
createdAt: createdAt, createdAt: createdAt,
color: color,
score: score,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: createCompanionCallback:
@@ -5309,11 +5512,15 @@ class $$TeamTableTableTableManager
required String id, required String id,
required String name, required String name,
required DateTime createdAt, required DateTime createdAt,
Value<String> color = const Value.absent(),
Value<int> score = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => TeamTableCompanion.insert( }) => TeamTableCompanion.insert(
id: id, id: id,
name: name, name: name,
createdAt: createdAt, createdAt: createdAt,
color: color,
score: score,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0

View File

@@ -12,6 +12,7 @@ class MatchTable extends Table {
.references(GroupTable, #id, onDelete: KeyAction.setNull) .references(GroupTable, #id, onDelete: KeyAction.setNull)
.nullable()(); .nullable()();
TextColumn get name => text()(); TextColumn get name => text()();
BoolColumn get isTeamMatch => boolean().withDefault(const Constant(false))();
TextColumn get notes => text()(); TextColumn get notes => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
DateTimeColumn get endedAt => dateTime().nullable()(); DateTimeColumn get endedAt => dateTime().nullable()();

View File

@@ -4,6 +4,8 @@ class TeamTable extends Table {
TextColumn get id => text()(); TextColumn get id => text()();
TextColumn get name => text()(); TextColumn get name => text()();
DateTimeColumn get createdAt => dateTime()(); DateTimeColumn get createdAt => dateTime()();
TextColumn get color => text().withDefault(const Constant('blue'))();
IntColumn get score => integer().withDefault(const Constant(0))();
@override @override
Set<Column<Object>> get primaryKey => {id}; Set<Column<Object>> get primaryKey => {id};

View File

@@ -16,6 +16,7 @@ class Match {
final Game game; final Game game;
final Group? group; final Group? group;
final List<Player> players; final List<Player> players;
final bool isTeamMatch;
final List<Team>? teams; final List<Team>? teams;
final String notes; final String notes;
Map<String, ScoreEntry?> scores; Map<String, ScoreEntry?> scores;
@@ -26,6 +27,7 @@ class Match {
required this.players, required this.players,
this.endedAt, this.endedAt,
this.group, this.group,
this.isTeamMatch = false,
this.teams, this.teams,
this.notes = '', this.notes = '',
String? id, String? id,
@@ -37,7 +39,7 @@ class Match {
@override @override
String toString() { 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({ Match copyWith({
@@ -48,6 +50,7 @@ class Match {
Game? game, Game? game,
Group? group, Group? group,
List<Player>? players, List<Player>? players,
bool? isTeamMatch,
List<Team>? teams, List<Team>? teams,
String? notes, String? notes,
Map<String, ScoreEntry?>? scores, Map<String, ScoreEntry?>? scores,
@@ -60,6 +63,7 @@ class Match {
game: game ?? this.game, game: game ?? this.game,
group: group ?? this.group, group: group ?? this.group,
players: players ?? this.players, players: players ?? this.players,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
teams: teams ?? this.teams, teams: teams ?? this.teams,
notes: notes ?? this.notes, notes: notes ?? this.notes,
scores: scores ?? this.scores, scores: scores ?? this.scores,
@@ -78,6 +82,7 @@ class Match {
game == other.game && game == other.game &&
group == other.group && group == other.group &&
const DeepCollectionEquality().equals(players, other.players) && const DeepCollectionEquality().equals(players, other.players) &&
isTeamMatch == other.isTeamMatch &&
const DeepCollectionEquality().equals(teams, other.teams) && const DeepCollectionEquality().equals(teams, other.teams) &&
notes == other.notes && notes == other.notes &&
const DeepCollectionEquality().equals(scores, other.scores); const DeepCollectionEquality().equals(scores, other.scores);
@@ -91,6 +96,7 @@ class Match {
game, game,
group, group,
const DeepCollectionEquality().hash(players), const DeepCollectionEquality().hash(players),
isTeamMatch,
const DeepCollectionEquality().hash(teams), const DeepCollectionEquality().hash(teams),
notes, notes,
const DeepCollectionEquality().hash(scores), const DeepCollectionEquality().hash(scores),
@@ -112,6 +118,7 @@ class Match {
), ),
group = null, group = null,
players = [], players = [],
isTeamMatch = json['isTeamMatch'],
teams = [], teams = [],
scores = json['scores'] != null scores = json['scores'] != null
? (json['scores'] as Map<String, dynamic>).map( ? (json['scores'] as Map<String, dynamic>).map(
@@ -133,11 +140,13 @@ class Match {
'gameId': game.id, 'gameId': game.id,
'groupId': group?.id, 'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(), 'playerIds': players.map((player) => player.id).toList(),
'isTeamMatch': isTeamMatch,
'teams': teams?.map((team) => team.toJson()).toList(), 'teams': teams?.map((team) => team.toJson()).toList(),
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())), 'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
'notes': notes, 'notes': notes,
}; };
// Most Valuable Player(s) based on the match's ruleset
List<Player> get mvp { List<Player> get mvp {
if (players.isEmpty || scores.isEmpty) return []; if (players.isEmpty || scores.isEmpty) return [];
@@ -195,4 +204,49 @@ class Match {
return playerScore.score == lowestScore; return playerScore.score == lowestScore;
}).toList(); }).toList();
} }
// MVP for team-based matches (Most Valuable Team)
List<Team> 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<Team> _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<Team> _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();
}
} }

View File

@@ -1,5 +1,6 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
@@ -7,31 +8,39 @@ class Team {
final String id; final String id;
final String name; final String name;
final DateTime createdAt; final DateTime createdAt;
final GameColor color;
final int score;
final List<Player> members; final List<Player> members;
Team({ Team({
String? id, String? id,
required this.name, required this.name,
DateTime? createdAt, DateTime? createdAt,
this.color = GameColor.blue,
this.score = 0,
required this.members, required this.members,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(); createdAt = createdAt ?? clock.now();
@override @override
String toString() { String toString() {
return 'Team{id: $id, name: $name, members: $members}'; return 'Team{id: $id, name: $name, color: $color, score: $score, members: $members}';
} }
Team copyWith({ Team copyWith({
String? id, String? id,
String? name, String? name,
DateTime? createdAt, DateTime? createdAt,
GameColor? color,
int? score,
List<Player>? members, List<Player>? members,
}) { }) {
return Team( return Team(
id: id ?? this.id, id: id ?? this.id,
name: name ?? this.name, name: name ?? this.name,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score ?? this.score,
members: members ?? this.members, members: members ?? this.members,
); );
} }
@@ -44,6 +53,8 @@ class Team {
id == other.id && id == other.id &&
name == other.name && name == other.name &&
createdAt == other.createdAt && createdAt == other.createdAt &&
color == other.color &&
score == other.score &&
const DeepCollectionEquality().equals(members, other.members); const DeepCollectionEquality().equals(members, other.members);
@override @override
@@ -51,6 +62,8 @@ class Team {
id, id,
name, name,
createdAt, createdAt,
color,
score,
const DeepCollectionEquality().hash(members), const DeepCollectionEquality().hash(members),
); );
@@ -58,12 +71,16 @@ class Team {
: id = json['id'], : id = json['id'],
name = json['name'], name = json['name'],
createdAt = DateTime.parse(json['createdAt']), createdAt = DateTime.parse(json['createdAt']),
color = GameColor.values.byName(json['color'] ?? GameColor.blue.name),
score = json['score'] ?? 0,
members = []; // Populated during import via DataTransferService members = []; // Populated during import via DataTransferService
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'id': id, 'id': id,
'name': name, 'name': name,
'createdAt': createdAt.toIso8601String(), 'createdAt': createdAt.toIso8601String(),
'color': color.name,
'score': score,
'memberIds': members.map((member) => member.id).toList(), 'memberIds': members.map((member) => member.id).toList(),
}; };
} }

View File

@@ -12,6 +12,7 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.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_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/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/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/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart'; import 'package:tallee/presentation/widgets/player_selection.dart';
@@ -59,6 +60,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
Group? selectedGroup; Group? selectedGroup;
Game? selectedGame; Game? selectedGame;
bool isTeamMatch = false;
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
/// GlobalKey for ScaffoldMessenger to show snackbars /// GlobalKey for ScaffoldMessenger to show snackbars
@@ -135,24 +137,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
trailing: selectedGame == null trailing: selectedGame == null
? Text(loc.none_group) ? Text(loc.none_group)
: Text(selectedGame!.name), : Text(selectedGame!.name),
onPressed: () async { onPressed: () async => await onChoosingGame(),
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;
}
});
},
), ),
// Group selection tile. // Group selection tile.
@@ -161,37 +146,20 @@ class _CreateMatchViewState extends State<CreateMatchView> {
trailing: selectedGroup == null trailing: selectedGroup == null
? Text(loc.none_group) ? Text(loc.none_group)
: Text(selectedGroup!.name), : Text(selectedGroup!.name),
onPressed: () async { onPressed: () async => onChoosingGroup(),
// 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 (!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. // Player selection widget.
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
@@ -211,9 +179,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
text: buttonText, text: buttonText,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton() onPressed: isSubmitButtonEnabled()
? () { ? () {
buttonNavigation(context); submitButtonNavigation(context);
} }
: null, : null,
), ),
@@ -228,12 +196,86 @@ class _CreateMatchViewState extends State<CreateMatchView> {
return widget.matchToEdit != null; 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<void> 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<void> 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<void> 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. /// Determines whether the "Create Match" button should be enabled.
/// ///
/// Returns `true` if: /// Returns `true` if:
/// - A ruleset is selected AND /// - A ruleset is selected AND
/// - Either a group is selected OR at least 2 players are selected. /// - Either a group is selected OR at least 2 players are selected.
bool _enableCreateGameButton() { bool isSubmitButtonEnabled() {
return (selectedGroup != null || return (selectedGroup != null ||
(selectedPlayers.length > 1) && selectedGame != null); (selectedPlayers.length > 1) && selectedGame != null);
} }
@@ -242,20 +284,32 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// ///
/// If a match is being edited, updates the match in the database. /// If a match is being edited, updates the match in the database.
/// Otherwise, creates a new match and navigates to the MatchResultView. /// Otherwise, creates a new match and navigates to the MatchResultView.
void buttonNavigation(BuildContext context) async { void submitButtonNavigation(BuildContext context) async {
if (isEditMode()) { if (isEditMode()) {
await updateMatch(); await updateMatch();
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
} }
} else { }
final match = await createMatch();
final match = await createMatch();
if (isTeamMatch) {
if (context.mounted) { if (context.mounted) {
Navigator.pushReplacement( Navigator.pushReplacement(
context, context,
adaptivePageRoute( adaptivePageRoute(
fullscreenDialog: true, fullscreenDialog: !isTeamMatch,
builder: (context) => OrganizeTeamsView(match: match),
),
);
}
} else {
if (context.mounted) {
Navigator.pushReplacement(
context,
adaptivePageRoute(
fullscreenDialog: !isTeamMatch,
builder: (context) => MatchResultView( builder: (context) => MatchResultView(
match: match, match: match,
onWinnerChanged: widget.onWinnerChanged, onWinnerChanged: widget.onWinnerChanged,
@@ -328,36 +382,10 @@ class _CreateMatchViewState extends State<CreateMatchView> {
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
isTeamMatch: isTeamMatch,
game: selectedGame!, game: selectedGame!,
); );
await db.matchDao.addMatch(match: match); await db.matchDao.addMatch(match: match);
return 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<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return;
if (!selectedPlayers.any(
(player) =>
selectedGroup!.members.any((member) => member.id == player.id),
)) {
setState(() {
selectedGroup = null;
});
}
}
} }

View File

@@ -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<OrganizeTeamsView> createState() => _OrganizeTeamsViewState();
}
class _OrganizeTeamsViewState extends State<OrganizeTeamsView> {
final Random _random = Random();
late final List<_TeamDraft> _teams;
List<Player> 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<Match> 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<AppDatabase>(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<Player> members = [];
void dispose() {
nameController.dispose();
}
}

View File

@@ -1,11 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.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/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.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'; import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart';
@@ -36,8 +38,8 @@ class _MatchResultViewState extends State<MatchResultView> {
late final Ruleset ruleset; late final Ruleset ruleset;
/// List of all players who participated in the match
late final List<Player> allPlayers; late final List<Player> allPlayers;
late final List<Team> allTeams;
/// List of text controllers for score entry, one for each player /// List of text controllers for score entry, one for each player
late final List<TextEditingController> controller; late final List<TextEditingController> controller;
@@ -45,44 +47,27 @@ class _MatchResultViewState extends State<MatchResultView> {
/// Flag to indicate if the save button should be enabled /// Flag to indicate if the save button should be enabled
late bool canSave; late bool canSave;
late bool isTeamMatch;
/// Currently selected winner player /// Currently selected winner player
Player? _selectedPlayer; Player? _selectedPlayer;
Team? _selectedTeam;
@override @override
void initState() { void initState() {
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
ruleset = widget.match.game.ruleset; ruleset = widget.match.game.ruleset;
canSave = !rulesetSupportsScoreEntry(); canSave = !rulesetSupportsScoreEntry();
isTeamMatch = widget.match.isTeamMatch;
print(widget.match.teams);
allPlayers = widget.match.players; if (isTeamMatch) {
allPlayers.sort((a, b) => a.name.compareTo(b.name)); initializeAsTeamMatch();
} else {
controller = List.generate( inizializeAsNormalMatch();
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();
} }
super.initState();
} }
@override @override
@@ -160,165 +145,16 @@ class _MatchResultViewState extends State<MatchResultView> {
// Show player selection // Show player selection
if (rulesetSupportsWinnerSelection()) if (rulesetSupportsWinnerSelection())
Expanded( Expanded(
child: RadioGroup<Player>( child: buildWinnerSelectionWidget(isTeamMatch),
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);
}
});
},
);
},
),
),
), ),
// Show score entry // Show score entry
if (rulesetSupportsScoreEntry()) if (rulesetSupportsScoreEntry())
Expanded( Expanded(child: buildScoreEntryWidget(isTeamMatch)),
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),
);
},
),
),
// Show draggable placement list // Show draggable placement list
if (rulesetSupportsPlacement()) if (rulesetSupportsPlacement())
Expanded( Expanded(child: buildPlacementWidget(isTeamMatch)),
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,
);
},
),
),
],
),
),
], ],
), ),
), ),
@@ -361,6 +197,63 @@ class _MatchResultViewState extends State<MatchResultView> {
); );
} }
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. /// Updated [canSave] everytime a text is entered in one of the score entry fields.
void onTextEnter() { void onTextEnter() {
if (rulesetSupportsScoreEntry()) { if (rulesetSupportsScoreEntry()) {
@@ -459,4 +352,311 @@ class _MatchResultViewState extends State<MatchResultView> {
bool rulesetSupportsPlacement() { bool rulesetSupportsPlacement() {
return ruleset == Ruleset.placement; 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<Team>(
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<Player>(
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]);
}
} }

View File

@@ -16,7 +16,7 @@ class MainMenuButton extends StatefulWidget {
}); });
/// The callback to be invoked when the button is pressed. /// The callback to be invoked when the button is pressed.
final void Function() onPressed; final void Function()? onPressed;
/// The icon of the button. /// The icon of the button.
final IconData icon; final IconData icon;
@@ -31,9 +31,11 @@ class MainMenuButton extends StatefulWidget {
} }
class _MainMenuButtonState extends State<MainMenuButton> class _MainMenuButtonState extends State<MainMenuButton>
with SingleTickerProviderStateMixin { with TickerProviderStateMixin {
late AnimationController _animationController; late AnimationController _animationController;
late AnimationController _disabledAnimationController;
late Animation<double> _scaleAnimation; late Animation<double> _scaleAnimation;
late Animation<double> _disabledScaleAnimation;
/// How long the button needs to be pressed to register it as long press /// How long the button needs to be pressed to register it as long press
Timer? _longPressTimer; Timer? _longPressTimer;
@@ -52,37 +54,59 @@ class _MainMenuButtonState extends State<MainMenuButton>
vsync: this, vsync: this,
); );
_disabledAnimationController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate( _scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
); );
_disabledScaleAnimation = Tween<double>(begin: 1.0, end: 0.98).animate(
CurvedAnimation(
parent: _disabledAnimationController,
curve: Curves.easeInOut,
),
);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ScaleTransition( return ScaleTransition(
scale: _scaleAnimation, scale: widget.onPressed == null
? _disabledScaleAnimation
: _scaleAnimation,
child: GestureDetector( child: GestureDetector(
onTapDown: (_) { onTapDown: (_) {
_animationController.forward(); if (widget.onPressed == null) {
if (widget.onLongPressed != null) { _disabledAnimationController.forward();
_longPressTimer = Timer(const Duration(milliseconds: 400), () { } else {
_isLongPressing = true; _animationController.forward();
widget.onLongPressed?.call(); if (widget.onLongPressed != null) {
_repeatTimer = Timer.periodic( _longPressTimer = Timer(const Duration(milliseconds: 400), () {
const Duration(milliseconds: 250), _isLongPressing = true;
(_) => widget.onLongPressed?.call(), widget.onLongPressed?.call();
); _repeatTimer = Timer.periodic(
}); const Duration(milliseconds: 250),
(_) => widget.onLongPressed?.call(),
);
});
}
} }
}, },
onTapUp: (_) async { onTapUp: (_) async {
_cancelTimers(); if (widget.onPressed == null) {
if (mounted && !_isLongPressing) { _disabledAnimationController.reverse();
widget.onPressed(); } 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: () { onTapCancel: () {
_isLongPressing = false; _isLongPressing = false;
@@ -91,7 +115,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
}, },
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: widget.onPressed == null ? Colors.grey : Colors.white,
borderRadius: BorderRadius.circular(30), borderRadius: BorderRadius.circular(30),
), ),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
@@ -122,6 +146,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
void dispose() { void dispose() {
_cancelTimers(); _cancelTimers();
_animationController.dispose(); _animationController.dispose();
_disabledAnimationController.dispose();
super.dispose(); super.dispose();
} }

View File

@@ -57,7 +57,6 @@ class TextInputField extends StatelessWidget {
filled: true, filled: true,
fillColor: CustomTheme.boxColor, fillColor: CustomTheme.boxColor,
hintText: hintText, hintText: hintText,
hintStyle: const TextStyle(fontSize: 18),
counterText: showCounterText ? null : '', counterText: showCounterText ? null : '',
enabledBorder: const OutlineInputBorder( enabledBorder: const OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(12)), borderRadius: BorderRadius.all(Radius.circular(12)),

View File

@@ -8,13 +8,13 @@ class CustomRadioListTile<T> extends StatelessWidget {
/// - [onContainerTap]: The callback invoked when the container is tapped. /// - [onContainerTap]: The callback invoked when the container is tapped.
const CustomRadioListTile({ const CustomRadioListTile({
super.key, super.key,
required this.text, required this.content,
required this.value, required this.value,
required this.onContainerTap, required this.onContainerTap,
}); });
/// The text to display next to the radio button. /// The text to display next to the radio button.
final String text; final Widget content;
/// The value associated with the radio button. /// The value associated with the radio button.
final T value; final T value;
@@ -37,16 +37,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
child: Row( child: Row(
children: [ children: [
Radio<T>(value: value, toggleable: true), Radio<T>(value: value, toggleable: true),
Expanded( Expanded(child: content),
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
], ],
), ),
), ),

View File

@@ -5,36 +5,34 @@ import 'package:tallee/l10n/generated/app_localizations.dart';
class ScoreListTile extends StatelessWidget { class ScoreListTile extends StatelessWidget {
/// A custom list tile widget that has a text field for inputting a score. /// 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. /// - [controller]: The controller for the text field to input the score.
const ScoreListTile({ const ScoreListTile({
super.key, super.key,
required this.text, required this.content,
required this.controller, required this.controller,
this.horizontalPadding = 20,
}); });
/// The text to display next to the radio button. final Widget content;
final String text;
final TextEditingController controller; final TextEditingController controller;
final double horizontalPadding;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), 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), decoration: const BoxDecoration(color: CustomTheme.boxColor),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text( content,
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
SizedBox( SizedBox(
width: 100, width: 100,
height: 40, height: 40,

View File

@@ -128,6 +128,12 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12), 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 // Winner / In Progress Info
if (match.mvp.isNotEmpty) ...[ if (match.mvp.isNotEmpty) ...[
Container( Container(

View File

@@ -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<Player> players;
final TextEditingController controller;
final String hintText;
final VoidCallback? onDelete;
final ValueChanged<GameColor>? onColorSelection;
final void Function(Player player)? onPlayerTap;
@override
State<TeamCreationTile> createState() => _TeamCreationTileState();
}
class _TeamCreationTileState extends State<TeamCreationTile> {
@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(),
),
],
),
);
}
}

View File

@@ -11,6 +11,7 @@ class TextIconListTile extends StatelessWidget {
required this.text, required this.text,
this.suffixText = '', this.suffixText = '',
this.icon, this.icon,
this.color,
this.onPressed, this.onPressed,
}); });
@@ -23,6 +24,8 @@ class TextIconListTile extends StatelessWidget {
/// The icon to display in the tile. /// The icon to display in the tile.
final IconData? icon; final IconData? icon;
final Color? color;
/// The callback to be invoked when the icon is pressed. /// The callback to be invoked when the icon is pressed.
final VoidCallback? onPressed; final VoidCallback? onPressed;
@@ -31,7 +34,17 @@ class TextIconListTile extends StatelessWidget {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 15), 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( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,

View File

@@ -6,12 +6,14 @@ class TextIconTile extends StatelessWidget {
/// - [text]: The text to display in the tile. /// - [text]: The text to display in the tile.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed. /// - [iconEnabled]: A boolean to determine if the icon should be displayed.
/// - [onIconTap]: The callback to be invoked when the icon is tapped. /// - [onIconTap]: The callback to be invoked when the icon is tapped.
/// - [icon]: Optional custom icon. Defaults to [Icons.close].
const TextIconTile({ const TextIconTile({
super.key, super.key,
required this.text, required this.text,
this.suffixText = '', this.suffixText = '',
this.iconEnabled = true, this.iconEnabled = true,
this.onIconTap, this.onIconTap,
this.icon = Icons.close,
}); });
/// The text to display in the tile. /// 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. /// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap; final VoidCallback? onIconTap;
/// The icon to display. Defaults to [Icons.close].
final IconData icon;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@@ -65,10 +70,7 @@ class TextIconTile extends StatelessWidget {
), ),
if (iconEnabled) ...<Widget>[ if (iconEnabled) ...<Widget>[
const SizedBox(width: 3), const SizedBox(width: 3),
GestureDetector( GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
], ],
], ],
), ),

View File

@@ -1,7 +1,7 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.30+264 version: 0.0.30+281
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1