feat: basic integration of teams
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -30,6 +30,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> 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<AppDatabase> 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<AppDatabase> 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<AppDatabase> 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<AppDatabase> with _$MatchDaoMixin {
|
||||
group: group,
|
||||
players: players,
|
||||
teams: teams.isEmpty ? null : teams,
|
||||
isTeamMatch: row.isTeamMatch,
|
||||
notes: row.notes,
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
|
||||
@@ -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<AppDatabase> 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<AppDatabase> 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<AppDatabase> 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<AppDatabase> 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<AppDatabase> with _$TeamDaoMixin {
|
||||
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 */
|
||||
|
||||
/// Deletes all teams from the database.
|
||||
|
||||
@@ -1185,6 +1185,21 @@ class $MatchTableTable extends MatchTable
|
||||
type: DriftSqlType.string,
|
||||
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');
|
||||
@override
|
||||
late final GeneratedColumn<String> notes = GeneratedColumn<String>(
|
||||
@@ -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<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
map['group_id'] = Variable<String>(groupId);
|
||||
}
|
||||
map['name'] = Variable<String>(name);
|
||||
map['is_team_match'] = Variable<bool>(isTeamMatch);
|
||||
map['notes'] = Variable<String>(notes);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
if (!nullToAbsent || endedAt != null) {
|
||||
@@ -1375,6 +1407,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
|
||||
? 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<MatchTableData> {
|
||||
gameId: serializer.fromJson<String>(json['gameId']),
|
||||
groupId: serializer.fromJson<String?>(json['groupId']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
isTeamMatch: serializer.fromJson<bool>(json['isTeamMatch']),
|
||||
notes: serializer.fromJson<String>(json['notes']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
endedAt: serializer.fromJson<DateTime?>(json['endedAt']),
|
||||
@@ -1406,6 +1440,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
|
||||
'gameId': serializer.toJson<String>(gameId),
|
||||
'groupId': serializer.toJson<String?>(groupId),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'isTeamMatch': serializer.toJson<bool>(isTeamMatch),
|
||||
'notes': serializer.toJson<String>(notes),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'endedAt': serializer.toJson<DateTime?>(endedAt),
|
||||
@@ -1417,6 +1452,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
|
||||
String? gameId,
|
||||
Value<String?> groupId = const Value.absent(),
|
||||
String? name,
|
||||
bool? isTeamMatch,
|
||||
String? notes,
|
||||
DateTime? createdAt,
|
||||
Value<DateTime?> endedAt = const Value.absent(),
|
||||
@@ -1425,6 +1461,7 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
..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<MatchTableData> {
|
||||
}
|
||||
|
||||
@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<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
final Value<String> gameId;
|
||||
final Value<String?> groupId;
|
||||
final Value<String> name;
|
||||
final Value<bool> isTeamMatch;
|
||||
final Value<String> notes;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<DateTime?> endedAt;
|
||||
@@ -1485,6 +1536,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
Expression<String>? gameId,
|
||||
Expression<String>? groupId,
|
||||
Expression<String>? name,
|
||||
Expression<bool>? isTeamMatch,
|
||||
Expression<String>? notes,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<DateTime>? endedAt,
|
||||
@@ -1519,6 +1573,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
Value<String>? gameId,
|
||||
Value<String?>? groupId,
|
||||
Value<String>? name,
|
||||
Value<bool>? isTeamMatch,
|
||||
Value<String>? notes,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<DateTime?>? endedAt,
|
||||
@@ -1541,6 +1597,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
|
||||
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<MatchTableData> {
|
||||
if (name.present) {
|
||||
map['name'] = Variable<String>(name.value);
|
||||
}
|
||||
if (isTeamMatch.present) {
|
||||
map['is_team_match'] = Variable<bool>(isTeamMatch.value);
|
||||
}
|
||||
if (notes.present) {
|
||||
map['notes'] = Variable<String>(notes.value);
|
||||
}
|
||||
@@ -1585,6 +1645,7 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
|
||||
..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<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
|
||||
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<TeamTableData> {
|
||||
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<String, Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -1934,6 +2039,8 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
|
||||
map['id'] = Variable<String>(id);
|
||||
map['name'] = Variable<String>(name);
|
||||
map['created_at'] = Variable<DateTime>(createdAt);
|
||||
map['color'] = Variable<String>(color);
|
||||
map['score'] = Variable<int>(score);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1942,6 +2049,8 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
|
||||
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<TeamTableData> {
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
name: serializer.fromJson<String>(json['name']),
|
||||
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||
color: serializer.fromJson<String>(json['color']),
|
||||
score: serializer.fromJson<int>(json['score']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -1963,20 +2074,31 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
|
||||
'id': serializer.toJson<String>(id),
|
||||
'name': serializer.toJson<String>(name),
|
||||
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||
'color': serializer.toJson<String>(color),
|
||||
'score': serializer.toJson<int>(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<TeamTableData> {
|
||||
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<TeamTableData> {
|
||||
final Value<String> id;
|
||||
final Value<String> name;
|
||||
final Value<DateTime> createdAt;
|
||||
final Value<String> color;
|
||||
final Value<int> score;
|
||||
final Value<int> 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<TeamTableData> {
|
||||
Expression<String>? id,
|
||||
Expression<String>? name,
|
||||
Expression<DateTime>? createdAt,
|
||||
Expression<String>? color,
|
||||
Expression<int>? score,
|
||||
Expression<int>? 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<TeamTableData> {
|
||||
Value<String>? id,
|
||||
Value<String>? name,
|
||||
Value<DateTime>? createdAt,
|
||||
Value<String>? color,
|
||||
Value<int>? score,
|
||||
Value<int>? 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<TeamTableData> {
|
||||
if (createdAt.present) {
|
||||
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) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
@@ -2072,6 +2218,8 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
|
||||
..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<String?> groupId,
|
||||
required String name,
|
||||
Value<bool> isTeamMatch,
|
||||
required String notes,
|
||||
required DateTime createdAt,
|
||||
Value<DateTime?> endedAt,
|
||||
@@ -4103,6 +4252,7 @@ typedef $$MatchTableTableUpdateCompanionBuilder =
|
||||
Value<String> gameId,
|
||||
Value<String?> groupId,
|
||||
Value<String> name,
|
||||
Value<bool> isTeamMatch,
|
||||
Value<String> notes,
|
||||
Value<DateTime> createdAt,
|
||||
Value<DateTime?> endedAt,
|
||||
@@ -4215,6 +4365,11 @@ class $$MatchTableTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<bool> get isTeamMatch => $composableBuilder(
|
||||
column: $table.isTeamMatch,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnFilters<String> get notes => $composableBuilder(
|
||||
column: $table.notes,
|
||||
builder: (column) => ColumnFilters(column),
|
||||
@@ -4346,6 +4501,11 @@ class $$MatchTableTableOrderingComposer
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<bool> get isTeamMatch => $composableBuilder(
|
||||
column: $table.isTeamMatch,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get notes => $composableBuilder(
|
||||
column: $table.notes,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
@@ -4423,6 +4583,11 @@ class $$MatchTableTableAnnotationComposer
|
||||
GeneratedColumn<String> get name =>
|
||||
$composableBuilder(column: $table.name, builder: (column) => column);
|
||||
|
||||
GeneratedColumn<bool> get isTeamMatch => $composableBuilder(
|
||||
column: $table.isTeamMatch,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
GeneratedColumn<String> get notes =>
|
||||
$composableBuilder(column: $table.notes, builder: (column) => column);
|
||||
|
||||
@@ -4566,6 +4731,7 @@ class $$MatchTableTableTableManager
|
||||
Value<String> gameId = const Value.absent(),
|
||||
Value<String?> groupId = const Value.absent(),
|
||||
Value<String> name = const Value.absent(),
|
||||
Value<bool> isTeamMatch = const Value.absent(),
|
||||
Value<String> notes = const Value.absent(),
|
||||
Value<DateTime> createdAt = const Value.absent(),
|
||||
Value<DateTime?> 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<String?> groupId = const Value.absent(),
|
||||
required String name,
|
||||
Value<bool> isTeamMatch = const Value.absent(),
|
||||
required String notes,
|
||||
required DateTime createdAt,
|
||||
Value<DateTime?> 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<String> color,
|
||||
Value<int> score,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $$TeamTableTableUpdateCompanionBuilder =
|
||||
@@ -5116,6 +5287,8 @@ typedef $$TeamTableTableUpdateCompanionBuilder =
|
||||
Value<String> id,
|
||||
Value<String> name,
|
||||
Value<DateTime> createdAt,
|
||||
Value<String> color,
|
||||
Value<int> score,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
@@ -5171,6 +5344,16 @@ class $$TeamTableTableFilterComposer
|
||||
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> Function($$PlayerMatchTableTableFilterComposer f) f,
|
||||
) {
|
||||
@@ -5220,6 +5403,16 @@ class $$TeamTableTableOrderingComposer
|
||||
column: $table.createdAt,
|
||||
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
|
||||
@@ -5240,6 +5433,12 @@ class $$TeamTableTableAnnotationComposer
|
||||
GeneratedColumn<DateTime> get createdAt =>
|
||||
$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> Function($$PlayerMatchTableTableAnnotationComposer a) f,
|
||||
) {
|
||||
@@ -5297,11 +5496,15 @@ class $$TeamTableTableTableManager
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<String> name = 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(),
|
||||
}) => 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<String> color = const Value.absent(),
|
||||
Value<int> score = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) => TeamTableCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
createdAt: createdAt,
|
||||
color: color,
|
||||
score: score,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
|
||||
@@ -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()();
|
||||
|
||||
@@ -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<Column<Object>> get primaryKey => {id};
|
||||
|
||||
@@ -16,6 +16,7 @@ class Match {
|
||||
final Game game;
|
||||
final Group? group;
|
||||
final List<Player> players;
|
||||
final bool isTeamMatch;
|
||||
final List<Team>? teams;
|
||||
final String notes;
|
||||
Map<String, ScoreEntry?> 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<Player>? players,
|
||||
bool? isTeamMatch,
|
||||
List<Team>? teams,
|
||||
String? notes,
|
||||
Map<String, ScoreEntry?>? 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<String, dynamic>).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<Player> 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<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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Player> 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<Player>? 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<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'color': color.name,
|
||||
'score': score,
|
||||
'memberIds': members.map((member) => member.id).toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<CreateMatchView> {
|
||||
|
||||
Group? selectedGroup;
|
||||
Game? selectedGame;
|
||||
bool isTeamMatch = false;
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
/// GlobalKey for ScaffoldMessenger to show snackbars
|
||||
@@ -135,24 +137,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
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<CreateMatchView> {
|
||||
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<CreateMatchView> {
|
||||
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<CreateMatchView> {
|
||||
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.
|
||||
///
|
||||
/// 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<CreateMatchView> {
|
||||
///
|
||||
/// 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<CreateMatchView> {
|
||||
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<void> removeGroupWhenNoMemberLeft() async {
|
||||
if (selectedGroup == null) return;
|
||||
|
||||
if (!selectedPlayers.any(
|
||||
(player) =>
|
||||
selectedGroup!.members.any((member) => member.id == player.id),
|
||||
)) {
|
||||
setState(() {
|
||||
selectedGroup = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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<MatchResultView> {
|
||||
|
||||
late final Ruleset ruleset;
|
||||
|
||||
/// List of all players who participated in the match
|
||||
late final List<Player> allPlayers;
|
||||
late final List<Team> allTeams;
|
||||
|
||||
/// List of text controllers for score entry, one for each player
|
||||
late final List<TextEditingController> controller;
|
||||
@@ -45,44 +47,27 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
/// 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<AppDatabase>(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<MatchResultView> {
|
||||
// Show player selection
|
||||
if (rulesetSupportsWinnerSelection())
|
||||
Expanded(
|
||||
child: 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(
|
||||
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<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.
|
||||
void onTextEnter() {
|
||||
if (rulesetSupportsScoreEntry()) {
|
||||
@@ -459,4 +352,311 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
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<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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<MainMenuButton>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with TickerProviderStateMixin {
|
||||
late AnimationController _animationController;
|
||||
late AnimationController _disabledAnimationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _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<MainMenuButton>
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_disabledAnimationController = AnimationController(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
|
||||
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
|
||||
);
|
||||
|
||||
_disabledScaleAnimation = Tween<double>(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<MainMenuButton>
|
||||
},
|
||||
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<MainMenuButton>
|
||||
void dispose() {
|
||||
_cancelTimers();
|
||||
_animationController.dispose();
|
||||
_disabledAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -8,13 +8,13 @@ class CustomRadioListTile<T> 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<T> extends StatelessWidget {
|
||||
child: Row(
|
||||
children: [
|
||||
Radio<T>(value: value, toggleable: true),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: content),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -128,6 +128,12 @@ class _MatchTileState extends State<MatchTile> {
|
||||
|
||||
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(
|
||||
|
||||
141
lib/presentation/widgets/tiles/team_creation_tile.dart
Normal file
141
lib/presentation/widgets/tiles/team_creation_tile.dart
Normal 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(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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) ...<Widget>[
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: onIconTap,
|
||||
child: const Icon(Icons.close, size: 20),
|
||||
),
|
||||
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user