Compare commits
55 Commits
feature/19
...
feature/16
| Author | SHA1 | Date | |
|---|---|---|---|
| 99c3c3c257 | |||
| 6d17539af2 | |||
| 3c9d115d08 | |||
| a1b2a1d722 | |||
| 25afe998f5 | |||
| 4b30673125 | |||
| 047c4e88d3 | |||
| cf7dbe60f4 | |||
| 47829a6955 | |||
| 96037e6062 | |||
| a0897d4966 | |||
| 6fb4a8996c | |||
| df64ef4b93 | |||
| a6deba4238 | |||
| a7d36787ce | |||
| 14eb77e241 | |||
| 46134a4f5c | |||
| 12b7bcdc6c | |||
| a803dc36d7 | |||
| a497ae872b | |||
| df8e060707 | |||
| 021a546479 | |||
| 32a8a6090a | |||
| f3f7d44994 | |||
| 60d746ede2 | |||
| 32d6eb1d18 | |||
| 042f44e8ef | |||
| e761fb1474 | |||
| 1f9ba96401 | |||
| 63f050b34f | |||
| aaeb4bf18c | |||
| 8b1a447bd9 | |||
| 0c40357847 | |||
| e61af14827 | |||
| 3493a74c6f | |||
| ec1182b560 | |||
| 4c5ce1aba0 | |||
| 369cabe996 | |||
| f069f62e26 | |||
| 44c474ae77 | |||
| 9c5e72e6ed | |||
| 9a8b93510e | |||
| 0812f18d77 | |||
| 0f621cd799 | |||
| 1356cbc92a | |||
| 9bce03d780 | |||
| 0a1e14a32d | |||
| 092dd5ec0a | |||
| 418a6307a0 | |||
| 07703037f2 | |||
| d9e0cdf506 | |||
| 2de8cef18d | |||
| 2f72b71fda | |||
| a957408c7e | |||
| badf5ea311 |
@@ -31,17 +31,21 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/cirruslabs/flutter:stable
|
||||
steps:
|
||||
- name: Install Node
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y nodejs npm
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Required for Flutter action
|
||||
- name: Install jq
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y jq
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
|
||||
@@ -7,20 +7,23 @@ on:
|
||||
- "main"
|
||||
|
||||
jobs:
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/cirruslabs/flutter:stable
|
||||
steps:
|
||||
- name: Install Node
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y nodejs npm
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Required for Flutter action
|
||||
- name: Install jq
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y jq
|
||||
|
||||
- name: Set up Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
|
||||
- name: Get dependencies
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
@@ -292,8 +295,6 @@ jobs:
|
||||
|
||||
- name: Setup Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
with:
|
||||
packages: "platform-tools platforms;android-34 build-tools;34.0.0"
|
||||
|
||||
# Required for Flutter action
|
||||
- name: Install jq
|
||||
|
||||
@@ -166,6 +166,9 @@
|
||||
"notes": {
|
||||
"type": "string"
|
||||
},
|
||||
"isTeamMatch": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"teams": {
|
||||
"type": ["array", "null"]
|
||||
}
|
||||
@@ -177,7 +180,8 @@
|
||||
"createdAt",
|
||||
"gameId",
|
||||
"playerIds",
|
||||
"notes"
|
||||
"notes",
|
||||
"isTeamMatch"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "csqlite",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/simolus3/CSQLite.git",
|
||||
"state" : {
|
||||
"revision" : "1ee46d19a4f451a7aa64ffc64fc99b4748131e62"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dkcamera",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -24,8 +24,23 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns a AppColor enum value based on the provided team [index].
|
||||
AppColor getTeamColor(int index) {
|
||||
final colors = [
|
||||
AppColor.red,
|
||||
AppColor.blue,
|
||||
AppColor.green,
|
||||
AppColor.yellow,
|
||||
AppColor.purple,
|
||||
AppColor.orange,
|
||||
AppColor.pink,
|
||||
AppColor.teal,
|
||||
];
|
||||
return colors[index % colors.length];
|
||||
}
|
||||
|
||||
/// Translates a [AppColor] enum value to its corresponding localized string.
|
||||
String translateAppColorToString(AppColor color, BuildContext context) {
|
||||
String translateGameColorToString(AppColor color, BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (color) {
|
||||
case AppColor.red:
|
||||
@@ -48,7 +63,7 @@ String translateAppColorToString(AppColor color, BuildContext context) {
|
||||
}
|
||||
|
||||
/// Returns the [Color] object corresponding to a [AppColor] enum value.
|
||||
Color getColorFromAppColor(AppColor color) {
|
||||
Color getColorFromGameColor(AppColor color) {
|
||||
switch (color) {
|
||||
case AppColor.red:
|
||||
return Colors.red;
|
||||
@@ -63,9 +78,9 @@ Color getColorFromAppColor(AppColor color) {
|
||||
case AppColor.orange:
|
||||
return const Color(0xFFef681f);
|
||||
case AppColor.pink:
|
||||
return const Color(0xFFE91E63);
|
||||
return Colors.pink;
|
||||
case AppColor.teal:
|
||||
return const Color(0xFF00BCD4);
|
||||
return Colors.teal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,11 +92,10 @@ IconData getRulesetIcon(Ruleset ruleset) {
|
||||
case Ruleset.lowestScore:
|
||||
return Icons.arrow_downward;
|
||||
case Ruleset.singleWinner:
|
||||
case Ruleset.multipleWinners:
|
||||
return Icons.emoji_events;
|
||||
case Ruleset.singleLoser:
|
||||
return Icons.sentiment_dissatisfied;
|
||||
case Ruleset.multipleWinners:
|
||||
return Icons.group;
|
||||
case Ruleset.placement:
|
||||
return RpgAwesome.podium;
|
||||
}
|
||||
@@ -113,6 +127,7 @@ String getExtraPlayerCount(Match match) {
|
||||
return ' + ${count.toString()}';
|
||||
}
|
||||
|
||||
/// Returns the player name count if greater 0 in the format " #2", otherwise an empty string
|
||||
String getNameCountText(Player player) {
|
||||
if (player.nameCount >= 1) {
|
||||
return ' #${player.nameCount}';
|
||||
@@ -120,6 +135,7 @@ String getNameCountText(Player player) {
|
||||
return '';
|
||||
}
|
||||
|
||||
/// Returns the correct singular or plural form of "point(s)" based on the [points] value.
|
||||
String getPointLabel(AppLocalizations loc, int points) {
|
||||
if (points == 1) {
|
||||
return '$points ${loc.point}';
|
||||
|
||||
@@ -65,7 +65,11 @@ class CustomTheme {
|
||||
|
||||
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
|
||||
color: boxColor,
|
||||
border: Border.all(color: textColor, width: 2),
|
||||
border: Border.all(
|
||||
color: textColor,
|
||||
width: 2,
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
),
|
||||
borderRadius: standardBorderRadiusAll,
|
||||
);
|
||||
|
||||
|
||||
@@ -42,33 +42,5 @@ enum Ruleset {
|
||||
singleLoser,
|
||||
}
|
||||
|
||||
/// Different colors for highlighting games
|
||||
/// Different colors for highlighting content
|
||||
enum AppColor { red, orange, yellow, green, teal, blue, purple, pink }
|
||||
|
||||
enum StatisticType {
|
||||
totalMatches,
|
||||
totalWins,
|
||||
totalScore,
|
||||
totalLosses,
|
||||
averageScore,
|
||||
bestScore,
|
||||
worstScore,
|
||||
winrate,
|
||||
}
|
||||
|
||||
enum StatisticScope {
|
||||
allPlayers,
|
||||
//selectedPlayer,
|
||||
selectedGroups,
|
||||
selectedGames,
|
||||
timeframe,
|
||||
}
|
||||
|
||||
enum Timeframe {
|
||||
last7Days,
|
||||
last30Days,
|
||||
last90Days,
|
||||
last180Days,
|
||||
lastYear,
|
||||
allTime,
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
/// Returns `true` if the game exists, `false` otherwise.
|
||||
Future<bool> gameExists({required String gameId}) async {
|
||||
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||
final row = await query.getSingleOrNull();
|
||||
return row != null;
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Retrieves all games from the database.
|
||||
@@ -103,15 +103,15 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
/// Retrieves a [Game] by its [gameId].
|
||||
Future<Game> getGameById({required String gameId}) async {
|
||||
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||
final row = await query.getSingle();
|
||||
final result = await query.getSingle();
|
||||
return Game(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
|
||||
description: row.description,
|
||||
color: AppColor.values.firstWhere((e) => e.name == row.color),
|
||||
icon: row.icon,
|
||||
createdAt: row.createdAt,
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset),
|
||||
description: result.description,
|
||||
color: AppColor.values.firstWhere((e) => e.name == result.color),
|
||||
icon: result.icon,
|
||||
createdAt: result.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
required String name,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
|
||||
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
|
||||
GameTableCompanion(name: Value(name)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
@@ -135,7 +135,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
required Ruleset ruleset,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
|
||||
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
|
||||
GameTableCompanion(ruleset: Value(ruleset.name)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
@@ -147,7 +147,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
required String description,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
|
||||
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
|
||||
GameTableCompanion(description: Value(description)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
@@ -159,7 +159,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
required AppColor color,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
|
||||
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
|
||||
GameTableCompanion(color: Value(color.name)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
@@ -171,7 +171,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
required String icon,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
|
||||
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(
|
||||
GameTableCompanion(icon: Value(icon)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
@@ -182,7 +182,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
/// Deletes the game with the given [gameId] from the database.
|
||||
/// Returns `true` if the game was deleted, `false` if the game did not exist.
|
||||
Future<bool> deleteGame({required String gameId}) async {
|
||||
final query = delete(gameTable)..where((tbl) => tbl.id.equals(gameId));
|
||||
final query = delete(gameTable)..where((g) => g.id.equals(gameId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@@ -143,16 +143,16 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
final query = select(groupTable);
|
||||
final result = await query.get();
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
result.map((groupData) async {
|
||||
final members = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: row.id,
|
||||
groupId: groupData.id,
|
||||
);
|
||||
return Group(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
id: groupData.id,
|
||||
name: groupData.name,
|
||||
description: groupData.description,
|
||||
members: members,
|
||||
createdAt: row.createdAt,
|
||||
createdAt: groupData.createdAt,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -161,18 +161,18 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
/// Retrieves a [Group] by its [groupId], including its members.
|
||||
Future<Group> getGroupById({required String groupId}) async {
|
||||
final query = select(groupTable)..where((g) => g.id.equals(groupId));
|
||||
final row = await query.getSingle();
|
||||
final result = await query.getSingle();
|
||||
|
||||
List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: groupId,
|
||||
);
|
||||
|
||||
return Group(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
description: result.description,
|
||||
members: members,
|
||||
createdAt: row.createdAt,
|
||||
createdAt: result.createdAt,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -180,49 +180,17 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
Future<int> getGroupCount() async {
|
||||
final count =
|
||||
await (selectOnly(groupTable)..addColumns([groupTable.id.count()]))
|
||||
.map((tbl) => tbl.read(groupTable.id.count()))
|
||||
.map((row) => row.read(groupTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
/// Retrieves all groups a specific player belongs to.
|
||||
/// Returns an empty list if the player is not part of any group.
|
||||
Future<List<Group>> getGroupsByPlayer({required String playerId}) async {
|
||||
final playerGroups = await (select(
|
||||
playerGroupTable,
|
||||
)..where((tbl) => tbl.playerId.equals(playerId))).get();
|
||||
|
||||
if (playerGroups.isEmpty) return [];
|
||||
|
||||
final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList();
|
||||
final result =
|
||||
await (select(groupTable)
|
||||
..where((tbl) => tbl.id.isIn(groupIds))
|
||||
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
|
||||
.get();
|
||||
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
final members = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: row.id,
|
||||
);
|
||||
return Group(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
members: members,
|
||||
createdAt: row.createdAt,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Checks if a group with the given [groupId] exists in the database.
|
||||
/// Returns `true` if the group exists, `false` otherwise.
|
||||
Future<bool> groupExists({required String groupId}) async {
|
||||
final query = select(groupTable)..where((g) => g.id.equals(groupId));
|
||||
final row = await query.getSingleOrNull();
|
||||
return row != null;
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/* Delete */
|
||||
@@ -252,8 +220,9 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
required String name,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
|
||||
.write(GroupTableCompanion(name: Value(name)));
|
||||
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
|
||||
GroupTableCompanion(name: Value(name)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@@ -264,8 +233,9 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
||||
required String description,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
|
||||
.write(GroupTableCompanion(description: Value(description)));
|
||||
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
|
||||
GroupTableCompanion(description: Value(description)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
@@ -258,15 +260,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Returns `true` if the match exists, otherwise `false`.
|
||||
Future<bool> matchExists({required String matchId}) async {
|
||||
final query = select(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final row = await query.getSingleOrNull();
|
||||
return row != null;
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Retrieves the number of matches in the database.
|
||||
Future<int> getMatchCount() async {
|
||||
final count =
|
||||
await (selectOnly(matchTable)..addColumns([matchTable.id.count()]))
|
||||
.map((tbl) => tbl.read(matchTable.id.count()))
|
||||
.map((row) => row.read(matchTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
@@ -279,12 +281,10 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
final game = await db.gameDao.getGameById(gameId: row.gameId);
|
||||
|
||||
Group? group;
|
||||
if (row.groupId != null) {
|
||||
group = await db.groupDao.getGroupById(groupId: row.groupId!);
|
||||
}
|
||||
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: row.id,
|
||||
);
|
||||
@@ -302,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,
|
||||
@@ -314,13 +315,13 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Retrieves a [Match] by its [matchId].
|
||||
Future<Match> getMatchById({required String matchId}) async {
|
||||
final query = select(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final row = await query.getSingle();
|
||||
final result = await query.getSingle();
|
||||
|
||||
final game = await db.gameDao.getGameById(gameId: row.gameId);
|
||||
final game = await db.gameDao.getGameById(gameId: result.gameId);
|
||||
|
||||
Group? group;
|
||||
if (row.groupId != null) {
|
||||
group = await db.groupDao.getGroupById(groupId: row.groupId!);
|
||||
if (result.groupId != null) {
|
||||
group = await db.groupDao.getGroupById(groupId: result.groupId!);
|
||||
}
|
||||
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
|
||||
@@ -330,15 +331,16 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
final teams = await _getMatchTeams(matchId: matchId);
|
||||
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
game: game,
|
||||
group: group,
|
||||
players: players,
|
||||
teams: teams.isEmpty ? null : teams,
|
||||
notes: row.notes,
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
isTeamMatch: result.isTeamMatch,
|
||||
notes: result.notes,
|
||||
createdAt: result.createdAt,
|
||||
endedAt: result.endedAt,
|
||||
scores: scores,
|
||||
);
|
||||
}
|
||||
@@ -349,73 +351,25 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
await (selectOnly(matchTable)
|
||||
..where(matchTable.gameId.equals(gameId))
|
||||
..addColumns([matchTable.id.count()]))
|
||||
.map((tbl) => tbl.read(matchTable.id.count()))
|
||||
.map((row) => row.read(matchTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
|
||||
Future<List<Match>> getMatchesByPlayer({required String playerId}) async {
|
||||
final playerMatches = await (select(
|
||||
playerMatchTable,
|
||||
)..where((tbl) => tbl.playerId.equals(playerId))).get();
|
||||
|
||||
if (playerMatches.isEmpty) return [];
|
||||
|
||||
final matchIds = playerMatches.map((tbl) => tbl.matchId).toSet().toList();
|
||||
final result =
|
||||
await (select(matchTable)
|
||||
..where((tbl) => tbl.id.isIn(matchIds))
|
||||
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
|
||||
.get();
|
||||
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
final game = await db.gameDao.getGameById(gameId: row.gameId);
|
||||
|
||||
Group? group;
|
||||
if (row.groupId != null) {
|
||||
group = await db.groupDao.getGroupById(groupId: row.groupId!);
|
||||
}
|
||||
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: row.id,
|
||||
);
|
||||
final scores = await db.scoreEntryDao.getAllMatchScores(
|
||||
matchId: row.id,
|
||||
);
|
||||
final teams = await _getMatchTeams(matchId: row.id);
|
||||
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
game: game,
|
||||
group: group,
|
||||
players: players,
|
||||
teams: teams.isEmpty ? null : teams,
|
||||
notes: row.notes,
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
scores: scores,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Retrieves all matches associated with the given [groupId].
|
||||
/// Queries the database directly, filtering by [groupId].
|
||||
Future<List<Match>> getMatchesByGroup({required String groupId}) async {
|
||||
final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
|
||||
final result = await query.get();
|
||||
final rows = await query.get();
|
||||
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
rows.map((row) async {
|
||||
final game = await db.gameDao.getGameById(gameId: row.gameId);
|
||||
final group = await db.groupDao.getGroupById(groupId: groupId);
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: row.id,
|
||||
);
|
||||
final teams = await _getMatchTeams(matchId: row.id);
|
||||
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -423,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,
|
||||
@@ -435,7 +390,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
Future<List<Team>> _getMatchTeams({required String matchId}) async {
|
||||
// Get all unique team IDs from PlayerMatchTable for this match
|
||||
final playerMatchQuery = select(db.playerMatchTable)
|
||||
..where((tbl) => tbl.matchId.equals(matchId) & tbl.teamId.isNotNull());
|
||||
..where((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull());
|
||||
final playerMatches = await playerMatchQuery.get();
|
||||
|
||||
if (playerMatches.isEmpty) return [];
|
||||
@@ -451,7 +406,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)),
|
||||
);
|
||||
|
||||
return teams;
|
||||
return teams
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
}
|
||||
|
||||
/* Update */
|
||||
@@ -462,7 +418,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
required String matchId,
|
||||
required String name,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(name: Value(name)),
|
||||
);
|
||||
@@ -477,7 +433,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
required String matchId,
|
||||
required String? groupId,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(groupId: Value(groupId)),
|
||||
);
|
||||
@@ -490,7 +446,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
required String matchId,
|
||||
required String notes,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(notes: Value(notes)),
|
||||
);
|
||||
@@ -501,7 +457,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Sets the groupId to null.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> removeMatchGroup({required String matchId}) async {
|
||||
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
const MatchTableCompanion(groupId: Value(null)),
|
||||
);
|
||||
@@ -515,7 +471,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
required String matchId,
|
||||
required DateTime endedAt,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(endedAt: Value(endedAt)),
|
||||
);
|
||||
@@ -527,7 +483,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Deletes the match with the given [matchId] from the database.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> deleteMatch({required String matchId}) async {
|
||||
final query = delete(matchTable)..where((tbl) => tbl.id.equals(matchId));
|
||||
final query = delete(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
@@ -543,7 +499,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Deletes all matches associated with a specific game.
|
||||
/// Returns the number of matches deleted.
|
||||
Future<int> deleteMatchesByGame({required String gameId}) async {
|
||||
final query = delete(matchTable)..where((tbl) => tbl.gameId.equals(gameId));
|
||||
final query = delete(matchTable)..where((m) => m.gameId.equals(gameId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/// the new one.
|
||||
Future<bool> addPlayer({required Player player}) async {
|
||||
if (!await playerExists(playerId: player.id)) {
|
||||
final int nameCount = await _processNameCount(name: player.name);
|
||||
final int nameCount = await calculateNameCount(name: player.name);
|
||||
|
||||
await into(playerTable).insert(
|
||||
PlayerTableCompanion.insert(
|
||||
@@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
final playersWithName = entry.value;
|
||||
|
||||
// Get the current nameCount
|
||||
var nameCount = await _processNameCount(name: name);
|
||||
var nameCount = await calculateNameCount(name: name);
|
||||
|
||||
// One player with the same name
|
||||
if (playersWithName.length == 1) {
|
||||
@@ -113,7 +113,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
Future<int> getPlayerCount() async {
|
||||
final count =
|
||||
await (selectOnly(playerTable)..addColumns([playerTable.id.count()]))
|
||||
.map((tbl) => tbl.read(playerTable.id.count()))
|
||||
.map((row) => row.read(playerTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
@@ -122,8 +122,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/// Returns `true` if the player exists, `false` otherwise.
|
||||
Future<bool> playerExists({required String playerId}) async {
|
||||
final query = select(playerTable)..where((p) => p.id.equals(playerId));
|
||||
final row = await query.getSingleOrNull();
|
||||
return row != null;
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Retrieves all players from the database.
|
||||
@@ -146,76 +146,57 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/// Retrieves a [Player] by their [id].
|
||||
Future<Player> getPlayerById({required String playerId}) async {
|
||||
final query = select(playerTable)..where((p) => p.id.equals(playerId));
|
||||
final row = await query.getSingle();
|
||||
final result = await query.getSingle();
|
||||
return Player(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
createdAt: row.createdAt,
|
||||
nameCount: row.nameCount,
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
description: result.description,
|
||||
createdAt: result.createdAt,
|
||||
nameCount: result.nameCount,
|
||||
);
|
||||
}
|
||||
|
||||
/* Update */
|
||||
|
||||
/// Updates the name of the player with the given [playerId] to [name].
|
||||
///
|
||||
/// Keeps the `nameCount` values of the affected name groups consistent:
|
||||
/// - The renamed player gets a fresh `nameCount` for the new name group.
|
||||
/// - All players in the previous name group whose `nameCount` was greater
|
||||
/// than the removed one get decremented by 1, so the numbering stays
|
||||
/// contiguous (1..N) in `createdAt` order.
|
||||
/// - If only one player remains in the previous name group, their
|
||||
/// `nameCount` is reset to 0.
|
||||
Future<bool> updatePlayerName({
|
||||
required String playerId,
|
||||
required String name,
|
||||
}) async {
|
||||
return transaction(() async {
|
||||
final previousPlayer = await (select(
|
||||
playerTable,
|
||||
)..where((tbl) => tbl.id.equals(playerId))).getSingleOrNull();
|
||||
if (previousPlayer == null) return false;
|
||||
// Get previous name and name count for the player before updating
|
||||
final previousPlayerName =
|
||||
await (select(playerTable)..where((p) => p.id.equals(playerId)))
|
||||
.map((row) => row.name)
|
||||
.getSingleOrNull() ??
|
||||
'';
|
||||
final previousNameCount = await getNameCount(name: previousPlayerName);
|
||||
|
||||
final previousName = previousPlayer.name;
|
||||
final previousCount = previousPlayer.nameCount;
|
||||
final rowsAffected =
|
||||
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
|
||||
PlayerTableCompanion(name: Value(name)),
|
||||
);
|
||||
|
||||
// Determine the nameCount for the renamed player in the new group.
|
||||
final newNameCount = await _processNameCount(name: name);
|
||||
// Update name count for the new name
|
||||
final count = await calculateNameCount(name: name);
|
||||
if (count > 0) {
|
||||
await (update(playerTable)..where((p) => p.name.equals(name))).write(
|
||||
PlayerTableCompanion(nameCount: Value(count)),
|
||||
);
|
||||
}
|
||||
|
||||
final rowsAffected =
|
||||
await (update(
|
||||
playerTable,
|
||||
)..where((tbl) => tbl.id.equals(playerId))).write(
|
||||
PlayerTableCompanion(
|
||||
name: Value(name),
|
||||
nameCount: Value(newNameCount),
|
||||
),
|
||||
);
|
||||
|
||||
// Consolidate the previous name group.
|
||||
final remainingCount = await getNameCount(name: previousName);
|
||||
|
||||
if (remainingCount == 1) {
|
||||
// Only one player left
|
||||
await (update(playerTable)..where((p) => p.name.equals(previousName)))
|
||||
.write(const PlayerTableCompanion(nameCount: Value(0)));
|
||||
} else if (remainingCount > 1 && previousCount > 0) {
|
||||
// Shift every player above the gap down by one to keep numbering in order.
|
||||
await (update(playerTable)..where(
|
||||
(tbl) =>
|
||||
tbl.name.equals(previousName) &
|
||||
tbl.nameCount.isBiggerThanValue(previousCount),
|
||||
))
|
||||
.write(
|
||||
PlayerTableCompanion.custom(
|
||||
nameCount: playerTable.nameCount - const Constant(1),
|
||||
),
|
||||
);
|
||||
if (previousNameCount > 0) {
|
||||
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
|
||||
final player = await getPlayerWithHighestNameCount(
|
||||
name: previousPlayerName,
|
||||
);
|
||||
if (player != null) {
|
||||
await updateNameCount(
|
||||
playerId: player.id,
|
||||
nameCount: previousNameCount,
|
||||
);
|
||||
}
|
||||
|
||||
return rowsAffected > 0;
|
||||
});
|
||||
}
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Updates the description of the player with the given [playerId] to
|
||||
@@ -226,8 +207,9 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
required String description,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(playerTable)..where((tbl) => tbl.id.equals(playerId)))
|
||||
.write(PlayerTableCompanion(description: Value(description)));
|
||||
await (update(playerTable)..where((g) => g.id.equals(playerId))).write(
|
||||
PlayerTableCompanion(description: Value(description)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@@ -236,7 +218,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/// Deletes the player with the given [id] from the database.
|
||||
/// Returns `true` if the player was deleted, `false` if the player did not exist.
|
||||
Future<bool> deletePlayer({required String playerId}) async {
|
||||
final query = delete(playerTable)..where((tbl) => tbl.id.equals(playerId));
|
||||
final query = delete(playerTable)..where((p) => p.id.equals(playerId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
@@ -244,10 +226,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
/* Name count management */
|
||||
|
||||
/// Retrieves the count of players with the given [name].
|
||||
/// Returns the highest name count if players with the same name exist,
|
||||
/// otherwise `null`.
|
||||
Future<int> getNameCount({required String name}) async {
|
||||
final query = select(playerTable)..where((tbl) => tbl.name.equals(name));
|
||||
final query = select(playerTable)..where((p) => p.name.equals(name));
|
||||
final result = await query.get();
|
||||
return result.length;
|
||||
}
|
||||
@@ -258,7 +238,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
required String playerId,
|
||||
required int nameCount,
|
||||
}) async {
|
||||
final query = update(playerTable)..where((tbl) => tbl.id.equals(playerId));
|
||||
final query = update(playerTable)..where((p) => p.id.equals(playerId));
|
||||
final rowsAffected = await query.write(
|
||||
PlayerTableCompanion(nameCount: Value(nameCount)),
|
||||
);
|
||||
@@ -268,8 +248,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
@visibleForTesting
|
||||
Future<Player?> getPlayerWithHighestNameCount({required String name}) async {
|
||||
final query = select(playerTable)
|
||||
..where((tbl) => tbl.name.equals(name))
|
||||
..orderBy([(tbl) => OrderingTerm.desc(tbl.nameCount)])
|
||||
..where((p) => p.name.equals(name))
|
||||
..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
|
||||
..limit(1);
|
||||
final result = await query.getSingleOrNull();
|
||||
if (result != null) {
|
||||
@@ -284,47 +264,34 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Processes the name count for a new player with the given [name].
|
||||
///- 0 Player: returning 0
|
||||
///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1
|
||||
///- Other: returning the existing count + 1
|
||||
Future<int> _processNameCount({required String name}) async {
|
||||
final nameCount = await calculateNameCount(name: name);
|
||||
if (nameCount == 2) {
|
||||
// If one other player exists with the same name, initialize the nameCount
|
||||
await initializeNameCount(name: name);
|
||||
}
|
||||
return nameCount;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
/// Calculates the name count for a new player with the given [name].
|
||||
/// - 0 Players: Name count is 0
|
||||
/// - 1 Player: Name count is 2 (since the existing player will be 1)
|
||||
/// - Other: Name count is the existing count + 1
|
||||
Future<int> calculateNameCount({required String name}) async {
|
||||
final count = await getNameCount(name: name);
|
||||
final int nameCount;
|
||||
|
||||
if (count == 0) {
|
||||
// If no other players exist with the same name, the returned nameCount is 0
|
||||
nameCount = 0;
|
||||
} else if (count == 1) {
|
||||
// If one other player with the name count exists, the returned name count is 2
|
||||
if (count == 1) {
|
||||
// If one other player exists with the same name, initialize the nameCount
|
||||
await initializeNameCount(name: name);
|
||||
// And for the new player, set nameCount to 2
|
||||
nameCount = 2;
|
||||
} else {
|
||||
} else if (count > 1) {
|
||||
// If more than one player exists with the same name, just increment
|
||||
// the nameCount for the new player
|
||||
nameCount = count + 1;
|
||||
} else {
|
||||
// If no other players exist with the same name, set nameCount to 0
|
||||
nameCount = 0;
|
||||
}
|
||||
|
||||
return nameCount;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<bool> initializeNameCount({required String name}) async {
|
||||
final rowsAffected =
|
||||
await (update(playerTable)..where((tbl) => tbl.name.equals(name)))
|
||||
.write(const PlayerTableCompanion(nameCount: Value(1)));
|
||||
await (update(playerTable)..where((p) => p.name.equals(name))).write(
|
||||
const PlayerTableCompanion(nameCount: Value(1)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,25 +39,18 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
|
||||
/// Retrieves all players belonging to a specific group by [groupId].
|
||||
Future<List<Player>> getPlayersOfGroup({required String groupId}) async {
|
||||
final query = select(playerGroupTable).join([
|
||||
innerJoin(
|
||||
playerTable,
|
||||
playerTable.id.equalsExp(playerGroupTable.playerId),
|
||||
),
|
||||
])..where(playerGroupTable.groupId.equals(groupId));
|
||||
final query = select(playerGroupTable)
|
||||
..where((pG) => pG.groupId.equals(groupId));
|
||||
final result = await query.get();
|
||||
|
||||
final result = await query.map((row) => row.readTable(playerTable)).get();
|
||||
return result
|
||||
.map(
|
||||
(row) => Player(
|
||||
id: row.id,
|
||||
createdAt: row.createdAt,
|
||||
name: row.name,
|
||||
nameCount: row.nameCount,
|
||||
description: row.description,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
List<Player> groupMembers = List.empty(growable: true);
|
||||
|
||||
for (var entry in result) {
|
||||
final player = await db.playerDao.getPlayerById(playerId: entry.playerId);
|
||||
groupMembers.add(player);
|
||||
}
|
||||
|
||||
return groupMembers;
|
||||
}
|
||||
|
||||
/// Checks if a player with [playerId] is in the group with [groupId].
|
||||
@@ -67,9 +60,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
required String groupId,
|
||||
}) async {
|
||||
final query = select(playerGroupTable)
|
||||
..where(
|
||||
(tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId),
|
||||
);
|
||||
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
@@ -90,7 +81,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
await db.transaction(() async {
|
||||
// Remove all existing players from the group
|
||||
final deleteQuery = delete(db.playerGroupTable)
|
||||
..where((tbl) => tbl.groupId.equals(groupId));
|
||||
..where((p) => p.groupId.equals(groupId));
|
||||
await deleteQuery.go();
|
||||
|
||||
// Add new players to the player table if they don't exist
|
||||
@@ -130,9 +121,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
required String groupId,
|
||||
}) async {
|
||||
final query = delete(playerGroupTable)
|
||||
..where(
|
||||
(tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId),
|
||||
);
|
||||
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
await (selectOnly(playerMatchTable)
|
||||
..where(playerMatchTable.matchId.equals(matchId))
|
||||
..addColumns([playerMatchTable.playerId.count()]))
|
||||
.map((tbl) => tbl.read(playerMatchTable.playerId.count()))
|
||||
.map((row) => row.read(playerMatchTable.playerId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
..where(playerMatchTable.matchId.equals(matchId))
|
||||
..where(playerMatchTable.playerId.equals(playerId))
|
||||
..addColumns([playerMatchTable.playerId.count()]))
|
||||
.map((tbl) => tbl.read(playerMatchTable.playerId.count()))
|
||||
.map((row) => row.read(playerMatchTable.playerId.count()))
|
||||
.getSingle();
|
||||
return (count ?? 0) > 0;
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
Future<List<Player>> getPlayersOfMatch({required String matchId}) async {
|
||||
final result = await (select(
|
||||
playerMatchTable,
|
||||
)..where((tbl) => tbl.matchId.equals(matchId))).get();
|
||||
)..where((p) => p.matchId.equals(matchId))).get();
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
|
||||
@@ -74,7 +74,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
|
||||
);
|
||||
final players = await Future.wait(futures);
|
||||
return players;
|
||||
return players
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
}
|
||||
|
||||
/// Retrieves a list of [Player]s associated with a specific team in a match.
|
||||
@@ -85,8 +86,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
}) async {
|
||||
final result =
|
||||
await (select(playerMatchTable)
|
||||
..where((tbl) => tbl.matchId.equals(matchId))
|
||||
..where((tbl) => tbl.teamId.equals(teamId)))
|
||||
..where((p) => p.matchId.equals(matchId))
|
||||
..where((p) => p.teamId.equals(teamId)))
|
||||
.get();
|
||||
|
||||
if (result.isEmpty) return [];
|
||||
@@ -109,8 +110,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(playerMatchTable)..where(
|
||||
(tbl) =>
|
||||
tbl.matchId.equals(matchId) & tbl.playerId.equals(playerId),
|
||||
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId),
|
||||
))
|
||||
.write(PlayerMatchTableCompanion(teamId: Value(teamId)));
|
||||
return rowsAffected > 0;
|
||||
@@ -144,9 +144,9 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
// Remove old players
|
||||
if (playersToRemove.isNotEmpty) {
|
||||
await (delete(playerMatchTable)..where(
|
||||
(tbl) =>
|
||||
tbl.matchId.equals(matchId) &
|
||||
tbl.playerId.isIn(playersToRemove.toList()),
|
||||
(pg) =>
|
||||
pg.matchId.equals(matchId) &
|
||||
pg.playerId.isIn(playersToRemove.toList()),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
@@ -183,8 +183,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
|
||||
required String playerId,
|
||||
}) async {
|
||||
final query = delete(playerMatchTable)
|
||||
..where((tbl) => tbl.matchId.equals(matchId))
|
||||
..where((tbl) => tbl.playerId.equals(playerId));
|
||||
..where((pg) => pg.matchId.equals(matchId))
|
||||
..where((pg) => pg.playerId.equals(playerId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@@ -16,12 +16,12 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
/* Create */
|
||||
|
||||
/// Adds a score entry to the database.
|
||||
Future<void> addScore({
|
||||
Future<bool> addScore({
|
||||
required String playerId,
|
||||
required String matchId,
|
||||
required ScoreEntry entry,
|
||||
}) async {
|
||||
await into(scoreEntryTable).insert(
|
||||
final rowsAffected = await into(scoreEntryTable).insert(
|
||||
ScoreEntryTableCompanion.insert(
|
||||
playerId: playerId,
|
||||
matchId: matchId,
|
||||
@@ -31,6 +31,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
Future<void> addScoresAsList({
|
||||
@@ -70,10 +72,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
}) async {
|
||||
final query = select(scoreEntryTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.playerId.equals(playerId) &
|
||||
tbl.matchId.equals(matchId) &
|
||||
tbl.roundNumber.equals(roundNumber),
|
||||
(s) =>
|
||||
s.playerId.equals(playerId) &
|
||||
s.matchId.equals(matchId) &
|
||||
s.roundNumber.equals(roundNumber),
|
||||
);
|
||||
|
||||
final result = await query.getSingleOrNull();
|
||||
@@ -91,7 +93,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
required String matchId,
|
||||
}) async {
|
||||
final query = select(scoreEntryTable)
|
||||
..where((tbl) => tbl.matchId.equals(matchId));
|
||||
..where((s) => s.matchId.equals(matchId));
|
||||
final result = await query.get();
|
||||
|
||||
final Map<String, ScoreEntry?> scoresByPlayer = {};
|
||||
@@ -113,10 +115,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
required String matchId,
|
||||
}) async {
|
||||
final query = select(scoreEntryTable)
|
||||
..where(
|
||||
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
|
||||
)
|
||||
..orderBy([(tbl) => OrderingTerm.asc(tbl.roundNumber)]);
|
||||
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId))
|
||||
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
|
||||
final result = await query.get();
|
||||
return result
|
||||
.map(
|
||||
@@ -138,8 +138,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
final query = selectOnly(scoreEntryTable)
|
||||
..where(scoreEntryTable.matchId.equals(matchId))
|
||||
..addColumns([scoreEntryTable.roundNumber.max()]);
|
||||
final row = await query.getSingle();
|
||||
return row.read(scoreEntryTable.roundNumber.max());
|
||||
final result = await query.getSingle();
|
||||
return result.read(scoreEntryTable.roundNumber.max());
|
||||
}
|
||||
|
||||
/// Aggregates the total score for a player in a match by summing all their
|
||||
@@ -168,10 +168,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(scoreEntryTable)..where(
|
||||
(tbl) =>
|
||||
tbl.playerId.equals(playerId) &
|
||||
tbl.matchId.equals(matchId) &
|
||||
tbl.roundNumber.equals(entry.roundNumber),
|
||||
(s) =>
|
||||
s.playerId.equals(playerId) &
|
||||
s.matchId.equals(matchId) &
|
||||
s.roundNumber.equals(entry.roundNumber),
|
||||
))
|
||||
.write(
|
||||
ScoreEntryTableCompanion(
|
||||
@@ -192,10 +192,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
}) async {
|
||||
final query = delete(scoreEntryTable)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.playerId.equals(playerId) &
|
||||
tbl.matchId.equals(matchId) &
|
||||
tbl.roundNumber.equals(roundNumber),
|
||||
(s) =>
|
||||
s.playerId.equals(playerId) &
|
||||
s.matchId.equals(matchId) &
|
||||
s.roundNumber.equals(roundNumber),
|
||||
);
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
@@ -203,7 +203,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
|
||||
Future<bool> deleteAllScoresForMatch({required String matchId}) async {
|
||||
final query = delete(scoreEntryTable)
|
||||
..where((tbl) => tbl.matchId.equals(matchId));
|
||||
..where((s) => s.matchId.equals(matchId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
@@ -213,9 +213,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
|
||||
required String playerId,
|
||||
}) async {
|
||||
final query = delete(scoreEntryTable)
|
||||
..where(
|
||||
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
|
||||
);
|
||||
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_table.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
|
||||
part 'statistic_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [StatisticTable])
|
||||
class StatisticDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$StatisticDaoMixin {
|
||||
StatisticDao(super.db);
|
||||
|
||||
/* Create */
|
||||
|
||||
Future<bool> addStatistic({required Statistic statistic}) async {
|
||||
await into(statisticTable).insert(
|
||||
StatisticTableCompanion.insert(
|
||||
id: statistic.id,
|
||||
type: statistic.type.name,
|
||||
timeframe: Value(statistic.timeframe?.name),
|
||||
displayCount: Value(statistic.displayCount),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
|
||||
await db.statisticScopeDao.addStatisticScopes(
|
||||
statisticId: statistic.id,
|
||||
scopes: statistic.scopes,
|
||||
);
|
||||
|
||||
if (statistic.selectedGroups != null) {
|
||||
await db.statisticGroupDao.addStatisticGroups(
|
||||
statisticId: statistic.id,
|
||||
groups: statistic.selectedGroups!,
|
||||
);
|
||||
}
|
||||
|
||||
if (statistic.selectedGames != null) {
|
||||
await db.statisticGameDao.addStatisticGames(
|
||||
statisticId: statistic.id,
|
||||
games: statistic.selectedGames!,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Read */
|
||||
|
||||
Future<Statistic?> getStatisticById(String statisticId) async {
|
||||
final query = select(statisticTable);
|
||||
final row = await query.getSingleOrNull();
|
||||
if (row != null) {
|
||||
final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id);
|
||||
final games = await db.statisticGameDao.getGamesForStatistic(row.id);
|
||||
final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id);
|
||||
|
||||
return Statistic(
|
||||
type: StatisticType.values.firstWhere((type) => type.name == row.type),
|
||||
scopes: scopes,
|
||||
timeframe: Timeframe.values.firstWhereOrNull(
|
||||
(t) => t.name == row.timeframe,
|
||||
),
|
||||
selectedGroups: groups,
|
||||
selectedGames: games,
|
||||
displayCount: row.displayCount,
|
||||
id: row.id,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Retrieves all statistics from the database, including their associated groups and games.
|
||||
Future<List<Statistic>> getAllStatistics() async {
|
||||
final query = select(statisticTable);
|
||||
final result = await query.get();
|
||||
return Future.wait(
|
||||
result.map((row) async {
|
||||
final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id);
|
||||
final games = await db.statisticGameDao.getGamesForStatistic(row.id);
|
||||
final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id);
|
||||
|
||||
return Statistic(
|
||||
type: StatisticType.values.firstWhere(
|
||||
(type) => type.name == row.type,
|
||||
),
|
||||
scopes: scopes,
|
||||
timeframe: Timeframe.values.firstWhereOrNull(
|
||||
(t) => t.name == row.timeframe,
|
||||
),
|
||||
selectedGroups: groups,
|
||||
selectedGames: games,
|
||||
displayCount: row.displayCount,
|
||||
id: row.id,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/* Update */
|
||||
|
||||
Future<bool> updateDisplayCount(String statisticId, int displayCount) async {
|
||||
final rowsUpdated =
|
||||
await (update(statisticTable)
|
||||
..where((tbl) => tbl.id.equals(statisticId)))
|
||||
.write(StatisticTableCompanion(displayCount: Value(displayCount)));
|
||||
|
||||
return rowsUpdated > 0;
|
||||
}
|
||||
|
||||
/* Delete */
|
||||
|
||||
Future<bool> deleteStatistic(String statisticId) async {
|
||||
final rowsDeleted = await (delete(
|
||||
statisticTable,
|
||||
)..where((tbl) => tbl.id.equals(statisticId))).go();
|
||||
|
||||
return rowsDeleted > 0;
|
||||
}
|
||||
|
||||
Future<bool> deleteAllStatistics() async {
|
||||
final rowsDeleted = await delete(statisticTable).go();
|
||||
return rowsDeleted > 0;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'statistic_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$StatisticDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
|
||||
StatisticDaoManager get managers => StatisticDaoManager(this);
|
||||
}
|
||||
|
||||
class StatisticDaoManager {
|
||||
final _$StatisticDaoMixin _db;
|
||||
StatisticDaoManager(this._db);
|
||||
$$StatisticTableTableTableManager get statisticTable =>
|
||||
$$StatisticTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticTable,
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_game_table.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
|
||||
part 'statistic_game_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [StatisticGameTable])
|
||||
class StatisticGameDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$StatisticGameDaoMixin {
|
||||
StatisticGameDao(super.db);
|
||||
|
||||
/// Retrieves a list of games associated with a specific statistic.
|
||||
Future<List<Game>?> getGamesForStatistic(String statisticId) async {
|
||||
final query = select(statisticGameTable).join([
|
||||
innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)),
|
||||
])..where(statisticGameTable.statisticId.equals(statisticId));
|
||||
|
||||
final results = await query.map((row) => row.readTable(gameTable)).get();
|
||||
if (results.isEmpty) return null;
|
||||
return results
|
||||
.map(
|
||||
(row) => Game(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
|
||||
description: row.description,
|
||||
color: AppColor.values.firstWhere((e) => e.name == row.color),
|
||||
icon: row.icon,
|
||||
createdAt: row.createdAt,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<bool> addStatisticGames({
|
||||
required String statisticId,
|
||||
required List<Game> games,
|
||||
}) {
|
||||
final entries = games
|
||||
.map(
|
||||
(game) => StatisticGameTableCompanion.insert(
|
||||
statisticId: statisticId,
|
||||
gameId: game.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return batch((batch) {
|
||||
batch.insertAll(
|
||||
statisticGameTable,
|
||||
entries,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}).then((_) => true).catchError((error) {
|
||||
print('Error adding statistic games: $error');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'statistic_game_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$StatisticGameDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
|
||||
$GameTableTable get gameTable => attachedDatabase.gameTable;
|
||||
$StatisticGameTableTable get statisticGameTable =>
|
||||
attachedDatabase.statisticGameTable;
|
||||
StatisticGameDaoManager get managers => StatisticGameDaoManager(this);
|
||||
}
|
||||
|
||||
class StatisticGameDaoManager {
|
||||
final _$StatisticGameDaoMixin _db;
|
||||
StatisticGameDaoManager(this._db);
|
||||
$$StatisticTableTableTableManager get statisticTable =>
|
||||
$$StatisticTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticTable,
|
||||
);
|
||||
$$GameTableTableTableManager get gameTable =>
|
||||
$$GameTableTableTableManager(_db.attachedDatabase, _db.gameTable);
|
||||
$$StatisticGameTableTableTableManager get statisticGameTable =>
|
||||
$$StatisticGameTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticGameTable,
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/db/tables/group_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_group_table.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
|
||||
part 'statistic_group_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [StatisticGroupTable, GroupTable])
|
||||
class StatisticGroupDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$StatisticGroupDaoMixin {
|
||||
StatisticGroupDao(super.db);
|
||||
|
||||
/// Retrieves a list of groups associated with a specific statistic.
|
||||
Future<List<Group>?> getGroupsForStatistic(String statisticId) async {
|
||||
final query = select(statisticGroupTable).join([
|
||||
innerJoin(
|
||||
groupTable,
|
||||
groupTable.id.equalsExp(statisticGroupTable.groupId),
|
||||
),
|
||||
])..where(statisticGroupTable.statisticId.equals(statisticId));
|
||||
|
||||
final results = await query.map((row) => row.readTable(groupTable)).get();
|
||||
if (results.isEmpty) return null;
|
||||
final groups = await Future.wait(
|
||||
results.map((result) async {
|
||||
final groupMembers = await db.playerGroupDao.getPlayersOfGroup(
|
||||
groupId: result.id,
|
||||
);
|
||||
return Group(
|
||||
id: result.id,
|
||||
createdAt: result.createdAt,
|
||||
name: result.name,
|
||||
description: result.description,
|
||||
members: groupMembers,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
Future<bool> addStatisticGroups({
|
||||
required String statisticId,
|
||||
required List<Group> groups,
|
||||
}) async {
|
||||
final entries = groups
|
||||
.map(
|
||||
(group) => StatisticGroupTableCompanion.insert(
|
||||
statisticId: statisticId,
|
||||
groupId: group.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return batch((batch) {
|
||||
batch.insertAll(
|
||||
statisticGroupTable,
|
||||
entries,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}).then((_) => true).catchError((error) {
|
||||
print('Error adding statistic groups: $error');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'statistic_group_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$StatisticGroupDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
|
||||
$GroupTableTable get groupTable => attachedDatabase.groupTable;
|
||||
$StatisticGroupTableTable get statisticGroupTable =>
|
||||
attachedDatabase.statisticGroupTable;
|
||||
StatisticGroupDaoManager get managers => StatisticGroupDaoManager(this);
|
||||
}
|
||||
|
||||
class StatisticGroupDaoManager {
|
||||
final _$StatisticGroupDaoMixin _db;
|
||||
StatisticGroupDaoManager(this._db);
|
||||
$$StatisticTableTableTableManager get statisticTable =>
|
||||
$$StatisticTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticTable,
|
||||
);
|
||||
$$GroupTableTableTableManager get groupTable =>
|
||||
$$GroupTableTableTableManager(_db.attachedDatabase, _db.groupTable);
|
||||
$$StatisticGroupTableTableTableManager get statisticGroupTable =>
|
||||
$$StatisticGroupTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticGroupTable,
|
||||
);
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_scope_table.dart';
|
||||
|
||||
part 'statistic_scope_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [StatisticScopeTable])
|
||||
class StatisticScopeDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$StatisticScopeDaoMixin {
|
||||
StatisticScopeDao(super.db);
|
||||
|
||||
/// Retrieves a list of statistic scopes associated with a specific statistic ID.
|
||||
Future<List<StatisticScope>> getScopeForStatistic(String statisticId) async {
|
||||
final query = select(statisticScopeTable)
|
||||
..where((tbl) => tbl.statisticId.equals(statisticId));
|
||||
|
||||
final result = await query.get();
|
||||
return result
|
||||
.map(
|
||||
(row) => StatisticScope.values.firstWhere(
|
||||
(e) => e.name == row.scope,
|
||||
orElse: () => throw Exception(
|
||||
'Invalid scope value: ${row.scope} for statistic ID: $statisticId',
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<bool> addStatisticScopes({
|
||||
required String statisticId,
|
||||
required List<StatisticScope> scopes,
|
||||
}) async {
|
||||
final entries = scopes
|
||||
.map(
|
||||
(scope) => StatisticScopeTableCompanion.insert(
|
||||
statisticId: statisticId,
|
||||
scope: scope.name,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
return batch((batch) {
|
||||
batch.insertAll(
|
||||
statisticScopeTable,
|
||||
entries,
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}).then((_) => true).catchError((error) {
|
||||
print('Error adding statistic scopes: $error');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'statistic_scope_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$StatisticScopeDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$StatisticTableTable get statisticTable => attachedDatabase.statisticTable;
|
||||
$StatisticScopeTableTable get statisticScopeTable =>
|
||||
attachedDatabase.statisticScopeTable;
|
||||
StatisticScopeDaoManager get managers => StatisticScopeDaoManager(this);
|
||||
}
|
||||
|
||||
class StatisticScopeDaoManager {
|
||||
final _$StatisticScopeDaoMixin _db;
|
||||
StatisticScopeDaoManager(this._db);
|
||||
$$StatisticTableTableTableManager get statisticTable =>
|
||||
$$StatisticTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticTable,
|
||||
);
|
||||
$$StatisticScopeTableTableTableManager get statisticScopeTable =>
|
||||
$$StatisticScopeTableTableTableManager(
|
||||
_db.attachedDatabase,
|
||||
_db.statisticScopeTable,
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
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';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/score_entry.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
|
||||
part 'team_dao.g.dart';
|
||||
@@ -22,6 +24,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 +60,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(),
|
||||
@@ -86,7 +92,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
|
||||
Future<int> getTeamCount() async {
|
||||
final count =
|
||||
await (selectOnly(teamTable)..addColumns([teamTable.id.count()]))
|
||||
.map((tbl) => tbl.read(teamTable.id.count()))
|
||||
.map((row) => row.read(teamTable.id.count()))
|
||||
.getSingle();
|
||||
return count ?? 0;
|
||||
}
|
||||
@@ -95,8 +101,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
|
||||
/// Returns `true` if the team exists, `false` otherwise.
|
||||
Future<bool> teamExists({required String teamId}) async {
|
||||
final query = select(teamTable)..where((t) => t.id.equals(teamId));
|
||||
final row = await query.getSingleOrNull();
|
||||
return row != null;
|
||||
final result = await query.getSingleOrNull();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// Retrieves all teams from the database.
|
||||
@@ -110,21 +116,43 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
createdAt: row.createdAt,
|
||||
color: AppColor.values.byName(row.color),
|
||||
score: row.score,
|
||||
members: members,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<Team>> getTeamsByMatchId({required String matchId}) async {
|
||||
final playerMatchQuery = select(db.playerMatchTable)
|
||||
..where((pm) => pm.matchId.equals(matchId));
|
||||
final playerMatches = await playerMatchQuery.get();
|
||||
|
||||
if (playerMatches.isEmpty) return [];
|
||||
|
||||
final teamIds = playerMatches
|
||||
.map((pm) => pm.teamId)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
|
||||
final teams = await Future.wait(
|
||||
teamIds.map((id) => getTeamById(teamId: id)),
|
||||
);
|
||||
return teams;
|
||||
}
|
||||
|
||||
/// Retrieves a [Team] by its [teamId], including its members.
|
||||
Future<Team> getTeamById({required String teamId}) async {
|
||||
final query = select(teamTable)..where((t) => t.id.equals(teamId));
|
||||
final row = await query.getSingle();
|
||||
final result = await query.getSingle();
|
||||
final members = await _getTeamMembers(teamId: teamId);
|
||||
return Team(
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
createdAt: row.createdAt,
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
createdAt: result.createdAt,
|
||||
color: AppColor.values.byName(result.color),
|
||||
score: result.score,
|
||||
members: members,
|
||||
);
|
||||
}
|
||||
@@ -133,19 +161,20 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
|
||||
Future<List<Player>> _getTeamMembers({required String teamId}) async {
|
||||
// Get all player_match entries with this teamId
|
||||
final playerMatchQuery = select(db.playerMatchTable)
|
||||
..where((tbl) => tbl.teamId.equals(teamId));
|
||||
..where((pm) => pm.teamId.equals(teamId));
|
||||
final playerMatches = await playerMatchQuery.get();
|
||||
|
||||
if (playerMatches.isEmpty) return [];
|
||||
|
||||
// Get unique player IDs
|
||||
final playerIds = playerMatches.map((tbl) => tbl.playerId).toSet();
|
||||
final playerIds = playerMatches.map((pm) => pm.playerId).toSet();
|
||||
|
||||
// Fetch all players
|
||||
final players = await Future.wait(
|
||||
playerIds.map((id) => db.playerDao.getPlayerById(playerId: id)),
|
||||
);
|
||||
return players;
|
||||
return players
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
}
|
||||
|
||||
/* Update */
|
||||
@@ -156,12 +185,87 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
|
||||
required String name,
|
||||
}) async {
|
||||
final rowsAffected =
|
||||
await (update(teamTable)..where((tbl) => tbl.id.equals(teamId))).write(
|
||||
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
|
||||
TeamTableCompanion(name: Value(name)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/// Updates the color of the team with the given [teamId].
|
||||
Future<bool> updateTeamColor({
|
||||
required String teamId,
|
||||
required AppColor 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].
|
||||
/// Updates the member scores correspondingly
|
||||
Future<bool> updateTeamScore({
|
||||
required String teamId,
|
||||
required String matchId,
|
||||
required int score,
|
||||
}) async {
|
||||
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
|
||||
const TeamTableCompanion(score: Value(null)),
|
||||
);
|
||||
await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId);
|
||||
|
||||
final rowsAffected =
|
||||
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
|
||||
TeamTableCompanion(score: Value(score)),
|
||||
);
|
||||
|
||||
final members = await _getTeamMembers(teamId: teamId);
|
||||
for (final member in members) {
|
||||
await db.scoreEntryDao.addScore(
|
||||
playerId: member.id,
|
||||
matchId: matchId,
|
||||
entry: ScoreEntry(score: score),
|
||||
);
|
||||
}
|
||||
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
Future<bool> removeScoreForTeam({
|
||||
required String teamId,
|
||||
required String matchId,
|
||||
}) async {
|
||||
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
|
||||
const TeamTableCompanion(score: Value(null)),
|
||||
);
|
||||
await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Removes the scores for all teams in the match with the given [matchId] by setting their scores to null.
|
||||
Future<bool> removeAllTeamScores({required String matchId}) async {
|
||||
// collect all teamIds for the given matchId from playerMatchTable
|
||||
final teamIds =
|
||||
await (selectOnly(playerMatchTable)
|
||||
..addColumns([playerMatchTable.teamId])
|
||||
..where(playerMatchTable.matchId.equals(matchId)))
|
||||
.map((row) => row.read(playerMatchTable.teamId))
|
||||
.get();
|
||||
|
||||
// filter null or duplicates
|
||||
final filteredTeamIds = teamIds.whereType<String>().toSet().toList();
|
||||
|
||||
var rowsAffected = 0;
|
||||
if (filteredTeamIds.isNotEmpty) {
|
||||
rowsAffected =
|
||||
await (update(teamTable)..where((t) => t.id.isIn(filteredTeamIds)))
|
||||
.write(const TeamTableCompanion(score: Value(null)));
|
||||
}
|
||||
await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/* Delete */
|
||||
|
||||
/// Deletes all teams from the database.
|
||||
@@ -175,8 +279,96 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
|
||||
/// Deletes the team with the given [teamId] from the database.
|
||||
/// Returns `true` if the team was deleted, `false` otherwise.
|
||||
Future<bool> deleteTeam({required String teamId}) async {
|
||||
final query = delete(teamTable)..where((tbl) => tbl.id.equals(teamId));
|
||||
final query = delete(teamTable)..where((t) => t.id.equals(teamId));
|
||||
final rowsAffected = await query.go();
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
|
||||
/* Score handling */
|
||||
|
||||
/// Sets the team with the given [teamId] as the winner of the match with the given [matchId] by assigning a score of 1.
|
||||
/// Returns `true` if the score was updated successfully, `false` otherwise.
|
||||
Future<bool> setWinnerTeam({
|
||||
required String teamId,
|
||||
required String matchId,
|
||||
}) async {
|
||||
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1);
|
||||
}
|
||||
|
||||
/// Sets multiple teams as winners of the match with the given [matchId] by assigning a score of 1 to each team.
|
||||
/// Returns `true` if all scores were updated successfully, `false` otherwise.
|
||||
Future<bool> setWinnerTeams({
|
||||
required List<Team> winners,
|
||||
required String matchId,
|
||||
}) async {
|
||||
// Reset all team scores .
|
||||
await removeAllTeamScores(matchId: matchId);
|
||||
// Reset all score entries
|
||||
for (final team in winners) {
|
||||
await _deleteAllScoresForMembersOfTeam(teamId: team.id, matchId: matchId);
|
||||
}
|
||||
|
||||
for (final team in winners) {
|
||||
await updateTeamScore(teamId: team.id, matchId: matchId, score: 1);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Removes the winner status from all Teams with the given [matchId] by setting its score to null.
|
||||
/// Returns `true` if the score was updated successfully, `false` otherwise.
|
||||
Future<bool> removeWinnerTeam({required String matchId}) async {
|
||||
return await removeAllTeamScores(matchId: matchId);
|
||||
}
|
||||
|
||||
/// Sets the team with the given [teamId] as the loser of the match with the given [matchId] by assigning a score of 0.
|
||||
/// Returns `true` if the score was updated successfully, `false` otherwise.
|
||||
Future<bool> setLoserTeam({
|
||||
required String teamId,
|
||||
required String matchId,
|
||||
}) async {
|
||||
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 0);
|
||||
}
|
||||
|
||||
/// Removes the loser from the match with the given [matchId] by setting its score to null.
|
||||
/// Returns `true` if the score was updated successfully, `false` otherwise.
|
||||
Future<bool> removeLoserTeam({required String matchId}) async {
|
||||
return await removeAllTeamScores(matchId: matchId);
|
||||
}
|
||||
|
||||
/// Sets the placements for the teams in the match with the given [matchId] by assigning scores based on their order in the [teams] list.
|
||||
/// Returns `true` if all scores were updated successfully, `false` otherwise.
|
||||
Future<bool> setTeamPlacements({
|
||||
required String matchId,
|
||||
required List<Team> teams,
|
||||
}) async {
|
||||
List<bool?> success = List.generate(teams.length, (index) => null);
|
||||
for (int i = 0; i < teams.length; i++) {
|
||||
success[i] = await updateTeamScore(
|
||||
matchId: matchId,
|
||||
teamId: teams[i].id,
|
||||
score: teams.length - i,
|
||||
);
|
||||
}
|
||||
return success.every((result) => result == true);
|
||||
}
|
||||
|
||||
/// Helper method to delete all scores for members of a team in a specific match.
|
||||
Future<bool> _deleteAllScoresForMembersOfTeam({
|
||||
required String teamId,
|
||||
required String matchId,
|
||||
}) async {
|
||||
final playerMatchQuery = select(db.playerMatchTable)
|
||||
..where((pm) => pm.teamId.equals(teamId) & pm.matchId.equals(matchId));
|
||||
final playerMatches = await playerMatchQuery.get();
|
||||
|
||||
if (playerMatches.isEmpty) return false;
|
||||
|
||||
for (final pm in playerMatches) {
|
||||
await db.scoreEntryDao.deleteAllScoresForPlayerInMatch(
|
||||
playerId: pm.playerId,
|
||||
matchId: matchId,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ import 'package:tallee/data/dao/player_dao.dart';
|
||||
import 'package:tallee/data/dao/player_group_dao.dart';
|
||||
import 'package:tallee/data/dao/player_match_dao.dart';
|
||||
import 'package:tallee/data/dao/score_entry_dao.dart';
|
||||
import 'package:tallee/data/dao/statistic_dao.dart';
|
||||
import 'package:tallee/data/dao/statistic_game_dao.dart';
|
||||
import 'package:tallee/data/dao/statistic_group_dao.dart';
|
||||
import 'package:tallee/data/dao/statistic_scope_dao.dart';
|
||||
import 'package:tallee/data/dao/team_dao.dart';
|
||||
import 'package:tallee/data/db/tables/game_table.dart';
|
||||
import 'package:tallee/data/db/tables/group_table.dart';
|
||||
@@ -20,10 +16,6 @@ import 'package:tallee/data/db/tables/player_group_table.dart';
|
||||
import 'package:tallee/data/db/tables/player_match_table.dart';
|
||||
import 'package:tallee/data/db/tables/player_table.dart';
|
||||
import 'package:tallee/data/db/tables/score_entry_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_game_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_group_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_scope_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_table.dart';
|
||||
import 'package:tallee/data/db/tables/team_table.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
@@ -38,10 +30,6 @@ part 'database.g.dart';
|
||||
GameTable,
|
||||
TeamTable,
|
||||
ScoreEntryTable,
|
||||
StatisticTable,
|
||||
StatisticScopeTable,
|
||||
StatisticGameTable,
|
||||
StatisticGroupTable,
|
||||
],
|
||||
daos: [
|
||||
PlayerDao,
|
||||
@@ -52,10 +40,6 @@ part 'database.g.dart';
|
||||
GameDao,
|
||||
ScoreEntryDao,
|
||||
TeamDao,
|
||||
StatisticDao,
|
||||
StatisticScopeDao,
|
||||
StatisticGameDao,
|
||||
StatisticGroupDao,
|
||||
],
|
||||
)
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()();
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/data/db/tables/game_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_table.dart';
|
||||
|
||||
class StatisticGameTable extends Table {
|
||||
TextColumn get statisticId =>
|
||||
text().references(StatisticTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get gameId =>
|
||||
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {statisticId, gameId};
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/data/db/tables/group_table.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_table.dart';
|
||||
|
||||
class StatisticGroupTable extends Table {
|
||||
TextColumn get statisticId =>
|
||||
text().references(StatisticTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get groupId =>
|
||||
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {statisticId, groupId};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:tallee/data/db/tables/statistic_table.dart';
|
||||
|
||||
class StatisticScopeTable extends Table {
|
||||
TextColumn get statisticId =>
|
||||
text().references(StatisticTable, #id, onDelete: KeyAction.cascade)();
|
||||
TextColumn get scope => text()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {statisticId, scope};
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class StatisticTable extends Table {
|
||||
TextColumn get id => text()();
|
||||
TextColumn get type => text()();
|
||||
TextColumn get timeframe => text().nullable()();
|
||||
IntColumn get displayCount => integer().withDefault(const Constant(5))();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
}
|
||||
@@ -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().nullable()();
|
||||
|
||||
@override
|
||||
Set<Column<Object>> get primaryKey => {id};
|
||||
|
||||
@@ -73,7 +73,10 @@ class Game {
|
||||
orElse: () => Ruleset.singleWinner,
|
||||
),
|
||||
description = json['description'],
|
||||
color = AppColor.values.firstWhere((e) => e.name == json['color']),
|
||||
color = AppColor.values.firstWhere(
|
||||
(e) => e.name == json['color'],
|
||||
orElse: () => AppColor.orange,
|
||||
),
|
||||
icon = json['icon'];
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
|
||||
@@ -5,17 +5,17 @@ import 'package:uuid/uuid.dart';
|
||||
|
||||
class Group {
|
||||
final String id;
|
||||
final DateTime createdAt;
|
||||
final String name;
|
||||
final List<Player> members;
|
||||
final String description;
|
||||
final DateTime createdAt;
|
||||
final List<Player> members;
|
||||
|
||||
Group({
|
||||
required this.name,
|
||||
required this.members,
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
required this.name,
|
||||
String? description,
|
||||
required this.members,
|
||||
}) : id = id ?? const Uuid().v4(),
|
||||
createdAt = createdAt ?? clock.now(),
|
||||
description = description ?? '';
|
||||
|
||||
@@ -16,9 +16,10 @@ 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;
|
||||
final Map<String, ScoreEntry?> scores;
|
||||
|
||||
Match({
|
||||
required this.name,
|
||||
@@ -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,59 @@ 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() {
|
||||
if (teams!.every((team) => team.score == null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final int highestScore = teams!
|
||||
.map((team) => team.score)
|
||||
.whereType<int>()
|
||||
.reduce((max, score) => score > max ? score : max);
|
||||
|
||||
return teams!.where((team) {
|
||||
return team.score == highestScore;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
List<Team> _getLowestScoreTeam() {
|
||||
if (teams!.every((team) => team.score == null)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final int lowestScore = teams!
|
||||
.map((team) => team.score)
|
||||
.whereType<int>()
|
||||
.reduce((min, score) => score < min ? score : min);
|
||||
|
||||
return teams!.where((team) {
|
||||
return team.score == lowestScore;
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
class Statistic {
|
||||
final String id;
|
||||
final StatisticType type;
|
||||
final List<StatisticScope> scopes;
|
||||
final Timeframe? timeframe;
|
||||
final List<Group>? selectedGroups;
|
||||
final List<Game>? selectedGames;
|
||||
final int displayCount;
|
||||
|
||||
Statistic({
|
||||
required this.type,
|
||||
required this.scopes,
|
||||
this.timeframe,
|
||||
this.selectedGroups,
|
||||
this.selectedGames,
|
||||
this.displayCount = 5,
|
||||
String? id,
|
||||
}) : id = id ?? const Uuid().v4();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Statistic(id: $id, type: $type, scopes: $scopes, timeframe: $timeframe, selectedGroups: $selectedGroups, selectedGames: $selectedGames)';
|
||||
}
|
||||
|
||||
Statistic copyWith({
|
||||
StatisticType? type,
|
||||
List<StatisticScope>? scopes,
|
||||
Timeframe? timeframe,
|
||||
List<Group>? selectedGroups,
|
||||
List<Game>? selectedGames,
|
||||
int? displayCount,
|
||||
}) {
|
||||
return Statistic(
|
||||
id: id,
|
||||
type: type ?? this.type,
|
||||
scopes: scopes ?? this.scopes,
|
||||
timeframe: timeframe ?? this.timeframe,
|
||||
selectedGroups: selectedGroups ?? this.selectedGroups,
|
||||
selectedGames: selectedGames ?? this.selectedGames,
|
||||
displayCount: displayCount ?? this.displayCount,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 AppColor color;
|
||||
final int? score;
|
||||
final List<Player> members;
|
||||
|
||||
Team({
|
||||
String? id,
|
||||
required this.name,
|
||||
DateTime? createdAt,
|
||||
this.color = AppColor.blue,
|
||||
this.score,
|
||||
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,
|
||||
AppColor? 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,19 @@ class Team {
|
||||
: id = json['id'],
|
||||
name = json['name'],
|
||||
createdAt = DateTime.parse(json['createdAt']),
|
||||
color = AppColor.values.firstWhere(
|
||||
(e) => e.name == json['color'],
|
||||
orElse: () => AppColor.orange,
|
||||
),
|
||||
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(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
"add_team": "Team hinzufügen",
|
||||
"all_players": "Alle Spieler:innen",
|
||||
"all_players_selected": "Alle Spieler:innen ausgewählt",
|
||||
"all_time": "Gesamter Zeitraum",
|
||||
"amount_of_matches": "Anzahl der Spiele",
|
||||
"app_name": "Tallee",
|
||||
"average_score": "Durchschnittliche Punktzahl",
|
||||
"best_player": "Beste:r Spieler:in",
|
||||
"best_score": "Beste Punktzahl",
|
||||
"cancel": "Abbrechen",
|
||||
"choose_color": "Farbe wählen",
|
||||
"choose_game": "Spielvorlage wählen",
|
||||
"choose_group": "Gruppe wählen",
|
||||
"choose_ruleset": "Regelwerk wählen",
|
||||
"classifier": "Klassifikator",
|
||||
"color": "Farbe",
|
||||
"color_blue": "Blau",
|
||||
"color_green": "Grün",
|
||||
@@ -23,31 +20,13 @@
|
||||
"color_red": "Rot",
|
||||
"color_teal": "Türkis",
|
||||
"color_yellow": "Gelb",
|
||||
"confirm": "Bestätigen",
|
||||
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
|
||||
"@could_not_add_player": {
|
||||
"placeholders": {
|
||||
"playerName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"create_game": "Spielvorlage erstellen",
|
||||
"create_group": "Gruppe erstellen",
|
||||
"create_match": "Spiel erstellen",
|
||||
"create_new_group": "Neue Gruppe erstellen",
|
||||
"create_new_match": "Neues Spiel erstellen",
|
||||
"create_statistic": "Statistik erstellen",
|
||||
"create_statistic_classifier_subtitle": "Wähle die anzuzeigende Hauptmetrik aus",
|
||||
"create_statistic_classifier_title": "Klassifikator",
|
||||
"create_statistic_games_subtitle": "Wähle die gefilterten Spielvorlagen",
|
||||
"create_statistic_games_title": "Spielvorlagen",
|
||||
"create_statistic_groups_subtitle": "Wähle die gefilterten Gruppen",
|
||||
"create_statistic_groups_title": "Gruppen",
|
||||
"create_statistic_scope_subtitle": "Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.",
|
||||
"create_statistic_scope_title": "Bereich",
|
||||
"create_statistic_timeframe_subtitle": "Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.",
|
||||
"create_statistic_timeframe_title": "Zeitraum",
|
||||
"create_teams": "Teams erstellen",
|
||||
"created_on": "Erstellt am",
|
||||
"data": "Daten",
|
||||
"data_successfully_deleted": "Daten erfolgreich gelöscht",
|
||||
@@ -67,15 +46,11 @@
|
||||
},
|
||||
"delete_group": "Gruppe löschen",
|
||||
"delete_match": "Spiel löschen",
|
||||
"delete_player": "Spieler:in löschen",
|
||||
"description": "Beschreibung",
|
||||
"displayed_entries": "Angezeigte Einträge",
|
||||
"drag_to_set_placement": "Ziehen um Platzierung zu setzen",
|
||||
"edit_game": "Spielvorlage bearbeiten",
|
||||
"edit_group": "Gruppe bearbeiten",
|
||||
"edit_match": "Gruppe bearbeiten",
|
||||
"edit_name": "Name ändern",
|
||||
"edit_player": "Spieler bearbeiten",
|
||||
"enter_points": "Punkte eingeben",
|
||||
"enter_results": "Ergebnisse eintragen",
|
||||
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
||||
@@ -86,42 +61,32 @@
|
||||
"exit_view": "Ansicht verlassen",
|
||||
"export_canceled": "Export abgebrochen",
|
||||
"export_data": "Daten exportieren",
|
||||
"filter": "Filter",
|
||||
"format_exception": "Formatfehler (siehe Konsole)",
|
||||
"game": "Spielvorlage",
|
||||
"game_name": "Spielvorlagenname",
|
||||
"games": "Spielvorlagen",
|
||||
"group": "Gruppe",
|
||||
"group_name": "Gruppenname",
|
||||
"group_profile": "Gruppenprofil",
|
||||
"groups": "Gruppen",
|
||||
"groups_part_of": "Gruppen Teil von",
|
||||
"highest_score": "Höchste Punkte",
|
||||
"home": "Startseite",
|
||||
"import_canceled": "Import abgebrochen",
|
||||
"import_data": "Daten importieren",
|
||||
"info": "Info",
|
||||
"invalid_schema": "Ungültiges Schema",
|
||||
"last_180_days": "Letzte 180 Tage",
|
||||
"last_30_days": "Letzte 30 Tage",
|
||||
"last_7_days": "Letzte 7 Tage",
|
||||
"last_90_days": "Letzte 90 Tage",
|
||||
"last_year": "Letztes Jahr",
|
||||
"least_points": "Niedrigste Punkte",
|
||||
"legal": "Rechtliches",
|
||||
"legal_notice": "Impressum",
|
||||
"licenses": "Lizenzen",
|
||||
"live_edit_mode": "Live-Bearbeitungsmodus",
|
||||
"loading": "Lädt...",
|
||||
"loser": "Verlierer:in",
|
||||
"lowest_score": "Niedrigste Punkte",
|
||||
"manage_members": "Mitglieder bearbeiten",
|
||||
"match_in_progress": "Spiel läuft...",
|
||||
"match_name": "Spieltitel",
|
||||
"match_profile": "Spielprofil",
|
||||
"matches": "Spiele",
|
||||
"matches_part_of": "Spiele Teil von",
|
||||
"matches_played": "Spiele gespielt",
|
||||
"matches_won": "Spiele gewonnen",
|
||||
"member": "Mitglied",
|
||||
"members": "Mitglieder",
|
||||
"most_points": "Höchste Punkte",
|
||||
"multiple_winners": "Mehrere Gewinner:innen",
|
||||
@@ -131,7 +96,7 @@
|
||||
"no_license_text_available": "Kein Lizenztext verfügbar",
|
||||
"no_licenses_found": "Keine Lizenzen gefunden",
|
||||
"no_matches_created_yet": "Noch keine Spiele erstellt",
|
||||
"no_matches_played_yet": "Noch kein Spiel gespielt",
|
||||
"no_players_available": "Keine Spieler:innen verfügbar",
|
||||
"no_players_created_yet": "Noch keine Spieler:in erstellt",
|
||||
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
|
||||
"no_players_selected": "Keine Spieler:innen ausgewählt",
|
||||
@@ -139,22 +104,21 @@
|
||||
"no_results_entered_yet": "Noch keine Ergebnisse eingetragen",
|
||||
"no_second_match_available": "Kein zweites Spiel verfügbar",
|
||||
"no_statistics_available": "Keine Statistiken verfügbar",
|
||||
"no_statistics_created_yet": "Noch keine Statistiken erstellt",
|
||||
"no_teams_available": "Keine Teams verfügbar",
|
||||
"none": "Kein",
|
||||
"none_group": "Keine",
|
||||
"not_available": "Nicht verfügbar",
|
||||
"not_part_of_any_group": "Noch keiner Gruppe hinzugefügt",
|
||||
"place": "Platz",
|
||||
"placement": "Platzierung",
|
||||
"played_matches": "Gespielte Spiele",
|
||||
"player_name": "Spieler:innenname",
|
||||
"player_profile": "Spieler:in-Profil",
|
||||
"players": "Spieler:innen",
|
||||
"point": "Punkt",
|
||||
"points": "Punkte",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"quick_create": "Schnellzugriff",
|
||||
"recent_matches": "Letzte Spiele",
|
||||
"redistribute": "Neu verteilen",
|
||||
"result": "Ergebnis",
|
||||
"results": "Ergebnisse",
|
||||
"ruleset": "Regelwerk",
|
||||
@@ -164,55 +128,31 @@
|
||||
"ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.",
|
||||
"ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"scope": "Bereich",
|
||||
"search_for_groups": "Nach Gruppen suchen",
|
||||
"search_for_players": "Nach Spieler:innen suchen",
|
||||
"select_a_classifier": "Klassifikator auswählen",
|
||||
"select_a_game": "Spielvorlage auswählen",
|
||||
"select_a_group": "Gruppe auswählen",
|
||||
"select_a_scope": "Bereich auswählen",
|
||||
"select_a_timeframe": "Zeitraum auswählen",
|
||||
"select_a_timeframe_for_which_data_will_be_filtered": "Wähle einen Zeitraum, für den die Daten gefiltert werden sollen",
|
||||
"select_loser": "Verlierer:in wählen",
|
||||
"select_the_filtered_games": "Wähle Spiele, nach denen gefiltert werden soll.",
|
||||
"select_the_filtered_groups": "Wähle Gruppen, nach denen gefiltert werden soll.",
|
||||
"select_the_filtered_timeframe": "Wähle einen Zeitraum, nach dem gefiltert werden soll.",
|
||||
"select_winner": "Gewinner:in wählen",
|
||||
"select_winners": "Gewinner:innen wählen",
|
||||
"selected_games": "Ausgewählte Spielvorlagen",
|
||||
"selected_groups": "Ausgewählte Gruppen",
|
||||
"selected_players": "Ausgewählte Spieler:innen",
|
||||
"set_name": "Name setzen",
|
||||
"settings": "Einstellungen",
|
||||
"single_loser": "Ein:e Verlierer:in",
|
||||
"single_winner": "Ein:e Gewinner:in",
|
||||
"statistic_type_average_score": "Durchschnittliche Punktzahl",
|
||||
"statistic_type_best_score": "Beste Punktzahl",
|
||||
"statistic_type_total_losses": "Niederlagen insgesamt",
|
||||
"statistic_type_total_matches": "Spiele insgesamt",
|
||||
"statistic_type_total_score": "Punktzahl insgesamt",
|
||||
"statistic_type_total_wins": "Siege insgesamt",
|
||||
"statistic_type_winrate": "Siegquote",
|
||||
"statistic_type_worst_score": "Schlechteste Punktzahl",
|
||||
"statistics": "Statistiken",
|
||||
"stats": "Statistiken",
|
||||
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
|
||||
"team": "Team",
|
||||
"team_match": "Teamspiel",
|
||||
"teams": "Teams",
|
||||
"there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht",
|
||||
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
|
||||
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
|
||||
"tie": "Unentschieden",
|
||||
"timeframe": "Zeitraum",
|
||||
"today_at": "Heute um",
|
||||
"total_losses": "Niederlagen insgesamt",
|
||||
"total_matches": "Spiele insgesamt",
|
||||
"total_score": "Punktzahl insgesamt",
|
||||
"total_wins": "Siege insgesamt",
|
||||
"undo": "Rückgängig",
|
||||
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
|
||||
"winner": "Gewinner:in",
|
||||
"winners": "Gewinner:innen",
|
||||
"winrate": "Siegquote",
|
||||
"wins": "Siege",
|
||||
"worst_score": "Schlechteste Punktzahl",
|
||||
"yesterday_at": "Gestern um"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"add_team": "Add Team",
|
||||
"all_players": "All players",
|
||||
"all_players_selected": "All players selected",
|
||||
"amount_of_matches": "Amount of Matches",
|
||||
@@ -18,30 +19,14 @@
|
||||
"color_purple": "Purple",
|
||||
"color_red": "Red",
|
||||
"color_teal": "Teal",
|
||||
"displayed_entries": "Displayed entries",
|
||||
"color_yellow": "Yellow",
|
||||
"confirm": "Confirm",
|
||||
"could_not_add_player": "Could not add player {playerName}",
|
||||
"@could_not_add_player": {
|
||||
"placeholders": {
|
||||
"playerName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"could_not_add_player": "Could not add player",
|
||||
"create_game": "Create Game",
|
||||
"create_group": "Create Group",
|
||||
"create_match": "Create match",
|
||||
"create_new_group": "Create new group",
|
||||
"create_new_match": "Create new match",
|
||||
"create_statistic": "Create statistic",
|
||||
"classifier": "Classifier",
|
||||
"select_the_filtered_timeframe": "Select the timeframe you want to filter by.",
|
||||
"select_the_filtered_games": "Select the games you want to filter by.",
|
||||
"games": "Games",
|
||||
"select_the_filtered_groups": "Select the groups you want to filter by.",
|
||||
"scope": "Scope",
|
||||
"timeframe": "Timeframe",
|
||||
"create_teams": "Create teams",
|
||||
"created_on": "Created on",
|
||||
"data": "Data",
|
||||
"data_successfully_deleted": "Data successfully deleted",
|
||||
@@ -59,17 +44,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"filter": "Filter",
|
||||
"delete_group": "Delete Group",
|
||||
"delete_match": "Delete Match",
|
||||
"delete_player": "Delete player?",
|
||||
"description": "Description",
|
||||
"drag_to_set_placement": "Drag to set placement",
|
||||
"edit_game": "Edit Game",
|
||||
"edit_group": "Edit Group",
|
||||
"edit_match": "Edit Match",
|
||||
"edit_name": "Edit name",
|
||||
"edit_player": "Edit player",
|
||||
"enter_points": "Enter points",
|
||||
"enter_results": "Enter Results",
|
||||
"error_creating_group": "Error while creating group, please try again",
|
||||
@@ -87,7 +68,6 @@
|
||||
"group_name": "Group name",
|
||||
"group_profile": "Group Profile",
|
||||
"groups": "Groups",
|
||||
"groups_part_of": "Groups part of",
|
||||
"highest_score": "Highest Score",
|
||||
"home": "Home",
|
||||
"import_canceled": "Import canceled",
|
||||
@@ -99,16 +79,14 @@
|
||||
"legal_notice": "Legal Notice",
|
||||
"licenses": "Licenses",
|
||||
"live_edit_mode": "Live Edit Mode",
|
||||
"loading": "Loading...",
|
||||
"loser": "Loser",
|
||||
"lowest_score": "Lowest Score",
|
||||
"manage_members": "Manage Members",
|
||||
"match_in_progress": "Match in progress...",
|
||||
"match_name": "Match name",
|
||||
"match_profile": "Match Profile",
|
||||
"matches": "Matches",
|
||||
"matches_part_of": "Matches part of",
|
||||
"matches_played": "Matches played",
|
||||
"matches_won": "Matches won",
|
||||
"member": "Member",
|
||||
"members": "Members",
|
||||
"most_points": "Most Points",
|
||||
"multiple_winners": "Multiple Winners",
|
||||
@@ -118,7 +96,7 @@
|
||||
"no_license_text_available": "No license text available",
|
||||
"no_licenses_found": "No licenses found",
|
||||
"no_matches_created_yet": "No matches created yet",
|
||||
"no_matches_played_yet": "No games played yet",
|
||||
"no_players_available": "No players available",
|
||||
"no_players_created_yet": "No players created yet",
|
||||
"no_players_found_with_that_name": "No players found with that name",
|
||||
"no_players_selected": "No players selected",
|
||||
@@ -126,22 +104,21 @@
|
||||
"no_results_entered_yet": "No results entered yet",
|
||||
"no_second_match_available": "No second match available",
|
||||
"no_statistics_available": "No statistics available",
|
||||
"no_statistics_created_yet": "No statistics created yet",
|
||||
"no_teams_available": "No teams available",
|
||||
"none": "None",
|
||||
"none_group": "None",
|
||||
"not_available": "Not available",
|
||||
"not_part_of_any_group": "Not part of any group yet",
|
||||
"place": "place",
|
||||
"placement": "Placement",
|
||||
"played_matches": "Played Matches",
|
||||
"player_name": "Player name",
|
||||
"player_profile": "Player Profile",
|
||||
"players": "Players",
|
||||
"point": "Point",
|
||||
"points": "Points",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"quick_create": "Quick Create",
|
||||
"recent_matches": "Recent Matches",
|
||||
"redistribute": "Redistribute",
|
||||
"results": "Results",
|
||||
"ruleset": "Ruleset",
|
||||
"ruleset_least_points": "Inverse scoring: the player with the fewest points wins.",
|
||||
@@ -156,26 +133,11 @@
|
||||
"select_winner": "Select Winner",
|
||||
"select_winners": "Select Winners",
|
||||
"selected_players": "Selected players",
|
||||
"set_name": "Set name",
|
||||
"settings": "Settings",
|
||||
"select_a_classifier": "Select a classifier",
|
||||
"select_a_game": "Select a game",
|
||||
"select_a_group": "Select a group",
|
||||
"select_a_scope": "Select a scope",
|
||||
"select_a_timeframe": "Select a timeframe",
|
||||
"single_loser": "Single Loser",
|
||||
"single_winner": "Single Winner",
|
||||
"statistics": "Statistics",
|
||||
"stats": "Stats",
|
||||
"selected_games": "Selected games",
|
||||
"selected_groups": "Selected groups",
|
||||
"average_score": "Average score",
|
||||
"best_score": "Best score",
|
||||
"total_losses": "Total losses",
|
||||
"total_matches": "Total matches",
|
||||
"total_score": "Total score",
|
||||
"total_wins": "Total wins",
|
||||
"worst_score": "Worst score",
|
||||
"successfully_added_player": "Successfully added player {playerName}",
|
||||
"@successfully_added_player": {
|
||||
"description": "Success message when adding a player",
|
||||
@@ -186,16 +148,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"team": "Team",
|
||||
"team_match": "Team Match",
|
||||
"teams": "Teams",
|
||||
"there_are_no_games_matching_your_search": "There are no games matching your search",
|
||||
"there_is_no_group_matching_your_search": "There is no group matching your search",
|
||||
"this_cannot_be_undone": "This can't be undone.",
|
||||
"tie": "Tie",
|
||||
"all_time": "All time",
|
||||
"last_180_days": "Last 180 days",
|
||||
"last_30_days": "Last 30 days",
|
||||
"last_7_days": "Last 7 days",
|
||||
"last_90_days": "Last 90 days",
|
||||
"last_year": "Last year",
|
||||
"today_at": "Today at",
|
||||
"undo": "Undo",
|
||||
"unknown_exception": "Unknown Exception (see console)",
|
||||
|
||||
@@ -98,6 +98,12 @@ abstract class AppLocalizations {
|
||||
Locale('en'),
|
||||
];
|
||||
|
||||
/// No description provided for @add_team.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add Team'**
|
||||
String get add_team;
|
||||
|
||||
/// No description provided for @all_players.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -206,29 +212,17 @@ abstract class AppLocalizations {
|
||||
/// **'Teal'**
|
||||
String get color_teal;
|
||||
|
||||
/// No description provided for @displayed_entries.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Displayed entries'**
|
||||
String get displayed_entries;
|
||||
|
||||
/// No description provided for @color_yellow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yellow'**
|
||||
String get color_yellow;
|
||||
|
||||
/// No description provided for @confirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Confirm'**
|
||||
String get confirm;
|
||||
|
||||
/// No description provided for @could_not_add_player.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Could not add player {playerName}'**
|
||||
String could_not_add_player(String playerName);
|
||||
/// **'Could not add player'**
|
||||
String could_not_add_player(Object playerName);
|
||||
|
||||
/// No description provided for @create_game.
|
||||
///
|
||||
@@ -260,53 +254,11 @@ abstract class AppLocalizations {
|
||||
/// **'Create new match'**
|
||||
String get create_new_match;
|
||||
|
||||
/// No description provided for @create_statistic.
|
||||
/// No description provided for @create_teams.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create statistic'**
|
||||
String get create_statistic;
|
||||
|
||||
/// No description provided for @classifier.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Classifier'**
|
||||
String get classifier;
|
||||
|
||||
/// No description provided for @select_the_filtered_timeframe.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select the timeframe you want to filter by.'**
|
||||
String get select_the_filtered_timeframe;
|
||||
|
||||
/// No description provided for @select_the_filtered_games.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select the games you want to filter by.'**
|
||||
String get select_the_filtered_games;
|
||||
|
||||
/// No description provided for @games.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Games'**
|
||||
String get games;
|
||||
|
||||
/// No description provided for @select_the_filtered_groups.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select the groups you want to filter by.'**
|
||||
String get select_the_filtered_groups;
|
||||
|
||||
/// No description provided for @scope.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Scope'**
|
||||
String get scope;
|
||||
|
||||
/// No description provided for @timeframe.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Timeframe'**
|
||||
String get timeframe;
|
||||
/// **'Create teams'**
|
||||
String get create_teams;
|
||||
|
||||
/// No description provided for @created_on.
|
||||
///
|
||||
@@ -368,12 +320,6 @@ abstract class AppLocalizations {
|
||||
/// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'**
|
||||
String delete_game_with_matches_warning(int count);
|
||||
|
||||
/// No description provided for @filter.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Filter'**
|
||||
String get filter;
|
||||
|
||||
/// No description provided for @delete_group.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -386,12 +332,6 @@ abstract class AppLocalizations {
|
||||
/// **'Delete Match'**
|
||||
String get delete_match;
|
||||
|
||||
/// No description provided for @delete_player.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete player?'**
|
||||
String get delete_player;
|
||||
|
||||
/// No description provided for @description.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -422,18 +362,6 @@ abstract class AppLocalizations {
|
||||
/// **'Edit Match'**
|
||||
String get edit_match;
|
||||
|
||||
/// No description provided for @edit_name.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit name'**
|
||||
String get edit_name;
|
||||
|
||||
/// No description provided for @edit_player.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit player'**
|
||||
String get edit_player;
|
||||
|
||||
/// No description provided for @enter_points.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -536,12 +464,6 @@ abstract class AppLocalizations {
|
||||
/// **'Groups'**
|
||||
String get groups;
|
||||
|
||||
/// No description provided for @groups_part_of.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Groups part of'**
|
||||
String get groups_part_of;
|
||||
|
||||
/// No description provided for @highest_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -608,12 +530,6 @@ abstract class AppLocalizations {
|
||||
/// **'Live Edit Mode'**
|
||||
String get live_edit_mode;
|
||||
|
||||
/// No description provided for @loading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Loading...'**
|
||||
String get loading;
|
||||
|
||||
/// No description provided for @loser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -626,6 +542,12 @@ abstract class AppLocalizations {
|
||||
/// **'Lowest Score'**
|
||||
String get lowest_score;
|
||||
|
||||
/// No description provided for @manage_members.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manage Members'**
|
||||
String get manage_members;
|
||||
|
||||
/// No description provided for @match_in_progress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -650,23 +572,11 @@ abstract class AppLocalizations {
|
||||
/// **'Matches'**
|
||||
String get matches;
|
||||
|
||||
/// No description provided for @matches_part_of.
|
||||
/// No description provided for @member.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches part of'**
|
||||
String get matches_part_of;
|
||||
|
||||
/// No description provided for @matches_played.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches played'**
|
||||
String get matches_played;
|
||||
|
||||
/// No description provided for @matches_won.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Matches won'**
|
||||
String get matches_won;
|
||||
/// **'Member'**
|
||||
String get member;
|
||||
|
||||
/// No description provided for @members.
|
||||
///
|
||||
@@ -722,11 +632,11 @@ abstract class AppLocalizations {
|
||||
/// **'No matches created yet'**
|
||||
String get no_matches_created_yet;
|
||||
|
||||
/// No description provided for @no_matches_played_yet.
|
||||
/// No description provided for @no_players_available.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No games played yet'**
|
||||
String get no_matches_played_yet;
|
||||
/// **'No players available'**
|
||||
String get no_players_available;
|
||||
|
||||
/// No description provided for @no_players_created_yet.
|
||||
///
|
||||
@@ -770,11 +680,11 @@ abstract class AppLocalizations {
|
||||
/// **'No statistics available'**
|
||||
String get no_statistics_available;
|
||||
|
||||
/// No description provided for @no_statistics_created_yet.
|
||||
/// No description provided for @no_teams_available.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No statistics created yet'**
|
||||
String get no_statistics_created_yet;
|
||||
/// **'No teams available'**
|
||||
String get no_teams_available;
|
||||
|
||||
/// No description provided for @none.
|
||||
///
|
||||
@@ -794,12 +704,6 @@ abstract class AppLocalizations {
|
||||
/// **'Not available'**
|
||||
String get not_available;
|
||||
|
||||
/// No description provided for @not_part_of_any_group.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not part of any group yet'**
|
||||
String get not_part_of_any_group;
|
||||
|
||||
/// No description provided for @place.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -824,12 +728,6 @@ abstract class AppLocalizations {
|
||||
/// **'Player name'**
|
||||
String get player_name;
|
||||
|
||||
/// No description provided for @player_profile.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Player Profile'**
|
||||
String get player_profile;
|
||||
|
||||
/// No description provided for @players.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -866,6 +764,12 @@ abstract class AppLocalizations {
|
||||
/// **'Recent Matches'**
|
||||
String get recent_matches;
|
||||
|
||||
/// No description provided for @redistribute.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Redistribute'**
|
||||
String get redistribute;
|
||||
|
||||
/// No description provided for @results.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -950,48 +854,12 @@ abstract class AppLocalizations {
|
||||
/// **'Selected players'**
|
||||
String get selected_players;
|
||||
|
||||
/// No description provided for @set_name.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set name'**
|
||||
String get set_name;
|
||||
|
||||
/// No description provided for @settings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Settings'**
|
||||
String get settings;
|
||||
|
||||
/// No description provided for @select_a_classifier.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a classifier'**
|
||||
String get select_a_classifier;
|
||||
|
||||
/// No description provided for @select_a_game.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a game'**
|
||||
String get select_a_game;
|
||||
|
||||
/// No description provided for @select_a_group.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a group'**
|
||||
String get select_a_group;
|
||||
|
||||
/// No description provided for @select_a_scope.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a scope'**
|
||||
String get select_a_scope;
|
||||
|
||||
/// No description provided for @select_a_timeframe.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a timeframe'**
|
||||
String get select_a_timeframe;
|
||||
|
||||
/// No description provided for @single_loser.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1016,66 +884,30 @@ abstract class AppLocalizations {
|
||||
/// **'Stats'**
|
||||
String get stats;
|
||||
|
||||
/// No description provided for @selected_games.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Selected games'**
|
||||
String get selected_games;
|
||||
|
||||
/// No description provided for @selected_groups.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Selected groups'**
|
||||
String get selected_groups;
|
||||
|
||||
/// No description provided for @average_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Average score'**
|
||||
String get average_score;
|
||||
|
||||
/// No description provided for @best_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Best score'**
|
||||
String get best_score;
|
||||
|
||||
/// No description provided for @total_losses.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total losses'**
|
||||
String get total_losses;
|
||||
|
||||
/// No description provided for @total_matches.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total matches'**
|
||||
String get total_matches;
|
||||
|
||||
/// No description provided for @total_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total score'**
|
||||
String get total_score;
|
||||
|
||||
/// No description provided for @total_wins.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Total wins'**
|
||||
String get total_wins;
|
||||
|
||||
/// No description provided for @worst_score.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Worst score'**
|
||||
String get worst_score;
|
||||
|
||||
/// Success message when adding a player
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Successfully added player {playerName}'**
|
||||
String successfully_added_player(String playerName);
|
||||
|
||||
/// No description provided for @team.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Team'**
|
||||
String get team;
|
||||
|
||||
/// No description provided for @team_match.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Team Match'**
|
||||
String get team_match;
|
||||
|
||||
/// No description provided for @teams.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Teams'**
|
||||
String get teams;
|
||||
|
||||
/// No description provided for @there_are_no_games_matching_your_search.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1100,42 +932,6 @@ abstract class AppLocalizations {
|
||||
/// **'Tie'**
|
||||
String get tie;
|
||||
|
||||
/// No description provided for @all_time.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'All time'**
|
||||
String get all_time;
|
||||
|
||||
/// No description provided for @last_180_days.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last 180 days'**
|
||||
String get last_180_days;
|
||||
|
||||
/// No description provided for @last_30_days.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last 30 days'**
|
||||
String get last_30_days;
|
||||
|
||||
/// No description provided for @last_7_days.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last 7 days'**
|
||||
String get last_7_days;
|
||||
|
||||
/// No description provided for @last_90_days.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last 90 days'**
|
||||
String get last_90_days;
|
||||
|
||||
/// No description provided for @last_year.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Last year'**
|
||||
String get last_year;
|
||||
|
||||
/// No description provided for @today_at.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -8,6 +8,9 @@ import 'app_localizations.dart';
|
||||
class AppLocalizationsDe extends AppLocalizations {
|
||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get add_team => 'Team hinzufügen';
|
||||
|
||||
@override
|
||||
String get all_players => 'Alle Spieler:innen';
|
||||
|
||||
@@ -62,17 +65,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get color_teal => 'Türkis';
|
||||
|
||||
@override
|
||||
String get displayed_entries => 'Angezeigte Einträge';
|
||||
|
||||
@override
|
||||
String get color_yellow => 'Gelb';
|
||||
|
||||
@override
|
||||
String get confirm => 'Bestätigen';
|
||||
|
||||
@override
|
||||
String could_not_add_player(String playerName) {
|
||||
String could_not_add_player(Object playerName) {
|
||||
return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
|
||||
}
|
||||
|
||||
@@ -92,31 +89,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get create_new_match => 'Neues Spiel erstellen';
|
||||
|
||||
@override
|
||||
String get create_statistic => 'Statistik erstellen';
|
||||
|
||||
@override
|
||||
String get classifier => 'Klassifikator';
|
||||
|
||||
@override
|
||||
String get select_the_filtered_timeframe =>
|
||||
'Wähle einen Zeitraum, nach dem gefiltert werden soll.';
|
||||
|
||||
@override
|
||||
String get select_the_filtered_games =>
|
||||
'Wähle Spiele, nach denen gefiltert werden soll.';
|
||||
|
||||
@override
|
||||
String get games => 'Spielvorlagen';
|
||||
|
||||
@override
|
||||
String get select_the_filtered_groups =>
|
||||
'Wähle Gruppen, nach denen gefiltert werden soll.';
|
||||
|
||||
@override
|
||||
String get scope => 'Bereich';
|
||||
|
||||
@override
|
||||
String get timeframe => 'Zeitraum';
|
||||
String get create_teams => 'Teams erstellen';
|
||||
|
||||
@override
|
||||
String get created_on => 'Erstellt am';
|
||||
@@ -158,18 +131,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filter => 'Filter';
|
||||
|
||||
@override
|
||||
String get delete_group => 'Gruppe löschen';
|
||||
|
||||
@override
|
||||
String get delete_match => 'Spiel löschen';
|
||||
|
||||
@override
|
||||
String get delete_player => 'Spieler:in löschen';
|
||||
|
||||
@override
|
||||
String get description => 'Beschreibung';
|
||||
|
||||
@@ -185,12 +152,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get edit_match => 'Gruppe bearbeiten';
|
||||
|
||||
@override
|
||||
String get edit_name => 'Name ändern';
|
||||
|
||||
@override
|
||||
String get edit_player => 'Spieler bearbeiten';
|
||||
|
||||
@override
|
||||
String get enter_points => 'Punkte eingeben';
|
||||
|
||||
@@ -246,9 +207,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get groups => 'Gruppen';
|
||||
|
||||
@override
|
||||
String get groups_part_of => 'Gruppen Teil von';
|
||||
|
||||
@override
|
||||
String get highest_score => 'Höchste Punkte';
|
||||
|
||||
@@ -282,15 +240,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get live_edit_mode => 'Live-Bearbeitungsmodus';
|
||||
|
||||
@override
|
||||
String get loading => 'Lädt...';
|
||||
|
||||
@override
|
||||
String get loser => 'Verlierer:in';
|
||||
|
||||
@override
|
||||
String get lowest_score => 'Niedrigste Punkte';
|
||||
|
||||
@override
|
||||
String get manage_members => 'Mitglieder bearbeiten';
|
||||
|
||||
@override
|
||||
String get match_in_progress => 'Spiel läuft...';
|
||||
|
||||
@@ -304,13 +262,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get matches => 'Spiele';
|
||||
|
||||
@override
|
||||
String get matches_part_of => 'Spiele Teil von';
|
||||
|
||||
@override
|
||||
String get matches_played => 'Spiele gespielt';
|
||||
|
||||
@override
|
||||
String get matches_won => 'Spiele gewonnen';
|
||||
String get member => 'Mitglied';
|
||||
|
||||
@override
|
||||
String get members => 'Mitglieder';
|
||||
@@ -340,7 +292,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get no_matches_created_yet => 'Noch keine Spiele erstellt';
|
||||
|
||||
@override
|
||||
String get no_matches_played_yet => 'Noch kein Spiel gespielt';
|
||||
String get no_players_available => 'Keine Spieler:innen verfügbar';
|
||||
|
||||
@override
|
||||
String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
|
||||
@@ -365,7 +317,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get no_statistics_available => 'Keine Statistiken verfügbar';
|
||||
|
||||
@override
|
||||
String get no_statistics_created_yet => 'Noch keine Statistiken erstellt';
|
||||
String get no_teams_available => 'Keine Teams verfügbar';
|
||||
|
||||
@override
|
||||
String get none => 'Kein';
|
||||
@@ -376,9 +328,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get not_available => 'Nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt';
|
||||
|
||||
@override
|
||||
String get place => 'Platz';
|
||||
|
||||
@@ -391,9 +340,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get player_name => 'Spieler:innenname';
|
||||
|
||||
@override
|
||||
String get player_profile => 'Spieler:in-Profil';
|
||||
|
||||
@override
|
||||
String get players => 'Spieler:innen';
|
||||
|
||||
@@ -412,6 +358,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get recent_matches => 'Letzte Spiele';
|
||||
|
||||
@override
|
||||
String get redistribute => 'Neu verteilen';
|
||||
|
||||
@override
|
||||
String get results => 'Ergebnisse';
|
||||
|
||||
@@ -459,27 +408,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get selected_players => 'Ausgewählte Spieler:innen';
|
||||
|
||||
@override
|
||||
String get set_name => 'Name setzen';
|
||||
|
||||
@override
|
||||
String get settings => 'Einstellungen';
|
||||
|
||||
@override
|
||||
String get select_a_classifier => 'Klassifikator auswählen';
|
||||
|
||||
@override
|
||||
String get select_a_game => 'Spielvorlage auswählen';
|
||||
|
||||
@override
|
||||
String get select_a_group => 'Gruppe auswählen';
|
||||
|
||||
@override
|
||||
String get select_a_scope => 'Bereich auswählen';
|
||||
|
||||
@override
|
||||
String get select_a_timeframe => 'Zeitraum auswählen';
|
||||
|
||||
@override
|
||||
String get single_loser => 'Ein:e Verlierer:in';
|
||||
|
||||
@@ -492,38 +423,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get stats => 'Statistiken';
|
||||
|
||||
@override
|
||||
String get selected_games => 'Ausgewählte Spielvorlagen';
|
||||
|
||||
@override
|
||||
String get selected_groups => 'Ausgewählte Gruppen';
|
||||
|
||||
@override
|
||||
String get average_score => 'Durchschnittliche Punktzahl';
|
||||
|
||||
@override
|
||||
String get best_score => 'Beste Punktzahl';
|
||||
|
||||
@override
|
||||
String get total_losses => 'Niederlagen insgesamt';
|
||||
|
||||
@override
|
||||
String get total_matches => 'Spiele insgesamt';
|
||||
|
||||
@override
|
||||
String get total_score => 'Punktzahl insgesamt';
|
||||
|
||||
@override
|
||||
String get total_wins => 'Siege insgesamt';
|
||||
|
||||
@override
|
||||
String get worst_score => 'Schlechteste Punktzahl';
|
||||
|
||||
@override
|
||||
String successfully_added_player(String playerName) {
|
||||
return 'Spieler:in $playerName erfolgreich hinzugefügt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get team => 'Team';
|
||||
|
||||
@override
|
||||
String get team_match => 'Teamspiel';
|
||||
|
||||
@override
|
||||
String get teams => 'Teams';
|
||||
|
||||
@override
|
||||
String get there_are_no_games_matching_your_search =>
|
||||
'Es gibt keine Spielvorlagen, die deiner Suche entspricht';
|
||||
@@ -539,24 +452,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get tie => 'Unentschieden';
|
||||
|
||||
@override
|
||||
String get all_time => 'Gesamter Zeitraum';
|
||||
|
||||
@override
|
||||
String get last_180_days => 'Letzte 180 Tage';
|
||||
|
||||
@override
|
||||
String get last_30_days => 'Letzte 30 Tage';
|
||||
|
||||
@override
|
||||
String get last_7_days => 'Letzte 7 Tage';
|
||||
|
||||
@override
|
||||
String get last_90_days => 'Letzte 90 Tage';
|
||||
|
||||
@override
|
||||
String get last_year => 'Letztes Jahr';
|
||||
|
||||
@override
|
||||
String get today_at => 'Heute um';
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import 'app_localizations.dart';
|
||||
class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get add_team => 'Add Team';
|
||||
|
||||
@override
|
||||
String get all_players => 'All players';
|
||||
|
||||
@@ -62,18 +65,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get color_teal => 'Teal';
|
||||
|
||||
@override
|
||||
String get displayed_entries => 'Displayed entries';
|
||||
|
||||
@override
|
||||
String get color_yellow => 'Yellow';
|
||||
|
||||
@override
|
||||
String get confirm => 'Confirm';
|
||||
|
||||
@override
|
||||
String could_not_add_player(String playerName) {
|
||||
return 'Could not add player $playerName';
|
||||
String could_not_add_player(Object playerName) {
|
||||
return 'Could not add player';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -92,31 +89,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get create_new_match => 'Create new match';
|
||||
|
||||
@override
|
||||
String get create_statistic => 'Create statistic';
|
||||
|
||||
@override
|
||||
String get classifier => 'Classifier';
|
||||
|
||||
@override
|
||||
String get select_the_filtered_timeframe =>
|
||||
'Select the timeframe you want to filter by.';
|
||||
|
||||
@override
|
||||
String get select_the_filtered_games =>
|
||||
'Select the games you want to filter by.';
|
||||
|
||||
@override
|
||||
String get games => 'Games';
|
||||
|
||||
@override
|
||||
String get select_the_filtered_groups =>
|
||||
'Select the groups you want to filter by.';
|
||||
|
||||
@override
|
||||
String get scope => 'Scope';
|
||||
|
||||
@override
|
||||
String get timeframe => 'Timeframe';
|
||||
String get create_teams => 'Create teams';
|
||||
|
||||
@override
|
||||
String get created_on => 'Created on';
|
||||
@@ -158,18 +131,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get filter => 'Filter';
|
||||
|
||||
@override
|
||||
String get delete_group => 'Delete Group';
|
||||
|
||||
@override
|
||||
String get delete_match => 'Delete Match';
|
||||
|
||||
@override
|
||||
String get delete_player => 'Delete player?';
|
||||
|
||||
@override
|
||||
String get description => 'Description';
|
||||
|
||||
@@ -185,12 +152,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get edit_match => 'Edit Match';
|
||||
|
||||
@override
|
||||
String get edit_name => 'Edit name';
|
||||
|
||||
@override
|
||||
String get edit_player => 'Edit player';
|
||||
|
||||
@override
|
||||
String get enter_points => 'Enter points';
|
||||
|
||||
@@ -246,9 +207,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get groups => 'Groups';
|
||||
|
||||
@override
|
||||
String get groups_part_of => 'Groups part of';
|
||||
|
||||
@override
|
||||
String get highest_score => 'Highest Score';
|
||||
|
||||
@@ -282,15 +240,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get live_edit_mode => 'Live Edit Mode';
|
||||
|
||||
@override
|
||||
String get loading => 'Loading...';
|
||||
|
||||
@override
|
||||
String get loser => 'Loser';
|
||||
|
||||
@override
|
||||
String get lowest_score => 'Lowest Score';
|
||||
|
||||
@override
|
||||
String get manage_members => 'Manage Members';
|
||||
|
||||
@override
|
||||
String get match_in_progress => 'Match in progress...';
|
||||
|
||||
@@ -304,13 +262,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get matches => 'Matches';
|
||||
|
||||
@override
|
||||
String get matches_part_of => 'Matches part of';
|
||||
|
||||
@override
|
||||
String get matches_played => 'Matches played';
|
||||
|
||||
@override
|
||||
String get matches_won => 'Matches won';
|
||||
String get member => 'Member';
|
||||
|
||||
@override
|
||||
String get members => 'Members';
|
||||
@@ -340,7 +292,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get no_matches_created_yet => 'No matches created yet';
|
||||
|
||||
@override
|
||||
String get no_matches_played_yet => 'No games played yet';
|
||||
String get no_players_available => 'No players available';
|
||||
|
||||
@override
|
||||
String get no_players_created_yet => 'No players created yet';
|
||||
@@ -365,7 +317,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get no_statistics_available => 'No statistics available';
|
||||
|
||||
@override
|
||||
String get no_statistics_created_yet => 'No statistics created yet';
|
||||
String get no_teams_available => 'No teams available';
|
||||
|
||||
@override
|
||||
String get none => 'None';
|
||||
@@ -376,9 +328,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get not_available => 'Not available';
|
||||
|
||||
@override
|
||||
String get not_part_of_any_group => 'Not part of any group yet';
|
||||
|
||||
@override
|
||||
String get place => 'place';
|
||||
|
||||
@@ -391,9 +340,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get player_name => 'Player name';
|
||||
|
||||
@override
|
||||
String get player_profile => 'Player Profile';
|
||||
|
||||
@override
|
||||
String get players => 'Players';
|
||||
|
||||
@@ -412,6 +358,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get recent_matches => 'Recent Matches';
|
||||
|
||||
@override
|
||||
String get redistribute => 'Redistribute';
|
||||
|
||||
@override
|
||||
String get results => 'Results';
|
||||
|
||||
@@ -459,27 +408,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get selected_players => 'Selected players';
|
||||
|
||||
@override
|
||||
String get set_name => 'Set name';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
|
||||
@override
|
||||
String get select_a_classifier => 'Select a classifier';
|
||||
|
||||
@override
|
||||
String get select_a_game => 'Select a game';
|
||||
|
||||
@override
|
||||
String get select_a_group => 'Select a group';
|
||||
|
||||
@override
|
||||
String get select_a_scope => 'Select a scope';
|
||||
|
||||
@override
|
||||
String get select_a_timeframe => 'Select a timeframe';
|
||||
|
||||
@override
|
||||
String get single_loser => 'Single Loser';
|
||||
|
||||
@@ -492,38 +423,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get stats => 'Stats';
|
||||
|
||||
@override
|
||||
String get selected_games => 'Selected games';
|
||||
|
||||
@override
|
||||
String get selected_groups => 'Selected groups';
|
||||
|
||||
@override
|
||||
String get average_score => 'Average score';
|
||||
|
||||
@override
|
||||
String get best_score => 'Best score';
|
||||
|
||||
@override
|
||||
String get total_losses => 'Total losses';
|
||||
|
||||
@override
|
||||
String get total_matches => 'Total matches';
|
||||
|
||||
@override
|
||||
String get total_score => 'Total score';
|
||||
|
||||
@override
|
||||
String get total_wins => 'Total wins';
|
||||
|
||||
@override
|
||||
String get worst_score => 'Worst score';
|
||||
|
||||
@override
|
||||
String successfully_added_player(String playerName) {
|
||||
return 'Successfully added player $playerName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get team => 'Team';
|
||||
|
||||
@override
|
||||
String get team_match => 'Team Match';
|
||||
|
||||
@override
|
||||
String get teams => 'Teams';
|
||||
|
||||
@override
|
||||
String get there_are_no_games_matching_your_search =>
|
||||
'There are no games matching your search';
|
||||
@@ -538,24 +451,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get tie => 'Tie';
|
||||
|
||||
@override
|
||||
String get all_time => 'All time';
|
||||
|
||||
@override
|
||||
String get last_180_days => 'Last 180 days';
|
||||
|
||||
@override
|
||||
String get last_30_days => 'Last 30 days';
|
||||
|
||||
@override
|
||||
String get last_7_days => 'Last 7 days';
|
||||
|
||||
@override
|
||||
String get last_90_days => 'Last 90 days';
|
||||
|
||||
@override
|
||||
String get last_year => 'Last year';
|
||||
|
||||
@override
|
||||
String get today_at => 'Today at';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/statistics_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/navbar_item.dart';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
import 'package:tallee/presentation/widgets/player_selection.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
|
||||
@@ -89,7 +89,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
Expanded(
|
||||
child: PlayerSelection(
|
||||
initialSelectedPlayers: initialSelectedPlayers,
|
||||
onPlayerCreated: () => widget.onMembersChanged?.call(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = [...value];
|
||||
@@ -97,19 +96,24 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: widget.groupToEdit == null
|
||||
? loc.create_group
|
||||
: loc.edit_group,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed:
|
||||
(_groupNameController.text.isEmpty ||
|
||||
(selectedPlayers.length < 2))
|
||||
? null
|
||||
: _saveGroup,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: AnimatedDialogButton(
|
||||
buttonConstraints: const BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: 50,
|
||||
),
|
||||
buttonText: widget.groupToEdit == null
|
||||
? loc.create_group
|
||||
: loc.edit_group,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed:
|
||||
(_groupNameController.text.isEmpty ||
|
||||
(selectedPlayers.length < 2))
|
||||
? null
|
||||
: _saveGroup,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -135,7 +139,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
widget.onMembersChanged?.call();
|
||||
await HapticFeedback.successNotification();
|
||||
if (mounted) {
|
||||
Navigator.pop(context, updatedGroup);
|
||||
@@ -159,6 +162,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
final success = await db.groupDao.addGroup(
|
||||
group: Group(name: groupName, members: selectedPlayers),
|
||||
);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
|
||||
@@ -150,7 +150,6 @@ class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
return TextIconTile(
|
||||
text: member.name,
|
||||
suffixText: getNameCountText(member),
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
@@ -77,7 +77,6 @@ class _GroupViewState extends State<GroupView> {
|
||||
);
|
||||
}
|
||||
return GroupTile(
|
||||
onPlayerChanged: loadGroups,
|
||||
group: groups[index],
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
@@ -107,10 +106,13 @@ class _GroupViewState extends State<GroupView> {
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
return CreateGroupView(onMembersChanged: loadGroups);
|
||||
return const CreateGroupView();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
loadGroups();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -51,6 +51,9 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
/// Games filtered according to the current search query
|
||||
late List<Game> filteredGames;
|
||||
|
||||
List<Game> get games =>
|
||||
widget.games..sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
@@ -59,7 +62,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
selectedGameId = widget.initialGameId;
|
||||
|
||||
// Start with all games visible
|
||||
filteredGames = List<Game>.from(widget.games);
|
||||
filteredGames = List<Game>.from(games);
|
||||
|
||||
super.initState();
|
||||
}
|
||||
@@ -77,9 +80,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
Navigator.of(context).pop(
|
||||
selectedGameId == ''
|
||||
? null
|
||||
: widget.games.firstWhere(
|
||||
(game) => game.id == selectedGameId,
|
||||
),
|
||||
: games.firstWhere((game) => game.id == selectedGameId),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -99,7 +100,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
);
|
||||
if (result != null && result.game != null) {
|
||||
setState(() {
|
||||
widget.games.insert(0, result.game);
|
||||
games.insert(0, result.game);
|
||||
});
|
||||
_refreshFromSource();
|
||||
}
|
||||
@@ -139,7 +140,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
child: Visibility(
|
||||
visible: filteredGames.isNotEmpty,
|
||||
replacement: Visibility(
|
||||
visible: widget.games.isNotEmpty,
|
||||
visible: games.isNotEmpty,
|
||||
replacement: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: loc.info,
|
||||
@@ -160,11 +161,8 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
return GameTile(
|
||||
title: game.name,
|
||||
description: game.description,
|
||||
badgeText: translateRulesetToString(
|
||||
game.ruleset,
|
||||
context,
|
||||
),
|
||||
badgeColor: getColorFromAppColor(game.color),
|
||||
subtitle: translateRulesetToString(game.ruleset, context),
|
||||
badgeColor: getColorFromGameColor(game.color),
|
||||
isHighlighted: selectedGameId == game.id,
|
||||
onTap: () async {
|
||||
setState(() {
|
||||
@@ -190,7 +188,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
);
|
||||
if (result != null && result.game != null) {
|
||||
// Find the index in the original list to mutate
|
||||
final originalIndex = widget.games.indexWhere(
|
||||
final originalIndex = games.indexWhere(
|
||||
(g) => g.id == game.id,
|
||||
);
|
||||
if (originalIndex == -1) {
|
||||
@@ -202,12 +200,12 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
if (selectedGameId == game.id) {
|
||||
selectedGameId = '';
|
||||
}
|
||||
widget.games.removeAt(originalIndex);
|
||||
games.removeAt(originalIndex);
|
||||
widget.onGamesUpdated?.call();
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
widget.games[originalIndex] = result.game;
|
||||
games[originalIndex] = result.game;
|
||||
});
|
||||
}
|
||||
_refreshFromSource();
|
||||
@@ -229,13 +227,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
||||
final q = query.toLowerCase().trim();
|
||||
if (q.isEmpty) {
|
||||
setState(() {
|
||||
filteredGames = List<Game>.from(widget.games);
|
||||
filteredGames = List<Game>.from(games);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
filteredGames = widget.games.where((game) {
|
||||
filteredGames = games.where((game) {
|
||||
final name = game.name.toLowerCase();
|
||||
final description = game.description.toLowerCase();
|
||||
return name.contains(q) || description.contains(q);
|
||||
|
||||
@@ -90,7 +90,7 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
AppColor.values.length,
|
||||
(index) => (
|
||||
AppColor.values[index],
|
||||
translateAppColorToString(AppColor.values[index], context),
|
||||
translateGameColorToString(AppColor.values[index], context),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -117,6 +117,7 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
|
||||
return ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text(isEditing ? loc.edit_game : loc.create_game),
|
||||
actions: [
|
||||
@@ -467,7 +468,7 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
height: 16,
|
||||
margin: const EdgeInsets.only(left: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: getColorFromAppColor(
|
||||
color: getColorFromGameColor(
|
||||
_colors[index].$1,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
@@ -501,13 +502,13 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: getColorFromAppColor(selectedColor!),
|
||||
color: getColorFromGameColor(selectedColor!),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: Text(translateAppColorToString(selectedColor!, context)),
|
||||
child: Text(translateGameColorToString(selectedColor!, context)),
|
||||
),
|
||||
Transform.rotate(
|
||||
angle: pi / 2,
|
||||
|
||||
@@ -12,8 +12,9 @@ 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/create_teams/create_teams_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
import 'package:tallee/presentation/widgets/player_selection.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
|
||||
@@ -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,42 +146,25 @@ 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: loc.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(
|
||||
key: ValueKey(selectedGroup?.id ?? 'no_group'),
|
||||
initialSelectedPlayers: selectedPlayers,
|
||||
onPlayerCreated: () => widget.onMatchesUpdated?.call(),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = value;
|
||||
@@ -207,15 +175,21 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
),
|
||||
|
||||
// Create or save button.
|
||||
CustomWidthButton(
|
||||
text: buttonText,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed: _enableCreateGameButton()
|
||||
? () {
|
||||
buttonNavigation(context);
|
||||
}
|
||||
: null,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: AnimatedDialogButton(
|
||||
buttonConstraints: const BoxConstraints(
|
||||
minWidth: double.infinity,
|
||||
minHeight: 50,
|
||||
),
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed: isSubmitButtonEnabled()
|
||||
? () {
|
||||
submitButtonNavigation(context);
|
||||
}
|
||||
: null,
|
||||
buttonText: buttonText,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -228,12 +202,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 game 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 +290,35 @@ 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.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: !isTeamMatch,
|
||||
builder: (context) => CreateTeamsView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
fullscreenDialog: !isTeamMatch,
|
||||
builder: (context) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
@@ -328,36 +391,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
createdAt: DateTime.now(),
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
isTeamMatch: isTeamMatch,
|
||||
game: selectedGame!,
|
||||
);
|
||||
await db.matchDao.addMatch(match: match);
|
||||
|
||||
// Team matches are saved in OrganizeTeamsView
|
||||
if (!isTeamMatch) await db.matchDao.addMatch(match: match);
|
||||
return match;
|
||||
}
|
||||
|
||||
// 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,190 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart';
|
||||
|
||||
class CreateTeamsView extends StatefulWidget {
|
||||
const CreateTeamsView({super.key, required this.match, this.onWinnerChanged});
|
||||
|
||||
final Match match;
|
||||
final VoidCallback? onWinnerChanged;
|
||||
|
||||
@override
|
||||
State<CreateTeamsView> createState() => _CreateTeamsViewState();
|
||||
}
|
||||
|
||||
class _CreateTeamsViewState extends State<CreateTeamsView> {
|
||||
final Random random = Random();
|
||||
List<Player> get matchPlayers => widget.match.players;
|
||||
|
||||
late List<Team> teams;
|
||||
late List<TextEditingController> nameController;
|
||||
final int initialTeamCount = 2;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Init the teams
|
||||
teams = List.generate(
|
||||
initialTeamCount,
|
||||
(index) => Team(
|
||||
name: '${loc.team} ${index + 1}',
|
||||
color: getTeamColor(index),
|
||||
members: [],
|
||||
),
|
||||
);
|
||||
|
||||
// Init the controllers
|
||||
nameController = teams.map(getNewController).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(title: Text(loc.create_teams)),
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 96),
|
||||
itemCount: teams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return TeamCreationTile(
|
||||
color: teams[index].color,
|
||||
controller: nameController[index],
|
||||
hintText: '${loc.team} ${index + 1}',
|
||||
onDelete: teams.length <= 2 ? null : () => removeTeam(index),
|
||||
onColorSelection: (color) {
|
||||
setState(() {
|
||||
teams[index] = teams[index].copyWith(color: color);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
// Button row
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Add new team
|
||||
MainMenuButton(
|
||||
icon: Icons.add,
|
||||
text: loc.add_team,
|
||||
onPressed: teams.length >= widget.match.players.length
|
||||
? null
|
||||
: addTeam,
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
|
||||
// Confirm teams
|
||||
MainMenuButton(
|
||||
icon: Icons.arrow_forward_sharp,
|
||||
onPressed: teams.length >= 2
|
||||
? () {
|
||||
final match = widget.match.copyWith(teams: teams);
|
||||
Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ManageMembersView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new team with a default name and color based on the current number
|
||||
Team getNewTeam() {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return Team(
|
||||
name: '${loc.team} ${teams.length + 1}',
|
||||
color: getTeamColor(teams.length),
|
||||
members: [],
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a [TextEditingController] for the given team and sets up a listener
|
||||
/// to update the team's name whenever the text changes.
|
||||
TextEditingController getNewController(Team team) {
|
||||
final textController = TextEditingController(text: team.name);
|
||||
textController.addListener(() {
|
||||
final index = teams.indexWhere((t) => t.id == team.id);
|
||||
if (index == -1) return;
|
||||
teams[index] = teams[index].copyWith(name: textController.text);
|
||||
});
|
||||
return textController;
|
||||
}
|
||||
|
||||
/// Adds a new team to the list of teams, creates a corresponding controller,
|
||||
/// and redistributes the players among all teams.
|
||||
void addTeam() {
|
||||
setState(() {
|
||||
final newTeam = getNewTeam();
|
||||
teams.add(newTeam);
|
||||
nameController.add(getNewController(newTeam));
|
||||
});
|
||||
}
|
||||
|
||||
/// Removes the team with the given index. If there are less than 2 teams the
|
||||
/// removed team gets replaced with a new one
|
||||
void removeTeam(int index) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
setState(() {
|
||||
teams.removeAt(index);
|
||||
final removedController = nameController.removeAt(index);
|
||||
removedController.dispose();
|
||||
|
||||
// Update index-based team names and default colors
|
||||
for (int i = 0; i < nameController.length; i++) {
|
||||
if (nameController[i].text.contains(
|
||||
RegExp('^${RegExp.escape(loc.team)} \\d+\$'),
|
||||
)) {
|
||||
nameController[i].text = '${loc.team} ${i + 1}';
|
||||
|
||||
// Reset color to default if it was based on the index
|
||||
final previousIndex = i < index ? i : i + 1;
|
||||
if (teams[i].color == getTeamColor(previousIndex)) {
|
||||
teams[i] = teams[i].copyWith(color: getTeamColor(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final c in nameController) {
|
||||
c.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
import 'dart:core' hide Match;
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_numeric_text/flutter_numeric_text.dart';
|
||||
import 'package:fluttericon/rpg_awesome_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart';
|
||||
|
||||
/// Displays the given [teams] as a flat reorderable list where every team is
|
||||
/// preceded by a header row and followed by its members. Members can be
|
||||
/// dragged across team boundaries to be reassigned to another team.
|
||||
class ManageMembersView extends StatefulWidget {
|
||||
const ManageMembersView({
|
||||
super.key,
|
||||
required this.match,
|
||||
required this.onWinnerChanged,
|
||||
});
|
||||
|
||||
final Match match;
|
||||
|
||||
final VoidCallback? onWinnerChanged;
|
||||
|
||||
@override
|
||||
State<ManageMembersView> createState() => _ManageMembersViewState();
|
||||
}
|
||||
|
||||
class _ManageMembersViewState extends State<ManageMembersView> {
|
||||
late AppDatabase db;
|
||||
|
||||
List<Team> get teams => widget.match.teams!;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
redistributePlayers();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(title: Text(loc.manage_members)),
|
||||
body: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ReorderableListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(0, 12, 0, 96),
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: allItemsCount,
|
||||
onReorderItem: onReorderItem,
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
Material(type: MaterialType.transparency, child: child),
|
||||
itemBuilder: (context, index) {
|
||||
final teamIndex = teamIndexForFlat(index);
|
||||
final memberIndex = memberIndexForFlat(index, teamIndex);
|
||||
final team = teams[teamIndex];
|
||||
|
||||
if (memberIndex == -1) {
|
||||
return buildTeamTile(team: team);
|
||||
}
|
||||
|
||||
final player = team.members[memberIndex];
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ValueKey('player_${player.id}'),
|
||||
index: index,
|
||||
child: TextIconListTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
icon: Icons.drag_handle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 20,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
MainMenuButton(
|
||||
onPressed: () => setState(() {
|
||||
redistributePlayers();
|
||||
}),
|
||||
icon: Icons.cached,
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
MainMenuButton(
|
||||
onPressed: allTeamsHaveMembers
|
||||
? () async => submitMatch()
|
||||
: null,
|
||||
text: loc.create_match,
|
||||
icon: RpgAwesome.clovers_card,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildTeamTile({required Team team}) {
|
||||
final color = getColorFromGameColor(team.color);
|
||||
final loc = AppLocalizations.of(context);
|
||||
final length = team.members.length;
|
||||
final memberText = length == 1 ? loc.member : loc.members;
|
||||
|
||||
return Padding(
|
||||
key: ValueKey(team.id),
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
// Color circle
|
||||
Container(
|
||||
width: 14,
|
||||
height: 14,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Team name
|
||||
Expanded(
|
||||
child: Text(
|
||||
team.name,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// Member length
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: NumericText(
|
||||
'$length $memberText',
|
||||
duration: const Duration(milliseconds: 200),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.end,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.hintColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Iterates through all teams and redistributes players randomly and
|
||||
// as evenly as possible.
|
||||
void redistributePlayers() {
|
||||
for (final team in teams) {
|
||||
team.members.clear();
|
||||
}
|
||||
var matchPlayers = widget.match.players;
|
||||
Random random = Random();
|
||||
|
||||
if (matchPlayers.isEmpty || teams.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final shuffledPlayers = [...matchPlayers]..shuffle(random);
|
||||
|
||||
for (int i = 0; i < shuffledPlayers.length; i++) {
|
||||
final teamIndex = i % teams.length;
|
||||
teams[teamIndex].members.add(shuffledPlayers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles moving a member from one team to another
|
||||
void onReorderItem(int oldIndex, int newIndex) {
|
||||
final sourceTeamIndex = teamIndexForFlat(oldIndex);
|
||||
final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex);
|
||||
|
||||
// Headers themselves can't be reordered.
|
||||
if (sourceMemberIndex == -1) return;
|
||||
|
||||
// When moving down, the target index is shifted by 1
|
||||
// because the item is removed first.
|
||||
var targetIndex = newIndex;
|
||||
if (newIndex > oldIndex) targetIndex -= 1;
|
||||
targetIndex = targetIndex.clamp(0, allItemsCount - 1);
|
||||
|
||||
// Resolve target location based on the item currently
|
||||
// at targetIndex before the move.
|
||||
int destTeamIndex;
|
||||
int insertPositionInTeam;
|
||||
|
||||
if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) {
|
||||
// dropped at the very end, append to the last team.
|
||||
destTeamIndex = teams.length - 1;
|
||||
insertPositionInTeam = teams[destTeamIndex].members.length;
|
||||
} else {
|
||||
destTeamIndex = teamIndexForFlat(targetIndex);
|
||||
final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex);
|
||||
|
||||
if (anchorMemberIndex == -1) {
|
||||
// dropped on a header, direction decides which team the player gets added
|
||||
// if moving down, insert as first member of that team.
|
||||
// if moving UP, append to the previous team.
|
||||
final isMovingDown = newIndex > oldIndex;
|
||||
if (isMovingDown) {
|
||||
insertPositionInTeam = 0;
|
||||
} else {
|
||||
final previousTeamIndex = destTeamIndex - 1;
|
||||
if (previousTeamIndex < 0) {
|
||||
// above the very first header, stay at top of team 0.
|
||||
insertPositionInTeam = 0;
|
||||
} else {
|
||||
destTeamIndex = previousTeamIndex;
|
||||
insertPositionInTeam = teams[destTeamIndex].members.length;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
insertPositionInTeam = anchorMemberIndex;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
final sourceMembers = teams[sourceTeamIndex].members;
|
||||
final player = sourceMembers.removeAt(sourceMemberIndex);
|
||||
|
||||
// Adjust insert index if removed from before the insert point in the
|
||||
// same team.
|
||||
if (sourceTeamIndex == destTeamIndex &&
|
||||
insertPositionInTeam > sourceMembers.length) {
|
||||
insertPositionInTeam = sourceMembers.length;
|
||||
}
|
||||
|
||||
teams[destTeamIndex].members.insert(insertPositionInTeam, player);
|
||||
});
|
||||
}
|
||||
|
||||
/// Total players + teams length
|
||||
int get allItemsCount {
|
||||
var count = 0;
|
||||
for (final team in teams) {
|
||||
count += 1 + team.members.length;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// Returns the index of the team that owns the flat-list item at [flatIndex].
|
||||
int teamIndexForFlat(int flatIndex) {
|
||||
var remaining = flatIndex;
|
||||
for (var i = 0; i < teams.length; i++) {
|
||||
final size = 1 + teams[i].members.length;
|
||||
if (remaining < size) return i;
|
||||
remaining -= size;
|
||||
}
|
||||
return teams.length - 1;
|
||||
}
|
||||
|
||||
/// Returns the member index within its team, or `-1` if the item at
|
||||
/// [flatIndex] is the team header.
|
||||
int memberIndexForFlat(int flatIndex, int teamIndex) {
|
||||
var offset = 0;
|
||||
for (var i = 0; i < teamIndex; i++) {
|
||||
offset += 1 + teams[i].members.length;
|
||||
}
|
||||
// offset now points to the header of [teamIndex]. Anything beyond is a
|
||||
// member of that team.
|
||||
final localIndex = flatIndex - offset;
|
||||
return localIndex == 0 ? -1 : localIndex - 1;
|
||||
}
|
||||
|
||||
bool get allTeamsHaveMembers =>
|
||||
teams.every((team) => team.members.isNotEmpty);
|
||||
|
||||
void submitMatch() async {
|
||||
final match = widget.match;
|
||||
await db.matchDao.addMatch(match: match);
|
||||
if (mounted) {
|
||||
Navigator.pushAndRemoveUntil(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
),
|
||||
),
|
||||
(route) => route.isFirst,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart';
|
||||
|
||||
class LiveEditView extends StatefulWidget {
|
||||
const LiveEditView({super.key, required this.match});
|
||||
final Match match;
|
||||
|
||||
@override
|
||||
State<LiveEditView> createState() => _LiveEditViewState();
|
||||
}
|
||||
|
||||
class _LiveEditViewState extends State<LiveEditView> {
|
||||
List<Team> get allTeams =>
|
||||
(widget.match.teams ?? [])..sort((a, b) => a.name.compareTo(b.name));
|
||||
List<Player> get allPlayers =>
|
||||
widget.match.players..sort((a, b) => a.name.compareTo(b.name));
|
||||
List<int> scores = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
if (widget.match.isTeamMatch) {
|
||||
scores = List.generate(
|
||||
allTeams.length,
|
||||
(index) => allTeams[index].score ?? 0,
|
||||
);
|
||||
} else {
|
||||
scores = List.generate(
|
||||
allPlayers.length,
|
||||
(index) => widget.match.scores[allPlayers[index].id]?.score ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(widget.match.name),
|
||||
leading: HapticIconButton(
|
||||
onPressed: () => Navigator.pop(context, scores),
|
||||
icon: const Icon(Icons.close),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(child: buildLiveEditWidget(widget.match.isTeamMatch)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildLiveEditWidget(bool isTeamMatch) {
|
||||
if (isTeamMatch) {
|
||||
return ListView.builder(
|
||||
itemCount: allTeams.length,
|
||||
itemBuilder: (context, index) {
|
||||
return LiveEditListTile(
|
||||
title: allTeams[index].name,
|
||||
onChanged: (value) {
|
||||
scores[index] = value;
|
||||
},
|
||||
value: scores[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return ListView.builder(
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return LiveEditListTile(
|
||||
title: allPlayers[index].name,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
scores[index] = value;
|
||||
});
|
||||
},
|
||||
value: scores[index],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import 'package:tallee/presentation/views/main_menu/match_view/create_match/crea
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/cards/team_card.dart';
|
||||
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
|
||||
@@ -43,13 +44,13 @@ class MatchDetailView extends StatefulWidget {
|
||||
class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
late Match match;
|
||||
late Match localMatch;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
match = widget.match;
|
||||
localMatch = widget.match;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,7 +84,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
await db.matchDao.deleteMatch(matchId: match.id);
|
||||
await db.matchDao.deleteMatch(matchId: localMatch.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
widget.onMatchUpdate.call();
|
||||
@@ -117,7 +118,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
|
||||
// Match Name
|
||||
Text(
|
||||
match.name,
|
||||
localMatch.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -129,7 +130,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
|
||||
// Creation Date
|
||||
Text(
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}',
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(localMatch.createdAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CustomTheme.textColor,
|
||||
@@ -139,14 +140,14 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Group Name
|
||||
if (match.group != null) ...[
|
||||
if (localMatch.group != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.group),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${match.group!.name}${getExtraPlayerCount(match)}',
|
||||
'${localMatch.group!.name}${getExtraPlayerCount(localMatch)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
@@ -154,25 +155,60 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
|
||||
// Players
|
||||
InfoTile(
|
||||
title: loc.players,
|
||||
icon: Icons.people,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: match.players.map((player) {
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
// Teams or Players
|
||||
if (localMatch.isTeamMatch) ...[
|
||||
// Teams
|
||||
InfoTile(
|
||||
title: loc.teams,
|
||||
icon: Icons.scoreboard,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content:
|
||||
localMatch.teams != null && localMatch.teams!.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: (localMatch.teams ?? []).map((team) {
|
||||
return TeamCard(team: team);
|
||||
}).toList(),
|
||||
)
|
||||
: Text(
|
||||
loc.no_teams_available,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Players
|
||||
InfoTile(
|
||||
title: loc.players,
|
||||
icon: Icons.people,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: localMatch.players.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: localMatch.players.map((player) {
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: Text(
|
||||
loc.no_players_available,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 15),
|
||||
|
||||
// Game
|
||||
@@ -186,12 +222,12 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
horizontal: 8,
|
||||
),
|
||||
child: GameLabel(
|
||||
title: match.game.name,
|
||||
title: localMatch.game.name,
|
||||
description: translateRulesetToString(
|
||||
match.game.ruleset,
|
||||
localMatch.game.ruleset,
|
||||
context,
|
||||
),
|
||||
color: match.game.color,
|
||||
color: localMatch.game.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -222,7 +258,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => CreateMatchView(
|
||||
matchToEdit: match,
|
||||
matchToEdit: localMatch,
|
||||
onMatchUpdated: onMatchUpdated,
|
||||
),
|
||||
),
|
||||
@@ -238,12 +274,10 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: () {
|
||||
match: localMatch,
|
||||
onWinnerChanged: () async {
|
||||
widget.onMatchUpdate.call();
|
||||
setState(() {
|
||||
updateScoresForCurrentMatch();
|
||||
});
|
||||
await updateScoresForCurrentMatch();
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -263,7 +297,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
/// updates the match in this view
|
||||
void onMatchUpdated(Match editedMatch) {
|
||||
setState(() {
|
||||
match = editedMatch;
|
||||
localMatch = editedMatch;
|
||||
});
|
||||
widget.onMatchUpdate.call();
|
||||
}
|
||||
@@ -284,95 +318,113 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
/// Returns the result row for single winner/loser rulesets or a placeholder
|
||||
/// if no result is entered yet
|
||||
List<Widget> getSingleResultRow(AppLocalizations loc) {
|
||||
if (match.mvp.isNotEmpty) {
|
||||
final ruleset = match.game.ruleset;
|
||||
final ruleset = localMatch.game.ruleset;
|
||||
|
||||
if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) {
|
||||
return [
|
||||
Text(
|
||||
ruleset == Ruleset.singleWinner ? loc.winner : loc.loser,
|
||||
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
|
||||
),
|
||||
Text(
|
||||
match.mvp.first.name,
|
||||
if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) {
|
||||
// Single winner/loser, multiple winner
|
||||
final names = localMatch.isTeamMatch
|
||||
? localMatch.mvt.map((t) => t.name).toList()
|
||||
: localMatch.mvp.map((p) => p.name).toList();
|
||||
final mvpNames = names.length == 1 ? names.first : names.join(', ');
|
||||
|
||||
final label = ruleset == Ruleset.singleWinner
|
||||
? loc.winner
|
||||
: ruleset == Ruleset.singleLoser
|
||||
? loc.loser
|
||||
: loc.winners;
|
||||
|
||||
return [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
|
||||
),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: Text(
|
||||
mvpNames,
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
];
|
||||
} else if (match.game.ruleset == Ruleset.multipleWinners) {
|
||||
return [
|
||||
Text(
|
||||
loc.winners,
|
||||
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 10),
|
||||
child: Text(
|
||||
match.mvp.map((player) => player.name).join(', '),
|
||||
textAlign: TextAlign.end,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
}
|
||||
),
|
||||
];
|
||||
} else {
|
||||
// No result yet
|
||||
return [
|
||||
Text(
|
||||
loc.no_results_entered_yet,
|
||||
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// No results yet
|
||||
return [
|
||||
Text(
|
||||
loc.no_results_entered_yet,
|
||||
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/// Returns the result widget for scores or placement
|
||||
Widget getMultiResultRows(AppLocalizations loc) {
|
||||
List<(String, int)> playerScores = [];
|
||||
for (var player in match.players) {
|
||||
int score = match.scores[player.id]?.score ?? 0;
|
||||
playerScores.add((player.name, score));
|
||||
}
|
||||
|
||||
final ruleset = match.game.ruleset;
|
||||
|
||||
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
|
||||
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
} else if (ruleset == Ruleset.lowestScore) {
|
||||
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
|
||||
}
|
||||
List<(String, int)> scores = getSortedScores();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
for (var i = 0; i < playerScores.length; i++)
|
||||
for (var i = 0; i < scores.length; i++)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
playerScores[i].$1,
|
||||
scores[i].$1,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
getResultValueText(loc, i, playerScores[i].$2),
|
||||
getResultValueText(loc, i, scores[i].$2),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of player/team names and their corresponding scores, sorted by score according to the ruleset
|
||||
List<(String, int)> getSortedScores() {
|
||||
List<(String, int)> namedScores = [];
|
||||
|
||||
if (localMatch.isTeamMatch) {
|
||||
final teams = localMatch.teams ?? [];
|
||||
for (var team in teams) {
|
||||
int score = team.score ?? 0;
|
||||
namedScores.add((team.name, score));
|
||||
}
|
||||
|
||||
final ruleset = localMatch.game.ruleset;
|
||||
|
||||
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
|
||||
namedScores.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
} else if (ruleset == Ruleset.lowestScore) {
|
||||
namedScores.sort((a, b) => a.$2.compareTo(b.$2));
|
||||
}
|
||||
} else {
|
||||
final scores = localMatch.scores;
|
||||
for (var player in localMatch.players) {
|
||||
int score = scores[player.id]?.score ?? 0;
|
||||
namedScores.add((player.name, score));
|
||||
}
|
||||
|
||||
final ruleset = localMatch.game.ruleset;
|
||||
|
||||
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
|
||||
namedScores.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
} else if (ruleset == Ruleset.lowestScore) {
|
||||
namedScores.sort((a, b) => a.$2.compareTo(b.$2));
|
||||
}
|
||||
}
|
||||
return namedScores;
|
||||
}
|
||||
|
||||
/// Returns the text widget for the score or placement value, styled according to the ruleset
|
||||
Widget getResultValueText(AppLocalizations loc, int index, int score) {
|
||||
final ruleset = match.game.ruleset;
|
||||
final ruleset = localMatch.game.ruleset;
|
||||
|
||||
if (ruleset == Ruleset.placement) {
|
||||
return Text(
|
||||
@@ -410,9 +462,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
|
||||
// Returns if the result can be displayed in a single row
|
||||
bool isSingleRowResult() {
|
||||
return match.game.ruleset == Ruleset.singleWinner ||
|
||||
match.game.ruleset == Ruleset.singleLoser ||
|
||||
match.game.ruleset == Ruleset.multipleWinners;
|
||||
return localMatch.game.ruleset == Ruleset.singleWinner ||
|
||||
localMatch.game.ruleset == Ruleset.singleLoser ||
|
||||
localMatch.game.ruleset == Ruleset.multipleWinners;
|
||||
}
|
||||
|
||||
String getPlacementText(BuildContext context, int rank) {
|
||||
@@ -443,9 +495,19 @@ class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
}
|
||||
}
|
||||
|
||||
void updateScoresForCurrentMatch() {
|
||||
db.scoreEntryDao
|
||||
.getAllMatchScores(matchId: match.id)
|
||||
.then((scores) => match.scores = scores);
|
||||
Future<void> updateScoresForCurrentMatch() async {
|
||||
if (localMatch.isTeamMatch) {
|
||||
final teams = await db.teamDao.getTeamsByMatchId(matchId: localMatch.id);
|
||||
setState(() {
|
||||
localMatch = localMatch.copyWith(teams: teams);
|
||||
});
|
||||
} else {
|
||||
final scores = await db.scoreEntryDao.getAllMatchScores(
|
||||
matchId: localMatch.id,
|
||||
);
|
||||
setState(() {
|
||||
localMatch = localMatch.copyWith(scores: scores);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -79,7 +79,7 @@ class _MatchViewState extends State<MatchView> {
|
||||
visible: matches.isNotEmpty,
|
||||
replacement: Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
icon: Icons.report,
|
||||
title: loc.info,
|
||||
message: loc.no_matches_created_yet,
|
||||
),
|
||||
@@ -97,7 +97,6 @@ class _MatchViewState extends State<MatchView> {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12.0),
|
||||
child: MatchTile(
|
||||
onPlayerEdited: loadMatches,
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
onTap: () async {
|
||||
Navigator.push(
|
||||
|
||||
@@ -1,394 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.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/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
|
||||
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class PlayerDetailView extends StatefulWidget {
|
||||
const PlayerDetailView({
|
||||
super.key,
|
||||
required this.player,
|
||||
required this.callback,
|
||||
});
|
||||
|
||||
/// The player to display
|
||||
final Player player;
|
||||
|
||||
final VoidCallback callback;
|
||||
|
||||
@override
|
||||
State<PlayerDetailView> createState() => _PlayerDetailViewState();
|
||||
}
|
||||
|
||||
class _PlayerDetailViewState extends State<PlayerDetailView> {
|
||||
late final AppDatabase db;
|
||||
late Player _player;
|
||||
late String playerNameCount;
|
||||
bool isLoading = true;
|
||||
|
||||
/// Total matches played by this player
|
||||
int totalMatches = 0;
|
||||
|
||||
/// Total matches won by this player
|
||||
int matchesWon = 0;
|
||||
|
||||
/// Total groups this player belongs to
|
||||
int totalGroups = 0;
|
||||
|
||||
/// Full list of groups this player belongs to
|
||||
List<Group> playerGroups = List.filled(
|
||||
4,
|
||||
Group(name: 'Skeleton group', members: []),
|
||||
);
|
||||
|
||||
/// Full list of matches this player played in
|
||||
List<Match> playerMatches = List.filled(
|
||||
4,
|
||||
Match(
|
||||
name: 'Skeleton match',
|
||||
game: Game(name: 'Game name', ruleset: Ruleset.singleWinner),
|
||||
players: [],
|
||||
),
|
||||
);
|
||||
|
||||
TextEditingController nameController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_player = widget.player;
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
playerNameCount = getNameCountText(_player);
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
nameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(loc.player_profile),
|
||||
actions: [
|
||||
HapticIconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: loc.delete_player,
|
||||
content: Text(loc.this_cannot_be_undone),
|
||||
actions: [
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
text: loc.delete,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
//TODO: implement player deletion in db
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
widget.callback();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 20,
|
||||
bottom: 100,
|
||||
),
|
||||
children: [
|
||||
const Center(
|
||||
child: ColoredIconContainer(
|
||||
icon: Icons.person,
|
||||
containerSize: 55,
|
||||
iconSize: 38,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_player.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Text(
|
||||
playerNameCount,
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor.withAlpha(120),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_player.createdAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
InfoTile(
|
||||
title: '${loc.matches_part_of} ($totalMatches)',
|
||||
icon: Icons.sports_esports,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
alignment: Alignment.topLeft,
|
||||
child: playerMatches.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: playerMatches.map((match) {
|
||||
return TextIconTile(
|
||||
text: match.name,
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: Text(
|
||||
loc.no_matches_played_yet,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InfoTile(
|
||||
title: '${loc.groups_part_of} ($totalGroups)',
|
||||
icon: Icons.people,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
alignment: Alignment.topLeft,
|
||||
child: playerGroups.isNotEmpty
|
||||
? Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: playerGroups.map((group) {
|
||||
return TextIconTile(
|
||||
text: group.name,
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
)
|
||||
: Text(
|
||||
loc.not_part_of_any_group,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InfoTile(
|
||||
title: loc.statistics,
|
||||
icon: Icons.bar_chart,
|
||||
content: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildStatRow(
|
||||
loc.matches_played,
|
||||
totalMatches.toString(),
|
||||
),
|
||||
_buildStatRow(loc.matches_won, matchesWon.toString()),
|
||||
_buildStatRow(
|
||||
loc.winrate,
|
||||
'${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: MainMenuButton(
|
||||
text: loc.edit_player,
|
||||
icon: Icons.edit,
|
||||
onPressed: () async {
|
||||
nameController.text = _player.name;
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return CustomAlertDialog(
|
||||
title: loc.edit_name,
|
||||
content: TextInputField(
|
||||
controller: nameController,
|
||||
hintText: loc.set_name,
|
||||
onChanged: (_) => setDialogState(() {}),
|
||||
),
|
||||
actions: [
|
||||
CustomDialogAction(
|
||||
onPressed: isConfirmButtonEnabled()
|
||||
? () => Navigator.of(context).pop(true)
|
||||
: null,
|
||||
text: loc.confirm,
|
||||
),
|
||||
CustomDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
buttonType: ButtonType.secondary,
|
||||
text: loc.cancel,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
final newName = nameController.text.trim();
|
||||
|
||||
if (newName != _player.name) {
|
||||
final fetchedPlayerNameCount = await db.playerDao
|
||||
.getNameCount(name: newName);
|
||||
await db.playerDao.updatePlayerName(
|
||||
playerId: _player.id,
|
||||
name: newName,
|
||||
);
|
||||
widget.callback.call();
|
||||
setState(() {
|
||||
_player = Player(
|
||||
name: newName,
|
||||
createdAt: _player.createdAt,
|
||||
id: _player.id,
|
||||
nameCount: _player.nameCount,
|
||||
description: _player.description,
|
||||
);
|
||||
|
||||
// If there is already a player with the same name,
|
||||
// the count of that player is 0, so we start counting from 2 to get the correct count for this player. If there are no players with the same name, we just show the name without a count.
|
||||
playerNameCount = fetchedPlayerNameCount == 0
|
||||
? ''
|
||||
: ' #${fetchedPlayerNameCount + 1}';
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads statistics for this player
|
||||
Future<void> _loadData() async {
|
||||
isLoading = true;
|
||||
final fetchedMatches = await db.matchDao.getMatchesByPlayer(
|
||||
playerId: _player.id,
|
||||
);
|
||||
final fetchedGroups = await db.groupDao.getGroupsByPlayer(
|
||||
playerId: _player.id,
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
playerMatches = fetchedMatches;
|
||||
totalMatches = fetchedMatches.length;
|
||||
matchesWon = fetchedMatches
|
||||
.where((match) => match.mvp.any((mvp) => mvp.id == _player.id))
|
||||
.length;
|
||||
playerGroups = fetchedGroups;
|
||||
totalGroups = fetchedGroups.length;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// Builds a single statistic row with a label and value
|
||||
/// - [label]: The label of the statistic
|
||||
/// - [value]: The value of the statistic
|
||||
Widget _buildStatRow(String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool isConfirmButtonEnabled() {
|
||||
return nameController.text.trim().isNotEmpty;
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ const allDependencies = <Package>[
|
||||
_cli_util,
|
||||
_clock,
|
||||
_code_assets,
|
||||
_code_builder,
|
||||
_collection,
|
||||
_convert,
|
||||
_coverage,
|
||||
@@ -153,7 +154,6 @@ const allDependencies = <Package>[
|
||||
_source_map_stack_trace,
|
||||
_source_maps,
|
||||
_source_span,
|
||||
_sqlcipher_flutter_libs,
|
||||
_sqlite3,
|
||||
_sqlite3_flutter_libs,
|
||||
_sqlparser,
|
||||
@@ -670,17 +670,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// build_runner 2.15.0
|
||||
/// build_runner 2.13.1
|
||||
const _build_runner = Package(
|
||||
name: 'build_runner',
|
||||
description: 'A build system for Dart code generation and modular compilation.',
|
||||
repository: 'https://github.com/dart-lang/build/tree/master/build_runner',
|
||||
authors: [],
|
||||
version: '2.15.0',
|
||||
version: '2.13.1',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')],
|
||||
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('code_builder'), PackageRef('collection'), PackageRef('convert'), PackageRef('crypto'), PackageRef('dart_style'), PackageRef('glob'), PackageRef('graphs'), PackageRef('http_multi_server'), PackageRef('io'), PackageRef('json_annotation'), PackageRef('logging'), PackageRef('meta'), PackageRef('mime'), PackageRef('package_config'), PackageRef('path'), PackageRef('pool'), PackageRef('pub_semver'), PackageRef('shelf'), PackageRef('shelf_web_socket'), PackageRef('stream_transform'), PackageRef('watcher'), PackageRef('web_socket_channel'), PackageRef('yaml')],
|
||||
devDependencies: [PackageRef('stream_channel'), PackageRef('test')],
|
||||
license: '''Copyright 2016, the Dart project authors.
|
||||
|
||||
@@ -1510,6 +1510,47 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// code_builder 4.11.1
|
||||
const _code_builder = Package(
|
||||
name: 'code_builder',
|
||||
description: 'A fluent, builder-based library for generating valid Dart code.',
|
||||
repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/code_builder',
|
||||
authors: [],
|
||||
version: '4.11.1',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('matcher'), PackageRef('meta')],
|
||||
devDependencies: [PackageRef('build'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('source_gen'), PackageRef('test')],
|
||||
license: '''Copyright 2016, the Dart project authors.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
* Neither the name of Google LLC nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// collection 1.19.1
|
||||
const _collection = Package(
|
||||
name: 'collection',
|
||||
@@ -2540,14 +2581,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// drift 2.33.0
|
||||
/// drift 2.31.0
|
||||
const _drift = Package(
|
||||
name: 'drift',
|
||||
description: 'Drift is a reactive library to store relational data in Dart and Flutter applications.',
|
||||
homepage: 'https://drift.simonbinder.eu/',
|
||||
repository: 'https://github.com/simolus3/drift',
|
||||
authors: [],
|
||||
version: '2.33.0',
|
||||
version: '2.31.0',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
@@ -2576,14 +2617,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// drift_dev 2.33.0
|
||||
/// drift_dev 2.31.0
|
||||
const _drift_dev = Package(
|
||||
name: 'drift_dev',
|
||||
description: 'Dev-dependency for users of drift. Contains the generator and development tools.',
|
||||
homepage: 'https://drift.simonbinder.eu/',
|
||||
repository: 'https://github.com/simolus3/drift',
|
||||
authors: [],
|
||||
version: '2.33.0',
|
||||
version: '2.31.0',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
@@ -2612,18 +2653,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// drift_flutter 0.3.0
|
||||
/// drift_flutter 0.2.8
|
||||
const _drift_flutter = Package(
|
||||
name: 'drift_flutter',
|
||||
description: 'Easily set up drift databases across platforms in Flutter apps.',
|
||||
homepage: 'https://drift.simonbinder.eu/',
|
||||
repository: 'https://github.com/simolus3/drift',
|
||||
authors: [],
|
||||
version: '0.3.0',
|
||||
version: '0.2.8',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('drift'), PackageRef('flutter'), PackageRef('meta'), PackageRef('path'), PackageRef('path_provider'), PackageRef('sqlite3'), PackageRef('sqlite3_flutter_libs'), PackageRef('sqlcipher_flutter_libs')],
|
||||
dependencies: [PackageRef('drift'), PackageRef('flutter'), PackageRef('meta'), PackageRef('path'), PackageRef('path_provider'), PackageRef('sqlite3'), PackageRef('sqlite3_flutter_libs')],
|
||||
devDependencies: [PackageRef('build_runner'), PackageRef('drift_dev'), PackageRef('lints'), PackageRef('test'), PackageRef('flutter_test'), PackageRef('async')],
|
||||
license: '''MIT License
|
||||
|
||||
@@ -3016,18 +3057,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// file_saver 0.4.0
|
||||
/// file_saver 0.3.1
|
||||
const _file_saver = Package(
|
||||
name: 'file_saver',
|
||||
description: 'Save files from bytes, paths, streams, and URLs across Android, iOS, Web, Windows, macOS, and Linux.',
|
||||
description: 'A Flutter plugin for saving files across all platforms (Android, iOS, Web, Windows, macOS, Linux). Save files from bytes, File objects, file paths, or download from URLs with a single method call. Features include MIME type support, Dio integration, and platform-specific save locations. Supports saveAs() dialog for user-selected locations on supported platforms.',
|
||||
homepage: 'https://hassanansari.dev',
|
||||
repository: 'https://github.com/incrediblezayed/file_saver',
|
||||
authors: [],
|
||||
version: '0.4.0',
|
||||
version: '0.3.1',
|
||||
spdxIdentifiers: ['BSD-3-Clause'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('collection'), PackageRef('dio'), PackageRef('flutter'), PackageRef('flutter_web_plugins'), PackageRef('path_provider'), PackageRef('web')],
|
||||
dependencies: [PackageRef('collection'), PackageRef('dio'), PackageRef('flutter'), PackageRef('flutter_web_plugins'), PackageRef('path_provider'), PackageRef('path_provider_linux'), PackageRef('path_provider_windows'), PackageRef('web')],
|
||||
devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')],
|
||||
license: '''BSD 3-Clause License
|
||||
|
||||
@@ -37642,264 +37683,18 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// sqlcipher_flutter_libs 0.7.0+eol
|
||||
const _sqlcipher_flutter_libs = Package(
|
||||
name: 'sqlcipher_flutter_libs',
|
||||
description: 'Not used anymore, update to version 3.x of package:sqlite3 instead',
|
||||
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/legacy/sqlcipher_flutter_libs',
|
||||
authors: [],
|
||||
version: '0.7.0+eol',
|
||||
spdxIdentifiers: ['Pixar', 'MIT', 'BSD-3-Clause-HP'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [],
|
||||
devDependencies: [],
|
||||
license: '''sqlcipher_flutter_libs
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Simon Binder
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
OpenSSL
|
||||
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
https://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
SQLCipher
|
||||
|
||||
Copyright (c) 2008-2020 Zetetic LLC
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the ZETETIC LLC nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY ZETETIC LLC ''AS IS'' AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL ZETETIC LLC BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
|
||||
);
|
||||
|
||||
/// sqlite3 3.3.1
|
||||
/// sqlite3 2.9.4
|
||||
const _sqlite3 = Package(
|
||||
name: 'sqlite3',
|
||||
description: 'Provides lightweight yet convenient bindings to SQLite by using dart:ffi',
|
||||
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3',
|
||||
authors: [],
|
||||
version: '3.3.1',
|
||||
version: '2.9.4',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('path'), PackageRef('web'), PackageRef('typed_data'), PackageRef('hooks'), PackageRef('code_assets'), PackageRef('native_toolchain_c'), PackageRef('crypto')],
|
||||
devDependencies: [PackageRef('analyzer'), PackageRef('build_daemon'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('http'), PackageRef('lints'), PackageRef('shelf'), PackageRef('shelf_static'), PackageRef('stream_channel'), PackageRef('test'), PackageRef('pub_semver'), PackageRef('convert'), PackageRef('package_config'), PackageRef('logging')],
|
||||
dependencies: [PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('path'), PackageRef('web'), PackageRef('typed_data')],
|
||||
devDependencies: [PackageRef('analyzer'), PackageRef('build_daemon'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('http'), PackageRef('lints'), PackageRef('shelf'), PackageRef('shelf_static'), PackageRef('stream_channel'), PackageRef('test'), PackageRef('pub_semver'), PackageRef('convert')],
|
||||
license: '''MIT License
|
||||
|
||||
Copyright (c) 2020 Simon Binder
|
||||
@@ -37923,17 +37718,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// sqlite3_flutter_libs 0.6.0+eol
|
||||
/// sqlite3_flutter_libs 0.5.42
|
||||
const _sqlite3_flutter_libs = Package(
|
||||
name: 'sqlite3_flutter_libs',
|
||||
description: 'Not used anymore, update to version 3.x of package:sqlite3 instead',
|
||||
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/legacy/sqlite3_flutter_libs',
|
||||
description: 'Flutter plugin to include native sqlite3 libraries with your app',
|
||||
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/v2/sqlite3_flutter_libs',
|
||||
authors: [],
|
||||
version: '0.6.0+eol',
|
||||
version: '0.5.42',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
dependencies: [],
|
||||
dependencies: [PackageRef('flutter')],
|
||||
devDependencies: [],
|
||||
license: '''MIT License
|
||||
|
||||
@@ -37958,14 +37753,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// sqlparser 0.44.4
|
||||
/// sqlparser 0.43.1
|
||||
const _sqlparser = Package(
|
||||
name: 'sqlparser',
|
||||
description: 'Parses sqlite statements and performs static analysis on them',
|
||||
homepage: 'https://github.com/simolus3/drift/tree/develop/sqlparser',
|
||||
repository: 'https://github.com/simolus3/drift',
|
||||
authors: [],
|
||||
version: '0.44.4',
|
||||
version: '0.43.1',
|
||||
spdxIdentifiers: ['MIT'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
@@ -39620,12 +39415,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.''',
|
||||
);
|
||||
|
||||
/// tallee 0.0.35+277
|
||||
/// tallee 0.0.33+273
|
||||
const _tallee = Package(
|
||||
name: 'tallee',
|
||||
description: 'Tracking App for Card Games',
|
||||
authors: [],
|
||||
version: '0.0.35+277',
|
||||
version: '0.0.33+273',
|
||||
spdxIdentifiers: ['LGPL-3.0'],
|
||||
isMarkdown: false,
|
||||
isSdk: false,
|
||||
|
||||
311
lib/presentation/views/main_menu/statistics_view.dart
Normal file
311
lib/presentation/views/main_menu/statistics_view.dart
Normal file
@@ -0,0 +1,311 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/constants.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/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/top_centered_message.dart';
|
||||
|
||||
class StatisticsView extends StatefulWidget {
|
||||
/// A view that displays player statistics
|
||||
const StatisticsView({super.key});
|
||||
|
||||
@override
|
||||
State<StatisticsView> createState() => _StatisticsViewState();
|
||||
}
|
||||
|
||||
class _StatisticsViewState extends State<StatisticsView> {
|
||||
int matchCount = 0;
|
||||
int groupCount = 0;
|
||||
|
||||
List<(Player, int)> winCounts = List.filled(6, (
|
||||
Player(name: 'Skeleton Player'),
|
||||
1,
|
||||
));
|
||||
List<(Player, int)> matchCounts = List.filled(6, (
|
||||
Player(name: 'Skeleton Player'),
|
||||
1,
|
||||
));
|
||||
List<(Player, double)> winRates = List.filled(6, (
|
||||
Player(name: 'Skeleton Player'),
|
||||
1,
|
||||
));
|
||||
bool isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
loadStatisticData();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
QuickInfoTile(
|
||||
width: constraints.maxWidth * 0.45,
|
||||
height: constraints.maxHeight * 0.13,
|
||||
title: loc.matches,
|
||||
icon: Icons.groups_rounded,
|
||||
value: matchCount,
|
||||
),
|
||||
SizedBox(width: constraints.maxWidth * 0.05),
|
||||
QuickInfoTile(
|
||||
width: constraints.maxWidth * 0.45,
|
||||
height: constraints.maxHeight * 0.13,
|
||||
title: loc.groups,
|
||||
icon: Icons.groups_rounded,
|
||||
value: groupCount,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: constraints.maxHeight * 0.02),
|
||||
Visibility(
|
||||
visible:
|
||||
winCounts.isEmpty &&
|
||||
matchCounts.isEmpty &&
|
||||
winRates.isEmpty,
|
||||
replacement: Column(
|
||||
children: [
|
||||
StatisticsTile(
|
||||
icon: Icons.sports_score,
|
||||
title: loc.wins,
|
||||
width: constraints.maxWidth * 0.95,
|
||||
values: winCounts,
|
||||
itemCount: 3,
|
||||
barColor: Colors.green,
|
||||
),
|
||||
SizedBox(height: constraints.maxHeight * 0.02),
|
||||
StatisticsTile(
|
||||
icon: Icons.percent,
|
||||
title: loc.winrate,
|
||||
width: constraints.maxWidth * 0.95,
|
||||
values: winRates,
|
||||
itemCount: 5,
|
||||
barColor: Colors.orange[700]!,
|
||||
),
|
||||
SizedBox(height: constraints.maxHeight * 0.02),
|
||||
StatisticsTile(
|
||||
icon: Icons.casino,
|
||||
title: loc.amount_of_matches,
|
||||
width: constraints.maxWidth * 0.95,
|
||||
values: matchCounts,
|
||||
itemCount: 10,
|
||||
barColor: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: loc.info,
|
||||
message: AppLocalizations.of(
|
||||
context,
|
||||
).no_statistics_available,
|
||||
),
|
||||
),
|
||||
SizedBox(height: MediaQuery.paddingOf(context).bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads matches and players from the database
|
||||
/// and calculates statistics for each player
|
||||
void loadStatisticData() {
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
|
||||
Future.wait([
|
||||
db.matchDao.getAllMatches(),
|
||||
db.playerDao.getAllPlayers(),
|
||||
db.matchDao.getMatchCount(),
|
||||
db.groupDao.getGroupCount(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
]).then((results) async {
|
||||
if (!mounted) return;
|
||||
|
||||
final matches = results[0] as List<Match>;
|
||||
final players = results[1] as List<Player>;
|
||||
matchCount = results[2] as int;
|
||||
groupCount = results[3] as int;
|
||||
|
||||
winCounts = _calculateWinsForAllPlayers(
|
||||
matches: matches,
|
||||
players: players,
|
||||
context: context,
|
||||
);
|
||||
matchCounts = _calculateMatchAmountsForAllPlayers(
|
||||
matches: matches,
|
||||
players: players,
|
||||
context: context,
|
||||
);
|
||||
winRates = computeWinRatePercent(
|
||||
winCounts: winCounts,
|
||||
matchCounts: matchCounts,
|
||||
);
|
||||
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Calculates the number of wins for each player
|
||||
/// and returns a sorted list of tuples (playerName, winCount)
|
||||
List<(Player, int)> _calculateWinsForAllPlayers({
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
List<(Player, int)> winCounts = [];
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Getting the winners
|
||||
for (var match in matches) {
|
||||
final mvps = match.mvp;
|
||||
for (var winner in mvps) {
|
||||
final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id);
|
||||
// -1 means winner not found in winCounts
|
||||
if (index != -1) {
|
||||
final current = winCounts[index].$2;
|
||||
winCounts[index] = (winner, current + 1);
|
||||
} else {
|
||||
winCounts.add((winner, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding all players with zero wins
|
||||
for (var player in players) {
|
||||
final index = winCounts.indexWhere((entry) => entry.$1.id == player.id);
|
||||
// -1 means player not found in winCounts
|
||||
if (index == -1) {
|
||||
winCounts.add((player, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace player IDs with names
|
||||
for (int i = 0; i < winCounts.length; i++) {
|
||||
final playerId = winCounts[i].$1.id;
|
||||
final player = players.firstWhere(
|
||||
(p) => p.id == playerId,
|
||||
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||
);
|
||||
winCounts[i] = (player, winCounts[i].$2);
|
||||
}
|
||||
|
||||
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
|
||||
return winCounts;
|
||||
}
|
||||
|
||||
/// Calculates the number of matches played for each player
|
||||
/// and returns a sorted list of tuples (playerName, matchCount)
|
||||
List<(Player, int)> _calculateMatchAmountsForAllPlayers({
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
}) {
|
||||
List<(Player, int)> matchCounts = [];
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
// Counting matches for each player
|
||||
for (var match in matches) {
|
||||
for (Player player in match.players) {
|
||||
// Check if the player is already in matchCounts
|
||||
final index = matchCounts.indexWhere(
|
||||
(entry) => entry.$1.id == player.id,
|
||||
);
|
||||
|
||||
// -1 -> not found
|
||||
if (index == -1) {
|
||||
// Add new entry
|
||||
matchCounts.add((player, 1));
|
||||
} else {
|
||||
// Update existing entry
|
||||
final currentMatchAmount = matchCounts[index].$2;
|
||||
matchCounts[index] = (player, currentMatchAmount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adding all players with zero matches
|
||||
for (var player in players) {
|
||||
final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id);
|
||||
// -1 means player not found in matchCounts
|
||||
if (index == -1) {
|
||||
matchCounts.add((player, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Replace player IDs with names
|
||||
for (int i = 0; i < matchCounts.length; i++) {
|
||||
final playerId = matchCounts[i].$1.id;
|
||||
final player = players.firstWhere(
|
||||
(p) => p.id == playerId,
|
||||
orElse: () => Player(id: playerId, name: loc.not_available),
|
||||
);
|
||||
matchCounts[i] = (player, matchCounts[i].$2);
|
||||
}
|
||||
|
||||
matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
|
||||
return matchCounts;
|
||||
}
|
||||
|
||||
List<(Player, double)> computeWinRatePercent({
|
||||
required List<(Player, int)> winCounts,
|
||||
required List<(Player, int)> matchCounts,
|
||||
}) {
|
||||
final Map<Player, int> winsMap = {for (var e in winCounts) e.$1: e.$2};
|
||||
final Map<Player, int> matchesMap = {for (var e in matchCounts) e.$1: e.$2};
|
||||
|
||||
// Get all unique player names
|
||||
final player = {...matchesMap.keys};
|
||||
|
||||
// Calculate win rates
|
||||
final result = player.map((name) {
|
||||
final int w = winsMap[name] ?? 0;
|
||||
final int m = matchesMap[name] ?? 0;
|
||||
// Calculate percentage and round to 2 decimal places
|
||||
// Avoid division by zero
|
||||
final double percent = (m > 0)
|
||||
? double.parse(((w / m)).toStringAsFixed(2))
|
||||
: 0;
|
||||
return (name, percent);
|
||||
}).toList();
|
||||
|
||||
// Sort the result: first by winrate descending,
|
||||
// then by wins descending in case of a tie
|
||||
result.sort((a, b) {
|
||||
final cmp = b.$2.compareTo(a.$2);
|
||||
if (cmp != 0) return cmp;
|
||||
final wa = winsMap[a.$1] ?? 0;
|
||||
final wb = winsMap[b.$1] ?? 0;
|
||||
return wb.compareTo(wa);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -1,635 +0,0 @@
|
||||
import 'package:animated_custom_dropdown/custom_dropdown.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/constants.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
|
||||
class CreateStatisticView extends StatefulWidget {
|
||||
const CreateStatisticView({super.key, required this.onStatisticCreated});
|
||||
|
||||
final void Function() onStatisticCreated;
|
||||
|
||||
@override
|
||||
State<CreateStatisticView> createState() => _CreateStatisticViewState();
|
||||
}
|
||||
|
||||
class _CreateStatisticViewState extends State<CreateStatisticView> {
|
||||
bool isLoading = false;
|
||||
|
||||
/* Data loaded from the database */
|
||||
List<Player> players = [];
|
||||
List<Game> games = [];
|
||||
List<Group> groups = [];
|
||||
|
||||
/* User selections */
|
||||
StatisticType? selectedType;
|
||||
List<StatisticScope> selectedScope = [];
|
||||
List<Game> selectedGames = [];
|
||||
List<Player> selectedPlayers = [];
|
||||
List<Group> selectedGroups = [];
|
||||
Timeframe? selectedTimeframe;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
loadAllData();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var loc = AppLocalizations.of(context);
|
||||
|
||||
return ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
appBar: AppBar(title: Text(loc.create_statistic)),
|
||||
body: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).padding.bottom + 80,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Classifier title
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.classifier,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'description',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Classifier selection
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: CustomDropdown<StatisticType>(
|
||||
closedHeaderPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
listItemBuilder:
|
||||
(context, item, isSelected, onItemSelect) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translateStatisticTypeToString(item, context),
|
||||
style: itemStyle,
|
||||
),
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
headerBuilder: (context, selectedType, enabled) => Text(
|
||||
translateStatisticTypeToString(selectedType, context),
|
||||
style: headerStyle,
|
||||
),
|
||||
hintText: loc.select_a_classifier,
|
||||
items: StatisticType.values,
|
||||
decoration: decoration,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedType = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Scope title
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.scope,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'description',
|
||||
textAlign: TextAlign.start,
|
||||
style: TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Scope selection
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: CustomDropdown<StatisticScope>.multiSelect(
|
||||
closedHeaderPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
hintText: loc.select_a_scope,
|
||||
items: StatisticScope.values,
|
||||
decoration: decoration,
|
||||
listItemBuilder:
|
||||
(context, scope, isSelected, onItemSelect) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translateScopeToString(scope, context),
|
||||
style: itemStyle,
|
||||
),
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
headerListBuilder: (context, selectedItems, enabled) =>
|
||||
Text(
|
||||
selectedItems
|
||||
.map((s) => translateScopeToString(s, context))
|
||||
.join(', '),
|
||||
style: headerStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onListChanged: (List<StatisticScope> values) {
|
||||
setState(() {
|
||||
selectedScope = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
if (selectedScope.contains(StatisticScope.selectedGames)) ...[
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// games title
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.games,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
loc.select_the_filtered_games,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// game selection
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: CustomDropdown<Game>.multiSelect(
|
||||
enabled: !isLoading,
|
||||
disabledDecoration: disabledDecoration,
|
||||
closedHeaderPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
hintText: isLoading ? loc.loading : loc.select_a_game,
|
||||
items: games,
|
||||
decoration: decoration,
|
||||
listItemBuilder:
|
||||
(context, item, isSelected, onItemSelect) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
// Name
|
||||
Text(item.name, style: itemStyle),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Ruleset
|
||||
Text(
|
||||
translateRulesetToString(
|
||||
item.ruleset,
|
||||
context,
|
||||
),
|
||||
style: hintStyle.copyWith(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Check icon
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
headerListBuilder: (context, selectedItems, enabled) =>
|
||||
Text(
|
||||
selectedItems.map((g) => g.name).join(', '),
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onListChanged: (List<Game> values) {
|
||||
setState(() {
|
||||
selectedGames = values;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (selectedScope.contains(
|
||||
StatisticScope.selectedGroups,
|
||||
)) ...[
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// groups title
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.groups,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
loc.select_the_filtered_groups,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// groups selection
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: CustomDropdown<Group>.multiSelect(
|
||||
enabled: !isLoading,
|
||||
disabledDecoration: disabledDecoration,
|
||||
closedHeaderPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
hintText: isLoading ? loc.loading : loc.select_a_group,
|
||||
items: groups,
|
||||
decoration: decoration,
|
||||
listItemBuilder:
|
||||
(context, item, isSelected, onItemSelect) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Name
|
||||
Text(item.name, style: itemStyle),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Ruleset
|
||||
Text(
|
||||
' ${item.members.length.toString()} ${loc.members}',
|
||||
style: hintStyle.copyWith(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
headerListBuilder: (context, selectedItems, enabled) =>
|
||||
Text(
|
||||
selectedItems.map((g) => g.name).join(', '),
|
||||
style: headerStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onListChanged: (List<Group> groups) {
|
||||
setState(() {
|
||||
selectedGroups = groups;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
if (selectedScope.contains(StatisticScope.timeframe)) ...[
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// timeframe title
|
||||
Padding(
|
||||
padding: const EdgeInsetsGeometry.only(left: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
loc.timeframe,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
loc.select_the_filtered_timeframe,
|
||||
textAlign: TextAlign.start,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// groups selection
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
child: CustomDropdown<Timeframe>(
|
||||
enabled: !isLoading,
|
||||
excludeSelected: false,
|
||||
disabledDecoration: disabledDecoration,
|
||||
closedHeaderPadding: const EdgeInsets.symmetric(
|
||||
vertical: 16,
|
||||
horizontal: 16,
|
||||
),
|
||||
hintText: isLoading
|
||||
? loc.loading
|
||||
: loc.select_a_timeframe,
|
||||
items: Timeframe.values,
|
||||
decoration: decoration,
|
||||
listItemBuilder:
|
||||
(context, timeframe, isSelected, onItemSelect) =>
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
translateTimeframeToString(
|
||||
timeframe,
|
||||
context,
|
||||
),
|
||||
style: itemStyle,
|
||||
),
|
||||
if (isSelected)
|
||||
const Icon(
|
||||
Icons.check,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
headerBuilder: (context, selectedTimeframe, enabled) =>
|
||||
Text(
|
||||
translateTimeframeToString(
|
||||
selectedTimeframe,
|
||||
context,
|
||||
),
|
||||
style: headerStyle,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onChanged: (Timeframe? timeframe) {
|
||||
setState(() {
|
||||
selectedTimeframe = timeframe;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Create statistic button
|
||||
Positioned(
|
||||
bottom: MediaQuery.of(context).padding.bottom,
|
||||
child: AnimatedDialogButton(
|
||||
buttonConstraints: const BoxConstraints(minWidth: 350),
|
||||
buttonText: loc.create_statistic,
|
||||
onPressed: selectedType != null && selectedScope.isNotEmpty
|
||||
? () => submitStatistic()
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
CustomDropdownDecoration get decoration => CustomDropdownDecoration(
|
||||
listItemDecoration: const ListItemDecoration(
|
||||
selectedIconBorder: BorderSide(color: CustomTheme.primaryColor, width: 1),
|
||||
selectedIconColor: CustomTheme.primaryColor,
|
||||
highlightColor: CustomTheme.secondaryColor,
|
||||
splashColor: Colors.transparent,
|
||||
selectedColor: CustomTheme.onBoxColor,
|
||||
),
|
||||
listItemStyle: itemStyle,
|
||||
headerStyle: headerStyle,
|
||||
hintStyle: hintStyle,
|
||||
closedFillColor: CustomTheme.boxColor,
|
||||
closedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1),
|
||||
expandedFillColor: CustomTheme.boxColor,
|
||||
expandedBorder: Border.all(color: CustomTheme.boxBorderColor, width: 1),
|
||||
);
|
||||
|
||||
CustomDropdownDisabledDecoration get disabledDecoration =>
|
||||
CustomDropdownDisabledDecoration(
|
||||
fillColor: CustomTheme.boxColor.withAlpha(125),
|
||||
border: Border.all(
|
||||
color: CustomTheme.boxBorderColor.withAlpha(125),
|
||||
width: 1,
|
||||
),
|
||||
headerStyle: disabledHeaderStyle,
|
||||
hintStyle: disabledHintStyle,
|
||||
);
|
||||
|
||||
TextStyle get headerStyle => const TextStyle(
|
||||
color: CustomTheme.textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
TextStyle get itemStyle =>
|
||||
const TextStyle(color: CustomTheme.textColor, fontSize: 14);
|
||||
|
||||
TextStyle get hintStyle =>
|
||||
const TextStyle(color: CustomTheme.hintColor, fontSize: 14);
|
||||
|
||||
TextStyle get disabledHeaderStyle => const TextStyle(
|
||||
color: CustomTheme.hintColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
);
|
||||
|
||||
TextStyle get disabledHintStyle =>
|
||||
const TextStyle(color: CustomTheme.hintColor, fontSize: 14);
|
||||
|
||||
Future<void> loadAllData() async {
|
||||
isLoading = true;
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
|
||||
Future.wait([
|
||||
db.playerDao.getAllPlayers(),
|
||||
db.groupDao.getAllGroups(),
|
||||
db.gameDao.getAllGames(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
])
|
||||
.then((results) async {
|
||||
players = results[0];
|
||||
groups = results[1];
|
||||
games = results[2];
|
||||
isLoading = false;
|
||||
})
|
||||
.catchError((error) {
|
||||
print('Error loading data: $error');
|
||||
});
|
||||
}
|
||||
|
||||
void submitStatistic() {
|
||||
final newStatistic = Statistic(
|
||||
type: selectedType!,
|
||||
scopes: selectedScope,
|
||||
timeframe: selectedTimeframe,
|
||||
selectedGroups: selectedGroups,
|
||||
selectedGames: selectedGames,
|
||||
);
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
db.statisticDao.addStatistic(statistic: newStatistic);
|
||||
Navigator.of(context).pop(newStatistic);
|
||||
}
|
||||
}
|
||||
|
||||
String translateTimeframeToString(Timeframe timeframe, BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (timeframe) {
|
||||
case Timeframe.last7Days:
|
||||
return loc.last_7_days;
|
||||
case Timeframe.last30Days:
|
||||
return loc.last_30_days;
|
||||
case Timeframe.last90Days:
|
||||
return loc.last_90_days;
|
||||
case Timeframe.last180Days:
|
||||
return loc.last_180_days;
|
||||
case Timeframe.lastYear:
|
||||
return loc.last_year;
|
||||
case Timeframe.allTime:
|
||||
return loc.all_time;
|
||||
}
|
||||
}
|
||||
|
||||
String translateScopeToString(StatisticScope scope, BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (scope) {
|
||||
case StatisticScope.allPlayers:
|
||||
return loc.all_players;
|
||||
case StatisticScope.selectedGroups:
|
||||
return loc.selected_groups;
|
||||
case StatisticScope.selectedGames:
|
||||
return loc.selected_games;
|
||||
case StatisticScope.timeframe:
|
||||
return loc.timeframe;
|
||||
}
|
||||
}
|
||||
|
||||
String translateStatisticTypeToString(
|
||||
StatisticType type,
|
||||
BuildContext context,
|
||||
) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (type) {
|
||||
case StatisticType.totalMatches:
|
||||
return loc.total_matches;
|
||||
case StatisticType.totalWins:
|
||||
return loc.total_wins;
|
||||
case StatisticType.totalScore:
|
||||
return loc.total_score;
|
||||
case StatisticType.totalLosses:
|
||||
return loc.total_losses;
|
||||
case StatisticType.averageScore:
|
||||
return loc.average_score;
|
||||
case StatisticType.bestScore:
|
||||
return loc.best_score;
|
||||
case StatisticType.worstScore:
|
||||
return loc.worst_score;
|
||||
case StatisticType.winrate:
|
||||
return loc.winrate;
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
|
||||
|
||||
class StatisticDetailView extends StatefulWidget {
|
||||
const StatisticDetailView({
|
||||
super.key,
|
||||
required this.statistic,
|
||||
required this.values,
|
||||
required this.icon,
|
||||
required this.barColor,
|
||||
});
|
||||
|
||||
final Statistic statistic;
|
||||
final List<(Player, num)> values;
|
||||
final IconData icon;
|
||||
final Color barColor;
|
||||
|
||||
@override
|
||||
State<StatisticDetailView> createState() => _StatisticDetailViewState();
|
||||
}
|
||||
|
||||
class _StatisticDetailViewState extends State<StatisticDetailView> {
|
||||
late int displayCount;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
displayCount = widget.statistic.displayCount;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
final title = translateStatisticTypeToString(
|
||||
widget.statistic.type,
|
||||
context,
|
||||
);
|
||||
const style = TextStyle(fontWeight: FontWeight.bold);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(title),
|
||||
leading: HapticIconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new),
|
||||
onPressed: () => handleBack(context),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
child: Column(
|
||||
children: [
|
||||
StatisticsTile(
|
||||
icon: widget.icon,
|
||||
title: title,
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
values: widget.values,
|
||||
barColor: widget.barColor,
|
||||
selectedGroups: widget.statistic.selectedGroups,
|
||||
selectedGames: widget.statistic.selectedGames,
|
||||
displayCount: displayCount,
|
||||
showAllValues: true,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
InfoTile(
|
||||
icon: Icons.filter_alt,
|
||||
title: loc.filter,
|
||||
content: Column(
|
||||
spacing: 12,
|
||||
|
||||
children: [
|
||||
// Scopes
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(loc.scope, style: style),
|
||||
Text(
|
||||
widget.statistic.scopes
|
||||
.map(
|
||||
(scope) => translateScopeToString(scope, context),
|
||||
)
|
||||
.join('\n'),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Timeframe
|
||||
if (widget.statistic.timeframe != null)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(loc.timeframe, style: style),
|
||||
Text(
|
||||
translateTimeframeToString(
|
||||
widget.statistic.timeframe!,
|
||||
context,
|
||||
),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Groups
|
||||
if (widget.statistic.selectedGroups != null)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(loc.groups, style: style),
|
||||
Text(
|
||||
widget.statistic.selectedGroups!
|
||||
.map((group) => group.name)
|
||||
.join('\n'),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Games
|
||||
if (widget.statistic.selectedGames != null)
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(loc.games, style: style),
|
||||
Text(
|
||||
widget.statistic.selectedGames!
|
||||
.map((game) => game.name)
|
||||
.join('\n'),
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
if (widget.values.isNotEmpty)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(loc.displayed_entries, style: style),
|
||||
Row(
|
||||
children: [
|
||||
HapticIconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: displayCount <= 1
|
||||
? null
|
||||
: () => setState(() => displayCount -= 1),
|
||||
),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
'$displayCount',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
HapticIconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: displayCount >= widget.values.length
|
||||
? null
|
||||
: () => setState(() => displayCount += 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Handles saving the display count and giving it to statistics view
|
||||
Future<void> handleBack(BuildContext context) async {
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
await db.statisticDao.updateDisplayCount(widget.statistic.id, displayCount);
|
||||
if (context.mounted) Navigator.of(context).pop(displayCount);
|
||||
}
|
||||
}
|
||||
@@ -1,322 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/match.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'
|
||||
show translateStatisticTypeToString;
|
||||
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
|
||||
|
||||
List<Color> _colorPalette = AppColor.values
|
||||
.map((c) => getColorFromAppColor(c))
|
||||
.toList();
|
||||
|
||||
/// Returns the icon for the given statistic type.
|
||||
IconData getStatisticIconForType(StatisticType type) =>
|
||||
_getStatisticIcon(type: type);
|
||||
|
||||
/// Returns a color from the palette based on the statistic's ID.
|
||||
Color getStatisticColorForStatistic(Statistic stat) => _getStatisticColor(stat);
|
||||
|
||||
/// Computes the statistic values for a given [Statistic].
|
||||
List<(Player, num)> computeStatisticValues({
|
||||
required Statistic statistic,
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
}) {
|
||||
final filteredMatches = _getFilterMatches(statistic, matches);
|
||||
final filteredPlayers = _getFilteredPlayers(
|
||||
statistic,
|
||||
players,
|
||||
filteredMatches,
|
||||
);
|
||||
|
||||
return _computeValuesForType(
|
||||
type: statistic.type,
|
||||
matches: filteredMatches,
|
||||
players: filteredPlayers,
|
||||
);
|
||||
}
|
||||
|
||||
/// Build the [StatisticsTile] for a given [Statistic].
|
||||
Widget buildStatisticTile({
|
||||
required Statistic statistic,
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
required BuildContext context,
|
||||
double? width,
|
||||
}) {
|
||||
final values = computeStatisticValues(
|
||||
statistic: statistic,
|
||||
matches: matches,
|
||||
players: players,
|
||||
);
|
||||
|
||||
return StatisticsTile(
|
||||
icon: _getStatisticIcon(type: statistic.type),
|
||||
title: translateStatisticTypeToString(statistic.type, context),
|
||||
width: width ?? MediaQuery.sizeOf(context).width * 0.95,
|
||||
values: values,
|
||||
barColor: _getStatisticColor(statistic),
|
||||
displayCount: statistic.displayCount,
|
||||
selectedGroups: statistic.selectedGroups,
|
||||
selectedGames: statistic.selectedGames,
|
||||
);
|
||||
}
|
||||
|
||||
List<Match> _getFilterMatches(Statistic statistic, List<Match> matches) {
|
||||
List<Match> filteredMatches = matches;
|
||||
|
||||
// Filter timeframe
|
||||
if (statistic.scopes.contains(StatisticScope.timeframe) &&
|
||||
statistic.timeframe != null) {
|
||||
final minDate = _getMinimumDate(timeframe: statistic.timeframe!);
|
||||
print(
|
||||
'Filtering matches by timeframe: ${statistic.timeframe}, minDate: $minDate',
|
||||
);
|
||||
if (minDate != null) {
|
||||
filteredMatches = matches
|
||||
.where((m) => m.endedAt != null && m.endedAt!.isAfter(minDate))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Filter games
|
||||
if (statistic.scopes.contains(StatisticScope.selectedGames) &&
|
||||
(statistic.selectedGames?.isNotEmpty ?? false)) {
|
||||
final gameIds = statistic.selectedGames!.map((g) => g.id).toSet();
|
||||
filteredMatches = filteredMatches
|
||||
.where((match) => gameIds.contains(match.game.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Filter groups
|
||||
if (statistic.scopes.contains(StatisticScope.selectedGroups) &&
|
||||
(statistic.selectedGroups?.isNotEmpty ?? false)) {
|
||||
final groupIds = statistic.selectedGroups!.map((g) => g.id).toSet();
|
||||
filteredMatches = filteredMatches
|
||||
.where((m) => m.group != null && groupIds.contains(m.group!.id))
|
||||
.toList();
|
||||
}
|
||||
|
||||
return filteredMatches;
|
||||
}
|
||||
|
||||
/// Returns a [Player] List with the selected players depending on
|
||||
List<Player> _getFilteredPlayers(
|
||||
Statistic statistic,
|
||||
List<Player> allPlayers,
|
||||
List<Match> filteredMatches,
|
||||
) {
|
||||
// allPlayers
|
||||
if (statistic.scopes.contains(StatisticScope.allPlayers)) {
|
||||
return allPlayers;
|
||||
}
|
||||
|
||||
// selectedGroups -> only members
|
||||
if (statistic.scopes.contains(StatisticScope.selectedGroups) &&
|
||||
(statistic.selectedGroups?.isNotEmpty ?? false)) {
|
||||
final Set<String> ids = {
|
||||
for (final g in statistic.selectedGroups!)
|
||||
for (final p in g.members) p.id,
|
||||
};
|
||||
return allPlayers.where((p) => ids.contains(p.id)).toList();
|
||||
}
|
||||
|
||||
// Else -> all players from filtered matches
|
||||
final Set<String> ids = {
|
||||
for (final m in filteredMatches)
|
||||
for (final p in m.players) p.id,
|
||||
};
|
||||
return allPlayers.where((p) => ids.contains(p.id)).toList();
|
||||
}
|
||||
|
||||
/// Returns a [DateTime] with the minimum time and date the [timeframe] allows
|
||||
DateTime? _getMinimumDate({required Timeframe timeframe}) {
|
||||
final now = DateTime.now();
|
||||
switch (timeframe) {
|
||||
case Timeframe.last7Days:
|
||||
return now.subtract(const Duration(days: 7));
|
||||
case Timeframe.last30Days:
|
||||
return now.subtract(const Duration(days: 30));
|
||||
case Timeframe.last90Days:
|
||||
return now.subtract(const Duration(days: 90));
|
||||
case Timeframe.last180Days:
|
||||
return now.subtract(const Duration(days: 180));
|
||||
case Timeframe.lastYear:
|
||||
return now.subtract(const Duration(days: 365));
|
||||
case Timeframe.allTime:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Computes the statistic values for each player based on the statistic type
|
||||
/// and returns a list of (Player, value) tuples sorted descending by value.
|
||||
List<(Player, num)> _computeValuesForType({
|
||||
required StatisticType type,
|
||||
required List<Match> matches,
|
||||
required List<Player> players,
|
||||
}) {
|
||||
switch (type) {
|
||||
case StatisticType.totalMatches:
|
||||
return _sortDesc(
|
||||
players.map((p) => (p, _matchesPlayed(p, matches) as num)).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.totalWins:
|
||||
return _sortDesc(
|
||||
players.map((p) => (p, _wins(p, matches) as num)).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.totalLosses:
|
||||
return _sortDesc(
|
||||
players
|
||||
.map(
|
||||
(p) =>
|
||||
(p, (_matchesPlayed(p, matches) - _wins(p, matches)) as num),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
case StatisticType.totalScore:
|
||||
return _sortDesc(
|
||||
players.map((p) => (p, _totalScore(p, matches) as num)).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.averageScore:
|
||||
return _sortDesc(
|
||||
players.map((p) {
|
||||
final scores = _scoresOf(p, matches);
|
||||
final avg = scores.isEmpty
|
||||
? 0.0
|
||||
: double.parse(
|
||||
(scores.reduce((a, b) => a + b) / scores.length)
|
||||
.toStringAsFixed(2),
|
||||
);
|
||||
return (p, avg as num);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.bestScore:
|
||||
return _sortDesc(
|
||||
players.map((p) {
|
||||
final scores = _scoresOf(p, matches);
|
||||
final best = scores.isEmpty ? 0 : scores.reduce(max);
|
||||
return (p, best as num);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
case StatisticType.worstScore:
|
||||
// Ascending here is more meaningful for "worst", but keep the
|
||||
// existing tile semantics (largest bar = top entry) by sorting
|
||||
// descending on the inverse — i.e. show smallest score on top.
|
||||
final entries = players.map((p) {
|
||||
final scores = _scoresOf(p, matches);
|
||||
final worst = scores.isEmpty ? 0 : scores.reduce(min);
|
||||
return (p, worst as num);
|
||||
}).toList();
|
||||
entries.sort((a, b) => a.$2.compareTo(b.$2));
|
||||
return entries;
|
||||
|
||||
case StatisticType.winrate:
|
||||
return _sortDesc(
|
||||
players.map((p) {
|
||||
final played = _matchesPlayed(p, matches);
|
||||
final wins = _wins(p, matches);
|
||||
final rate = played == 0
|
||||
? 0.0
|
||||
: double.parse((wins / played).toStringAsFixed(2));
|
||||
return (p, rate as num);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* Helper functions for different statistic types */
|
||||
|
||||
/// Detemerines how many matches the player has played in the given list of matches.
|
||||
int _matchesPlayed(Player p, List<Match> matches) =>
|
||||
matches.where((m) => m.players.any((mp) => mp.id == p.id)).length;
|
||||
|
||||
/// Determines how many matches the player has won in the given list of matches.
|
||||
int _wins(Player p, List<Match> matches) =>
|
||||
matches.where((m) => m.mvp.any((mp) => mp.id == p.id)).length;
|
||||
|
||||
/// Determines the total score of the player in the given list of matches.
|
||||
int _totalScore(Player p, List<Match> matches) {
|
||||
var total = 0;
|
||||
for (final m in matches) {
|
||||
final s = m.scores[p.id];
|
||||
if (s != null) total += s.score;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
/// Returns a list of all scores the player has achieved in the given list of matches.
|
||||
List<int> _scoresOf(Player p, List<Match> matches) => [
|
||||
for (final m in matches)
|
||||
if (m.scores[p.id] != null) m.scores[p.id]!.score,
|
||||
];
|
||||
|
||||
/// Returns the list of entries sorted descending by the statistic value.
|
||||
List<(Player, num)> _sortDesc(List<(Player, num)> entries) {
|
||||
entries.sort((a, b) => b.$2.compareTo(a.$2));
|
||||
return entries;
|
||||
}
|
||||
|
||||
/* Icon and color */
|
||||
|
||||
/// Returns the icon for the given statistic type.
|
||||
IconData _getStatisticIcon({required StatisticType type}) {
|
||||
switch (type) {
|
||||
case StatisticType.totalMatches:
|
||||
return Icons.casino;
|
||||
case StatisticType.totalWins:
|
||||
return Icons.emoji_events;
|
||||
case StatisticType.totalLosses:
|
||||
return Icons.sentiment_dissatisfied;
|
||||
case StatisticType.totalScore:
|
||||
return Icons.scoreboard;
|
||||
case StatisticType.averageScore:
|
||||
return Icons.show_chart;
|
||||
case StatisticType.bestScore:
|
||||
return Icons.trending_up;
|
||||
case StatisticType.worstScore:
|
||||
return Icons.trending_down;
|
||||
case StatisticType.winrate:
|
||||
return Icons.percent;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a color from the palette based on the statistic's ID as random seed.
|
||||
Color _getStatisticColor(Statistic stat) {
|
||||
final seed = stat.id.hashCode;
|
||||
return _colorPalette[seed.abs() % _colorPalette.length];
|
||||
}
|
||||
|
||||
/* Skeleton data */
|
||||
|
||||
/// A placeholder tile with mock data for the loading state.
|
||||
Widget buildSkeletonStatisticTile({required BuildContext context}) {
|
||||
final count = 4 + Random().nextInt(5); // 4..8
|
||||
final values = <(Player, num)>[
|
||||
for (var i = 0; i < count; i++)
|
||||
(Player(name: 'Player ${i + 1}'), count - i),
|
||||
];
|
||||
|
||||
return StatisticsTile(
|
||||
icon: Icons.bar_chart,
|
||||
title: 'Skeleton title',
|
||||
width: MediaQuery.sizeOf(context).width * 0.95,
|
||||
values: values,
|
||||
barColor: _colorPalette[Random().nextInt(_colorPalette.length)],
|
||||
selectedGames: [Game(name: 'Game 1', ruleset: Ruleset.highestScore)],
|
||||
selectedGroups: [Group(name: 'Group 1', members: [])],
|
||||
displayCount: 5,
|
||||
);
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/constants.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/statistic.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_detail_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/top_centered_message.dart';
|
||||
|
||||
class StatisticsView extends StatefulWidget {
|
||||
/// A view that displays player statistics
|
||||
const StatisticsView({super.key});
|
||||
|
||||
@override
|
||||
State<StatisticsView> createState() => _StatisticsViewState();
|
||||
}
|
||||
|
||||
class _StatisticsViewState extends State<StatisticsView> {
|
||||
bool isLoading = true;
|
||||
List<Match> _allMatches = const [];
|
||||
List<Player> _allPlayers = const [];
|
||||
List<Statistic> _statistics = const [];
|
||||
List<Widget> statisticTiles = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
loadStatistics(context);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Stack(
|
||||
alignment: AlignmentDirectional.bottomCenter,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Visibility(
|
||||
visible: statisticTiles.isNotEmpty,
|
||||
replacement: Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: loc.info,
|
||||
message: loc.no_statistics_created_yet,
|
||||
),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
fixLayoutBuilder: true,
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
...statisticTiles,
|
||||
SizedBox(
|
||||
height: MediaQuery.paddingOf(context).bottom + 80,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 20,
|
||||
child: MainMenuButton(
|
||||
text: loc.create_statistic,
|
||||
icon: Icons.bar_chart,
|
||||
onPressed: () async {
|
||||
Statistic newStatistic = await Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => CreateStatisticView(
|
||||
onStatisticCreated: () => loadStatistics(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
setState(() {
|
||||
_statistics = [..._statistics, newStatistic];
|
||||
statisticTiles = _statistics
|
||||
.map((stat) => _buildStatisticTile(context, stat))
|
||||
.toList();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadStatistics(BuildContext context) async {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
statisticTiles = List.generate(
|
||||
4,
|
||||
(index) => Column(
|
||||
children: [
|
||||
buildSkeletonStatisticTile(context: context),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
|
||||
final results = await Future.wait([
|
||||
db.statisticDao.getAllStatistics(),
|
||||
db.matchDao.getAllMatches(),
|
||||
db.playerDao.getAllPlayers(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
]);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
final statistics = results[0] as List<Statistic>;
|
||||
_allMatches = results[1] as List<Match>;
|
||||
_allPlayers = results[2] as List<Player>;
|
||||
_statistics = statistics;
|
||||
|
||||
setState(() {
|
||||
statisticTiles = _statistics
|
||||
.map((stat) => _buildStatisticTile(context, stat))
|
||||
.toList();
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Widget _buildStatisticTile(BuildContext context, Statistic statistic) {
|
||||
final values = computeStatisticValues(
|
||||
statistic: statistic,
|
||||
matches: _allMatches,
|
||||
players: _allPlayers,
|
||||
);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final newDisplayCount = await Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => StatisticDetailView(
|
||||
statistic: statistic,
|
||||
values: values,
|
||||
icon: getStatisticIconForType(statistic.type),
|
||||
barColor: getStatisticColorForStatistic(statistic),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (newDisplayCount != null &&
|
||||
newDisplayCount != statistic.displayCount) {
|
||||
setState(() {
|
||||
_statistics = _statistics
|
||||
.map(
|
||||
(stat) => stat.id == statistic.id
|
||||
? stat.copyWith(displayCount: newDisplayCount)
|
||||
: stat,
|
||||
)
|
||||
.toList();
|
||||
statisticTiles = _statistics
|
||||
.map((stat) => _buildStatisticTile(context, stat))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
},
|
||||
child: buildStatisticTile(
|
||||
statistic: statistic,
|
||||
matches: _allMatches,
|
||||
players: _allPlayers,
|
||||
context: context,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,11 @@ class AppSkeleton extends StatefulWidget {
|
||||
/// - [child]: The widget tree to apply the skeleton effect to.
|
||||
/// - [enabled]: A boolean to enable or disable the skeleton effect.
|
||||
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
|
||||
/// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center].
|
||||
const AppSkeleton({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
this.fixLayoutBuilder = false,
|
||||
this.alignment = Alignment.center,
|
||||
});
|
||||
|
||||
/// The widget tree to apply the skeleton effect to.
|
||||
@@ -24,9 +22,6 @@ class AppSkeleton extends StatefulWidget {
|
||||
/// A boolean to fix the layout builder for AnimatedSwitcher.
|
||||
final bool fixLayoutBuilder;
|
||||
|
||||
/// The alignment used for the custom layout builder and optional Align wrapper
|
||||
final Alignment alignment;
|
||||
|
||||
@override
|
||||
State<AppSkeleton> createState() => _AppSkeletonState();
|
||||
}
|
||||
@@ -50,14 +45,13 @@ class _AppSkeletonState extends State<AppSkeleton> {
|
||||
layoutBuilder: !widget.fixLayoutBuilder
|
||||
? AnimatedSwitcher.defaultLayoutBuilder
|
||||
: (Widget? currentChild, List<Widget> previousChildren) {
|
||||
final children = <Widget>[...previousChildren];
|
||||
if (currentChild != null) children.add(currentChild);
|
||||
return Stack(alignment: widget.alignment, children: children);
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [...previousChildren, ?currentChild],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: widget.fixLayoutBuilder
|
||||
? Align(alignment: widget.alignment, child: widget.child)
|
||||
: widget.child,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ class AnimatedDialogButton extends StatefulWidget {
|
||||
const AnimatedDialogButton({
|
||||
super.key,
|
||||
required this.buttonText,
|
||||
this.onPressed,
|
||||
required this.onPressed,
|
||||
this.buttonConstraints,
|
||||
this.buttonType = ButtonType.primary,
|
||||
this.isDescructive = false,
|
||||
this.content,
|
||||
});
|
||||
|
||||
final String buttonText;
|
||||
@@ -27,6 +28,8 @@ class AnimatedDialogButton extends StatefulWidget {
|
||||
|
||||
final bool isDescructive;
|
||||
|
||||
final Widget? content;
|
||||
|
||||
@override
|
||||
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
|
||||
}
|
||||
@@ -38,12 +41,12 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
Widget build(BuildContext context) {
|
||||
final textStyling = _getTextStyling();
|
||||
final buttonDecoration = _getButtonDecoration();
|
||||
bool isDisabled = widget.onPressed == null;
|
||||
final isDisabled = widget.onPressed == null;
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: isDisabled,
|
||||
child: Opacity(
|
||||
opacity: isDisabled ? 0.5 : 1.0,
|
||||
opacity: isDisabled ? 0.4 : 1.0,
|
||||
child: GestureDetector(
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
onTapUp: (_) => setState(() => _isPressed = false),
|
||||
@@ -64,11 +67,13 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
|
||||
vertical: 12,
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Text(
|
||||
widget.buttonText,
|
||||
style: textStyling,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
child: widget.buttonText == ''
|
||||
? widget.content!
|
||||
: Text(
|
||||
widget.buttonText,
|
||||
style: textStyling,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -56,6 +56,7 @@ class CustomWidthButton extends StatelessWidget {
|
||||
onPressed!.call();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
foregroundColor: textcolor,
|
||||
disabledForegroundColor: disabledTextColor,
|
||||
backgroundColor: buttonBackgroundColor,
|
||||
@@ -91,6 +92,7 @@ class CustomWidthButton extends StatelessWidget {
|
||||
onPressed!.call();
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
foregroundColor: textcolor,
|
||||
disabledForegroundColor: disabledTextColor,
|
||||
backgroundColor: buttonBackgroundColor,
|
||||
@@ -128,6 +130,7 @@ class CustomWidthButton extends StatelessWidget {
|
||||
onPressed!.call();
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
foregroundColor: textcolor,
|
||||
disabledForegroundColor: disabledTextColor,
|
||||
backgroundColor: buttonBackgroundColor,
|
||||
|
||||
@@ -17,7 +17,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;
|
||||
@@ -32,9 +32,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;
|
||||
@@ -53,45 +55,67 @@ 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),
|
||||
() async {
|
||||
_isLongPressing = true;
|
||||
widget.onLongPressed?.call();
|
||||
await HapticFeedback.heavyImpact();
|
||||
_repeatTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 250),
|
||||
(_) async {
|
||||
widget.onLongPressed?.call();
|
||||
await HapticFeedback.heavyImpact();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (widget.onPressed == null) {
|
||||
_disabledAnimationController.forward();
|
||||
} else {
|
||||
_animationController.forward();
|
||||
if (widget.onLongPressed != null) {
|
||||
_longPressTimer = Timer(
|
||||
const Duration(milliseconds: 400),
|
||||
() async {
|
||||
_isLongPressing = true;
|
||||
widget.onLongPressed?.call();
|
||||
await HapticFeedback.heavyImpact();
|
||||
_repeatTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 250),
|
||||
(_) async {
|
||||
widget.onLongPressed?.call();
|
||||
await HapticFeedback.heavyImpact();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onTapUp: (_) async {
|
||||
_cancelTimers();
|
||||
if (mounted && !_isLongPressing) {
|
||||
await HapticFeedback.selectionClick();
|
||||
widget.onPressed();
|
||||
if (widget.onPressed == null) {
|
||||
_disabledAnimationController.reverse();
|
||||
} else {
|
||||
_cancelTimers();
|
||||
if (mounted && !_isLongPressing) {
|
||||
await HapticFeedback.selectionClick();
|
||||
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;
|
||||
@@ -100,7 +124,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),
|
||||
@@ -131,6 +155,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
|
||||
void dispose() {
|
||||
_cancelTimers();
|
||||
_animationController.dispose();
|
||||
_disabledAnimationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
103
lib/presentation/widgets/cards/team_card.dart
Normal file
103
lib/presentation/widgets/cards/team_card.dart
Normal file
@@ -0,0 +1,103 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/models/team.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class TeamCard extends StatelessWidget {
|
||||
const TeamCard({
|
||||
super.key,
|
||||
required this.team,
|
||||
this.compact = false,
|
||||
this.width = double.infinity,
|
||||
});
|
||||
|
||||
final Team team;
|
||||
|
||||
final bool compact;
|
||||
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final teamColor = getColorFromGameColor(team.color);
|
||||
|
||||
if (compact) {
|
||||
return Container(
|
||||
width: width,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: teamColor.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: teamColor, width: 2),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
team.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
width: 1,
|
||||
height: 14,
|
||||
color: Colors.white.withValues(alpha: 0.35),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.people_alt_rounded, size: 14, color: Colors.white),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${team.members.length}',
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
width: width,
|
||||
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: teamColor.withAlpha(50),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: teamColor, width: 2),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 6,
|
||||
children: [
|
||||
Text(
|
||||
team.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: team.members.map((player) {
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class CustomAlertDialog extends StatelessWidget {
|
||||
final String title;
|
||||
final Widget content;
|
||||
final List<CustomDialogAction> actions;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
|
||||
@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
|
||||
/// - [onPressed]: Callback function that is triggered when the button is pressed.
|
||||
const CustomDialogAction({
|
||||
super.key,
|
||||
this.onPressed,
|
||||
required this.onPressed,
|
||||
required this.text,
|
||||
this.buttonType = ButtonType.primary,
|
||||
this.isDestructive = false,
|
||||
@@ -20,18 +20,17 @@ class CustomDialogAction extends StatelessWidget {
|
||||
|
||||
final ButtonType buttonType;
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
final bool isDestructive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedDialogButton(
|
||||
onPressed: onPressed != null
|
||||
? () async {
|
||||
await HapticFeedback.selectionClick();
|
||||
onPressed?.call();
|
||||
}
|
||||
: null,
|
||||
onPressed: () async {
|
||||
await HapticFeedback.selectionClick();
|
||||
onPressed.call();
|
||||
},
|
||||
buttonText: text,
|
||||
buttonType: buttonType,
|
||||
isDescructive: isDestructive,
|
||||
|
||||
@@ -16,40 +16,37 @@ class GameLabel extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final backgroundColor = getColorFromAppColor(color);
|
||||
final backgroundColor = getColorFromGameColor(color);
|
||||
final fontColor = backgroundColor.computeLuminance() > 0.5
|
||||
? Colors.black
|
||||
: Colors.white;
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Title
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withAlpha(230),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
bottomLeft: Radius.circular(8),
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Title
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withAlpha(230),
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(8),
|
||||
bottomLeft: Radius.circular(8),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: fontColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: fontColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Description
|
||||
Flexible(
|
||||
child: Container(
|
||||
// Description
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor.withAlpha(140),
|
||||
borderRadius: const BorderRadius.only(
|
||||
@@ -60,9 +57,6 @@ class GameLabel extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||
child: Text(
|
||||
description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
softWrap: false,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: fontColor,
|
||||
@@ -70,8 +64,8 @@ class GameLabel extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ class PlayerSelection extends StatefulWidget {
|
||||
this.availablePlayers,
|
||||
this.initialSelectedPlayers,
|
||||
required this.onChanged,
|
||||
this.onPlayerCreated,
|
||||
});
|
||||
|
||||
/// An optional list of players to choose from. If null, all players from the database are used.
|
||||
@@ -38,9 +37,6 @@ class PlayerSelection extends StatefulWidget {
|
||||
/// A callback function that is invoked whenever the selection changes,
|
||||
final Function(List<Player> value) onChanged;
|
||||
|
||||
/// A callback function that is invoked when a player was created in this widget
|
||||
final VoidCallback? onPlayerCreated;
|
||||
|
||||
@override
|
||||
State<PlayerSelection> createState() => _PlayerSelectionState();
|
||||
}
|
||||
@@ -147,9 +143,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
child: TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
onIconTap: () {
|
||||
setState(() async {
|
||||
await HapticFeedback.selectionClick();
|
||||
onIconTap: () async {
|
||||
await HapticFeedback.selectionClick();
|
||||
setState(() {
|
||||
// Removes the player from the selection and notifies the parent.
|
||||
selectedPlayers.remove(player);
|
||||
widget.onChanged([...selectedPlayers]);
|
||||
@@ -256,6 +252,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
suggestedPlayers = suggestedPlayers
|
||||
.where((p) => !selectedPlayers.any((sp) => sp.id == p.id))
|
||||
.toList();
|
||||
}
|
||||
} else {
|
||||
// Otherwise, use the loaded players from the database.
|
||||
@@ -327,7 +326,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
|
||||
/// Updates the state after successfully adding a new player.
|
||||
void _handleSuccessfulPlayerCreation(Player player) {
|
||||
widget.onPlayerCreated?.call();
|
||||
selectedPlayers.insert(0, player);
|
||||
widget.onChanged([...selectedPlayers]);
|
||||
allPlayers.add(player);
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:tallee/core/enums.dart';
|
||||
class GameTile extends StatelessWidget {
|
||||
/// A list tile widget that displays a title and description, with optional highlighting and badge.
|
||||
/// - [title]: The title text displayed on the tile.
|
||||
/// - [subtitle]: An optional subtitle displayed under the title.
|
||||
/// - [description]: The description text displayed below the title.
|
||||
/// - [onTap]: The callback invoked when the tile is tapped.
|
||||
/// - [onLongPress]: The callback invoked when the tile is tapped.
|
||||
@@ -17,6 +18,7 @@ class GameTile extends StatelessWidget {
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
this.isHighlighted = false,
|
||||
@@ -24,25 +26,20 @@ class GameTile extends StatelessWidget {
|
||||
this.badgeColor,
|
||||
});
|
||||
|
||||
/// The title text displayed on the tile.
|
||||
final String title;
|
||||
|
||||
/// The description text displayed below the title.
|
||||
final String? subtitle;
|
||||
|
||||
final String description;
|
||||
|
||||
/// The callback invoked when the tile is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// The callback invoked when the tile is long-pressed.
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
/// A boolean to determine if the tile should be highlighted.
|
||||
final bool isHighlighted;
|
||||
|
||||
/// Optional text to display in a badge on the right side of the title.
|
||||
final String? badgeText;
|
||||
|
||||
/// Optional color for the badge background.
|
||||
final Color? badgeColor;
|
||||
|
||||
@override
|
||||
@@ -51,7 +48,7 @@ class GameTile extends StatelessWidget {
|
||||
? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white)
|
||||
: Colors.white;
|
||||
|
||||
final gameColor = badgeColor ?? getColorFromAppColor(AppColor.orange);
|
||||
final gameColor = badgeColor ?? getColorFromGameColor(AppColor.orange);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
@@ -67,13 +64,14 @@ class GameTile extends StatelessWidget {
|
||||
}
|
||||
},
|
||||
child: AnimatedContainer(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
|
||||
decoration: !isHighlighted
|
||||
? CustomTheme.standardBoxDecoration
|
||||
: CustomTheme.highlightedBoxDecoration.copyWith(
|
||||
border: Border.all(
|
||||
color: gameColor.withValues(alpha: 0.9),
|
||||
width: 2,
|
||||
strokeAlign: BorderSide.strokeAlignCenter,
|
||||
),
|
||||
),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
@@ -118,6 +116,21 @@ class GameTile extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Title
|
||||
if (subtitle != null && subtitle!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Badge
|
||||
if (badgeText != null) ...[
|
||||
const SizedBox(height: 5),
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class GroupTile extends StatefulWidget {
|
||||
@@ -17,7 +15,6 @@ class GroupTile extends StatefulWidget {
|
||||
required this.group,
|
||||
this.isHighlighted = false,
|
||||
this.onTap,
|
||||
this.onPlayerChanged,
|
||||
});
|
||||
|
||||
/// The group data to be displayed.
|
||||
@@ -29,9 +26,6 @@ class GroupTile extends StatefulWidget {
|
||||
/// Callback function to be executed when the tile is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Callback function to be executed when the players in the group are changed.
|
||||
final VoidCallback? onPlayerChanged;
|
||||
|
||||
@override
|
||||
State<GroupTile> createState() => _GroupTileState();
|
||||
}
|
||||
@@ -97,20 +91,6 @@ class _GroupTileState extends State<GroupTile> {
|
||||
TextIconTile(
|
||||
text: member.name,
|
||||
suffixText: getNameCountText(member),
|
||||
iconEnabled: false,
|
||||
onTileTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => PlayerDetailView(
|
||||
player: member,
|
||||
callback: () {
|
||||
widget.onPlayerChanged?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -5,12 +5,12 @@ import 'package:tallee/core/custom_theme.dart';
|
||||
class CustomCheckboxListTile extends StatelessWidget {
|
||||
const CustomCheckboxListTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.content,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final Widget content;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
@@ -39,16 +39,7 @@ class CustomCheckboxListTile extends StatelessWidget {
|
||||
onChanged(v);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(child: content),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,13 +3,12 @@ import 'dart:core' hide Match;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.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/models/match.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
|
||||
import 'package:tallee/presentation/widgets/cards/team_card.dart';
|
||||
import 'package:tallee/presentation/widgets/game_label.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
@@ -19,14 +18,11 @@ class MatchTile extends StatefulWidget {
|
||||
/// - [match]: The match data to be displayed.
|
||||
/// - [onTap]: The callback invoked when the tile is tapped.
|
||||
/// - [width]: Optional width for the tile.
|
||||
/// - [compact]: Whether to display the tile in a compact mode
|
||||
const MatchTile({
|
||||
super.key,
|
||||
required this.match,
|
||||
required this.onTap,
|
||||
this.width,
|
||||
this.compact = false,
|
||||
this.onPlayerEdited,
|
||||
});
|
||||
|
||||
/// The match data to be displayed.
|
||||
@@ -35,15 +31,9 @@ class MatchTile extends StatefulWidget {
|
||||
/// The callback invoked when the tile is tapped.
|
||||
final VoidCallback onTap;
|
||||
|
||||
/// The callback invoked when the players are edited
|
||||
final VoidCallback? onPlayerEdited;
|
||||
|
||||
/// Optional width for the tile.
|
||||
final double? width;
|
||||
|
||||
/// Whether to display the tile in a compact mode
|
||||
final bool compact;
|
||||
|
||||
@override
|
||||
State<MatchTile> createState() => _MatchTileState();
|
||||
}
|
||||
@@ -106,40 +96,59 @@ class _MatchTileState extends State<MatchTile> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
] else if (widget.compact) ...[
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.person, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${match.players.length} ${loc.players}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
] else ...[
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
// Game + Ruleset Badge
|
||||
if (!widget.compact)
|
||||
GameLabel(
|
||||
title: match.game.name,
|
||||
description: translateRulesetToString(
|
||||
match.game.ruleset,
|
||||
context,
|
||||
),
|
||||
color: match.game.color,
|
||||
GameLabel(
|
||||
title: match.game.name,
|
||||
description: translateRulesetToString(
|
||||
match.game.ruleset,
|
||||
context,
|
||||
),
|
||||
color: match.game.color,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Winner / In Progress Info
|
||||
if (match.mvp.isNotEmpty) ...[
|
||||
if (match.isTeamMatch && match.mvt.isNotEmpty) ...[
|
||||
// MVT Display for team matches
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.green.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
getMvpIcon(),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
getMvtText(loc),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else if (match.mvp.isNotEmpty) ...[
|
||||
// MVP Display for player matches
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
@@ -173,6 +182,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else ...[
|
||||
// Match in progress display
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
@@ -211,8 +221,46 @@ class _MatchTileState extends State<MatchTile> {
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Players List
|
||||
if (players.isNotEmpty && widget.compact == false) ...[
|
||||
if (match.teams != null &&
|
||||
match.teams!.isNotEmpty &&
|
||||
match.isTeamMatch) ...[
|
||||
// Team display
|
||||
Text(
|
||||
loc.teams,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final useSingleColumn = match.teams!.any(
|
||||
(team) => team.name.length > 10,
|
||||
);
|
||||
|
||||
const spacing = 8.0;
|
||||
final itemWidth = useSingleColumn
|
||||
? constraints.maxWidth
|
||||
: (constraints.maxWidth - spacing) / 2;
|
||||
|
||||
return Wrap(
|
||||
spacing: spacing,
|
||||
runSpacing: spacing,
|
||||
children: match.teams!.map((team) {
|
||||
return TeamCard(
|
||||
team: team,
|
||||
compact: true,
|
||||
width: itemWidth,
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else if (players.isNotEmpty) ...[
|
||||
// Player display
|
||||
Text(
|
||||
loc.players,
|
||||
style: const TextStyle(
|
||||
@@ -229,23 +277,17 @@ class _MatchTileState extends State<MatchTile> {
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
suffixText: getNameCountText(player),
|
||||
iconEnabled: false,
|
||||
onTileTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) => PlayerDetailView(
|
||||
player: player,
|
||||
callback: () {
|
||||
widget.onPlayerEdited?.call();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
loc.no_players_available,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.hintColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
@@ -271,6 +313,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the appropriate text based on the match's ruleset and MVP.
|
||||
String getMvpText(AppLocalizations loc) {
|
||||
if (widget.match.mvp.isEmpty) return '';
|
||||
final ruleset = widget.match.game.ruleset;
|
||||
@@ -294,11 +337,41 @@ class _MatchTileState extends State<MatchTile> {
|
||||
return '${loc.winner}: n.A.';
|
||||
}
|
||||
|
||||
// Returns the appropriate text based on the match's ruleset and MVT.
|
||||
String getMvtText(AppLocalizations loc) {
|
||||
if (widget.match.mvt.isEmpty) return '';
|
||||
final ruleset = widget.match.game.ruleset;
|
||||
|
||||
switch (ruleset) {
|
||||
case Ruleset.singleWinner:
|
||||
return '${loc.winner}: ${widget.match.mvt.first.name}';
|
||||
case Ruleset.singleLoser:
|
||||
return '${loc.loser}: ${widget.match.mvt.first.name}';
|
||||
case Ruleset.highestScore:
|
||||
case Ruleset.lowestScore:
|
||||
final mvt = widget.match.mvt;
|
||||
final mvtScore =
|
||||
widget.match.teams!
|
||||
.firstWhere((team) => team.id == mvt.first.id)
|
||||
.score ??
|
||||
0;
|
||||
final mvtNames = mvt.map((team) => team.name).join(', ');
|
||||
return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})';
|
||||
case Ruleset.placement:
|
||||
return '${loc.winner}: ${widget.match.mvt.first.name}';
|
||||
case Ruleset.multipleWinners:
|
||||
final mvtNames = widget.match.mvt.map((team) => team.name).join(', ');
|
||||
return '${loc.winners}: $mvtNames';
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the appropriate icon based on the match's ruleset.
|
||||
Icon getMvpIcon() {
|
||||
final icon = getRulesetIcon(widget.match.game.ruleset);
|
||||
|
||||
switch (widget.match.game.ruleset) {
|
||||
case Ruleset.singleWinner:
|
||||
case Ruleset.multipleWinners:
|
||||
return Icon(icon, size: 20, color: Colors.amber);
|
||||
case Ruleset.singleLoser:
|
||||
return Icon(icon, size: 20, color: Colors.blue);
|
||||
@@ -306,8 +379,6 @@ class _MatchTileState extends State<MatchTile> {
|
||||
return Icon(icon, size: 20, color: Colors.orange);
|
||||
case Ruleset.highestScore:
|
||||
return Icon(icon, size: 20, color: Colors.green);
|
||||
case Ruleset.multipleWinners:
|
||||
return Icon(icon, size: 20, color: Colors.amber);
|
||||
case Ruleset.placement:
|
||||
return Icon(icon, size: 20, color: Colors.deepOrangeAccent);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/rpg_awesome_icons.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
@@ -25,11 +21,8 @@ class StatisticsTile extends StatelessWidget {
|
||||
required this.title,
|
||||
required this.width,
|
||||
required this.values,
|
||||
required this.itemCount,
|
||||
required this.barColor,
|
||||
required this.displayCount,
|
||||
this.selectedGroups,
|
||||
this.selectedGames,
|
||||
this.showAllValues = false,
|
||||
});
|
||||
|
||||
/// The icon displayed next to the title.
|
||||
@@ -44,16 +37,12 @@ class StatisticsTile extends StatelessWidget {
|
||||
/// A list of tuples containing labels and their corresponding numeric values.
|
||||
final List<(Player, num)> values;
|
||||
|
||||
/// The maximum number of items to display.
|
||||
final int itemCount;
|
||||
|
||||
/// The color of the bars representing the values.
|
||||
final Color barColor;
|
||||
|
||||
// statistic data
|
||||
final int displayCount;
|
||||
final List<Group>? selectedGroups;
|
||||
final List<Game>? selectedGames;
|
||||
|
||||
final bool showAllValues;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
@@ -63,202 +52,91 @@ class StatisticsTile extends StatelessWidget {
|
||||
title: title,
|
||||
icon: icon,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Visibility(
|
||||
visible: values.isNotEmpty,
|
||||
|
||||
// No data avaiable message
|
||||
replacement: Center(
|
||||
heightFactor: 4,
|
||||
child: Text(loc.no_data_available),
|
||||
),
|
||||
|
||||
// Bar chart
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxBarWidth = constraints.maxWidth * 0.8;
|
||||
|
||||
// If displayCount wasnt provided, take all values
|
||||
final valuesShown = showAllValues
|
||||
? values.length
|
||||
: min(values.length, displayCount);
|
||||
final displayValues = values.take(valuesShown).toList();
|
||||
final maxVal = displayValues.isNotEmpty
|
||||
? displayValues.fold<num>(
|
||||
0,
|
||||
(currentMax, entry) =>
|
||||
entry.$2 > currentMax ? entry.$2 : currentMax,
|
||||
)
|
||||
: 0;
|
||||
|
||||
final maxBarWidth = constraints.maxWidth * 0.65;
|
||||
return Column(
|
||||
children: [
|
||||
// Bars
|
||||
...List.generate(valuesShown, (index) {
|
||||
/// Fraction of wins
|
||||
final double fraction = (maxVal > 0)
|
||||
? (displayValues[index].$2 / maxVal)
|
||||
: 0.0;
|
||||
children: List.generate(min(values.length, itemCount), (index) {
|
||||
/// The maximum wins among all players
|
||||
final maxMatches = values.isNotEmpty ? values[0].$2 : 0;
|
||||
|
||||
/// Calculated width for current the bar
|
||||
final double barWidth = (maxBarWidth * fraction).clamp(
|
||||
0.0,
|
||||
maxBarWidth,
|
||||
);
|
||||
/// Fraction of wins
|
||||
final double fraction = (maxMatches > 0)
|
||||
? (values[index].$2 / maxMatches)
|
||||
: 0.0;
|
||||
|
||||
final barClr = index >= displayCount
|
||||
? barColor.withAlpha(150)
|
||||
: barColor;
|
||||
/// Calculated width for current the bar
|
||||
final double barWidth = maxBarWidth * fraction;
|
||||
|
||||
var textClr = barColor.computeLuminance() > 0.5
|
||||
? const Color(0xFF101010)
|
||||
: CustomTheme.textColor;
|
||||
textClr = textClr.withAlpha(
|
||||
index >= displayCount ? 220 : 255,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: maxBarWidth,
|
||||
child: Stack(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
children: [
|
||||
// Bar
|
||||
Container(
|
||||
height: 24,
|
||||
width: barWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: barClr,
|
||||
),
|
||||
),
|
||||
|
||||
// Player
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: RichText(
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: displayValues[index].$1.name,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: textClr,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: getNameCountText(
|
||||
displayValues[index].$1,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color:
|
||||
(barColor ==
|
||||
getColorFromAppColor(
|
||||
AppColor.yellow,
|
||||
)
|
||||
? const Color(
|
||||
0xFF101010,
|
||||
)
|
||||
: CustomTheme.textColor)
|
||||
.withAlpha(150),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// Value
|
||||
Center(
|
||||
child: Text(
|
||||
displayValues[index].$2 <= 1 &&
|
||||
displayValues[index].$2 is double
|
||||
? displayValues[index].$2.toStringAsFixed(2)
|
||||
: displayValues[index].$2.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: 24,
|
||||
width: barWidth,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
color: barColor,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4.0),
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: values[index].$1.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: getNameCountText(values[index].$1),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor.withAlpha(
|
||||
150,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
Center(
|
||||
child: Text(
|
||||
values[index].$2 <= 1 && values[index].$2 is double
|
||||
? values[index].$2.toStringAsFixed(2)
|
||||
: values[index].$2.toString(),
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
|
||||
// Group & Game info
|
||||
if (hasGame || hasGroup)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
// Game
|
||||
if (hasGroup)
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(
|
||||
RpgAwesome.clovers_card,
|
||||
color: CustomTheme.hintColor,
|
||||
size: 20,
|
||||
),
|
||||
Text(
|
||||
getGameText(selectedGames!),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.hintColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasGroup && hasGame) const SizedBox(width: 20),
|
||||
|
||||
// Group
|
||||
if (hasGroup)
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.groups,
|
||||
color: CustomTheme.hintColor,
|
||||
),
|
||||
Text(
|
||||
getGroupText(selectedGroups!),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.hintColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -266,24 +144,4 @@ class StatisticsTile extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String getGroupText(List<Group> groups) {
|
||||
var text = groups[0].name;
|
||||
if (groups.length > 1) {
|
||||
return '$text + ${groups.length - 1}';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
String getGameText(List<Game> games) {
|
||||
var text = games[0].name;
|
||||
if (games.length > 1) {
|
||||
return '$text + ${games.length - 1}';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
bool get hasGroup => selectedGroups != null && selectedGroups!.isNotEmpty;
|
||||
|
||||
bool get hasGame => selectedGames != null && selectedGames!.isNotEmpty;
|
||||
}
|
||||
|
||||
144
lib/presentation/widgets/tiles/team_creation_tile.dart
Normal file
144
lib/presentation/widgets/tiles/team_creation_tile.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/font_awesome_icons.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/constants.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
|
||||
class TeamCreationTile extends StatefulWidget {
|
||||
const TeamCreationTile({
|
||||
super.key,
|
||||
required this.color,
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
this.onDelete,
|
||||
this.onColorSelection,
|
||||
});
|
||||
|
||||
final AppColor color;
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
final String hintText;
|
||||
|
||||
final VoidCallback? onDelete;
|
||||
|
||||
final ValueChanged<AppColor>? onColorSelection;
|
||||
|
||||
@override
|
||||
State<TeamCreationTile> createState() => _TeamCreationTileState();
|
||||
}
|
||||
|
||||
class _TeamCreationTileState extends State<TeamCreationTile> {
|
||||
final teamColors = List.generate(
|
||||
AppColor.values.length,
|
||||
(index) => getTeamColor(index),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Container(
|
||||
margin: CustomTheme.standardMargin,
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12,
|
||||
horizontal: 6,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Name input + delete icon
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextInputField(
|
||||
controller: widget.controller,
|
||||
hintText: widget.hintText,
|
||||
maxLength: Constants.MAX_TEAM_NAME_LENGTH,
|
||||
),
|
||||
),
|
||||
HapticIconButton(
|
||||
icon: const Icon(FontAwesome.trash),
|
||||
color: CustomTheme.textColor,
|
||||
iconSize: 25,
|
||||
onPressed: widget.onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Color label
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: Text(
|
||||
loc.color,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Color picker
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: teamColors.map((color) {
|
||||
final isSelected = widget.color == color;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
widget.onColorSelection?.call(color);
|
||||
},
|
||||
child: Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: getColorFromGameColor(color),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: Colors.transparent,
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
Icons.check,
|
||||
size: 18,
|
||||
color: Colors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -4,15 +4,14 @@ import 'package:tallee/core/custom_theme.dart';
|
||||
class TextIconTile extends StatelessWidget {
|
||||
/// A tile widget that displays text with an optional icon that can be tapped.
|
||||
/// - [text]: The text to display in the tile.
|
||||
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
|
||||
/// - [onIconTap]: The callback to be invoked when the icon is tapped.
|
||||
/// - [icon]: Optional custom icon. Defaults to [Icons.close].
|
||||
const TextIconTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.suffixText = '',
|
||||
this.iconEnabled = true,
|
||||
this.onIconTap,
|
||||
this.onTileTap,
|
||||
this.icon = Icons.close,
|
||||
});
|
||||
|
||||
/// The text to display in the tile.
|
||||
@@ -20,64 +19,57 @@ class TextIconTile extends StatelessWidget {
|
||||
|
||||
final String suffixText;
|
||||
|
||||
/// A boolean to determine if the icon should be displayed.
|
||||
final bool iconEnabled;
|
||||
|
||||
/// The callback to be invoked when the icon is tapped.
|
||||
final VoidCallback? onIconTap;
|
||||
|
||||
/// The callback to be invoked when the tile is tapped.
|
||||
final VoidCallback? onTileTap;
|
||||
/// The icon to display. Defaults to [Icons.close].
|
||||
final IconData icon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTileTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.onBoxColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (iconEnabled) const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
final iconEnabled = onIconTap != null;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.onBoxColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (iconEnabled) const SizedBox(width: 3),
|
||||
Flexible(
|
||||
child: RichText(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
style: DefaultTextStyle.of(context).style,
|
||||
children: [
|
||||
TextSpan(
|
||||
text: text,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
TextSpan(
|
||||
text: suffixText,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CustomTheme.textColor.withAlpha(120),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: suffixText,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: CustomTheme.textColor.withAlpha(120),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (iconEnabled) ...<Widget>[
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(
|
||||
onTap: onIconTap,
|
||||
child: const Icon(Icons.close, size: 20),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (iconEnabled) ...<Widget>[
|
||||
const SizedBox(width: 3),
|
||||
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ class DataTransferService {
|
||||
static Future<void> deleteAllData(BuildContext context) async {
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
|
||||
await db.statisticDao.deleteAllStatistics();
|
||||
await db.matchDao.deleteAllMatches();
|
||||
await db.teamDao.deleteAllTeams();
|
||||
await db.groupDao.deleteAllGroups();
|
||||
@@ -201,13 +200,9 @@ class DataTransferService {
|
||||
.map((id) => playerById[id])
|
||||
.whereType<Player>()
|
||||
.toList();
|
||||
final team = Team.fromJson(map);
|
||||
|
||||
return Team(
|
||||
id: map['id'] as String,
|
||||
name: map['name'] as String,
|
||||
members: members,
|
||||
createdAt: DateTime.parse(map['createdAt'] as String),
|
||||
);
|
||||
return team.copyWith(members: members);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -232,6 +227,7 @@ class DataTransferService {
|
||||
final endedAt = map['endedAt'] != null
|
||||
? DateTime.parse(map['endedAt'] as String)
|
||||
: null;
|
||||
final isTeamMatch = map['isTeamMatch'] as bool;
|
||||
final notes = map['notes'] as String? ?? '';
|
||||
final scoresJson = map['scores'] as Map<String, dynamic>? ?? {};
|
||||
final scores = scoresJson.map(
|
||||
@@ -263,6 +259,7 @@ class DataTransferService {
|
||||
game: game,
|
||||
group: group,
|
||||
players: players,
|
||||
isTeamMatch: isTeamMatch,
|
||||
teams: teams.isEmpty ? null : teams,
|
||||
createdAt: createdAt,
|
||||
endedAt: endedAt,
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@@ -17,14 +17,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.1"
|
||||
animated_custom_dropdown:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: animated_custom_dropdown
|
||||
sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
arb_utils:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -361,14 +353,6 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
dropdown_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dropdown_flutter
|
||||
sha256: "5ae3d05d768d0bb6030ff735e6b4b93f7b29be3cf3bec7c86cd4f444c8f067ff"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
26
pubspec.yaml
26
pubspec.yaml
@@ -1,21 +1,19 @@
|
||||
name: tallee
|
||||
description: "Tracking App for Card Games"
|
||||
publish_to: 'none'
|
||||
version: 0.0.33+281
|
||||
version: 0.0.33+340
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.0
|
||||
sdk: ^3.8.1
|
||||
|
||||
dependencies:
|
||||
animated_custom_dropdown: ^3.1.1
|
||||
clock: ^1.1.2
|
||||
collection: ^1.19.1
|
||||
dropdown_flutter: ^1.0.3
|
||||
cupertino_icons: ^1.0.9
|
||||
drift: ^2.33.0
|
||||
drift_flutter: ^0.3.0
|
||||
cupertino_icons: ^1.0.6
|
||||
drift: ^2.27.0
|
||||
drift_flutter: ^0.2.4
|
||||
file_picker: ^11.0.2
|
||||
file_saver: ^0.4.0
|
||||
file_saver: ^0.3.1
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_localizations:
|
||||
@@ -26,20 +24,20 @@ dependencies:
|
||||
font_awesome_flutter: ^11.0.0
|
||||
intl: any
|
||||
json_schema: ^5.2.2
|
||||
package_info_plus: ^9.0.1
|
||||
package_info_plus: ^9.0.0
|
||||
path_provider: ^2.1.5
|
||||
provider: ^6.1.5
|
||||
skeletonizer: ^2.1.3
|
||||
skeletonizer: ^2.1.0+1
|
||||
url_launcher: ^6.3.2
|
||||
uuid: ^4.5.3
|
||||
uuid: ^4.5.2
|
||||
|
||||
dev_dependencies:
|
||||
arb_utils: ^0.11.0
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
build_runner: ^2.15.0
|
||||
dart_pubspec_licenses: ^3.2.0
|
||||
drift_dev: ^2.33.0
|
||||
build_runner: ^2.7.0
|
||||
dart_pubspec_licenses: ^3.0.14
|
||||
drift_dev: ^2.27.0
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
|
||||
@@ -194,31 +194,6 @@ void main() {
|
||||
expect(allGroups, isEmpty);
|
||||
});
|
||||
|
||||
test('getGroupsByPlayer() works correctly', () async {
|
||||
await database.groupDao.addGroupsAsList(
|
||||
groups: [testGroup1, testGroup2],
|
||||
);
|
||||
|
||||
final groups = await database.groupDao.getGroupsByPlayer(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
|
||||
expect(groups, hasLength(2));
|
||||
expect(groups.any((group) => group.id == testGroup1.id), isTrue);
|
||||
expect(groups.any((group) => group.id == testGroup2.id), isTrue);
|
||||
});
|
||||
|
||||
test(
|
||||
'getGroupsByPlayer() returns empty list for non-existent player',
|
||||
() async {
|
||||
final groups = await database.groupDao.getGroupsByPlayer(
|
||||
playerId: 'non-existent-player-id',
|
||||
);
|
||||
|
||||
expect(groups, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('addGroupsAsList() with duplicate groups only adds once', () async {
|
||||
await database.groupDao.addGroupsAsList(
|
||||
groups: [testGroup1, testGroup1, testGroup1],
|
||||
|
||||
@@ -260,34 +260,6 @@ void main() {
|
||||
expect(match.group!.id, testGroup1.id);
|
||||
});
|
||||
|
||||
test('getMatchesByPlayer() works correctly', () async {
|
||||
await database.matchDao.addMatchesAsList(
|
||||
matches: [testMatch1, testMatch2],
|
||||
);
|
||||
|
||||
final matches = await database.matchDao.getMatchesByPlayer(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.id, testMatch2.id);
|
||||
expect(
|
||||
matches.first.players.any((p) => p.id == testPlayer1.id),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'getMatchesByPlayer() returns empty list for non-existent player',
|
||||
() async {
|
||||
final matches = await database.matchDao.getMatchesByPlayer(
|
||||
playerId: 'non-existing-player-id',
|
||||
);
|
||||
|
||||
expect(matches, isEmpty);
|
||||
},
|
||||
);
|
||||
|
||||
test('getMatchCount() works correctly', () async {
|
||||
var count = await database.matchDao.getMatchCount();
|
||||
expect(count, 0);
|
||||
@@ -535,34 +507,36 @@ void main() {
|
||||
deleted = await database.matchDao.deleteAllMatches();
|
||||
expect(deleted, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
test('deleteMatchesByGame() deletes all matches for a game', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
await database.matchDao.addMatch(match: testMatch2);
|
||||
test('deleteMatchesByGame() deletes all matches for a game', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
await database.matchDao.addMatch(match: testMatch2);
|
||||
|
||||
var count = await database.matchDao.getMatchCountByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(count, 2);
|
||||
var count = await database.matchDao.getMatchCountByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(count, 2);
|
||||
|
||||
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(deletedCount, 2);
|
||||
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(deletedCount, 2);
|
||||
|
||||
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
|
||||
expect(count, 0);
|
||||
count = await database.matchDao.getMatchCountByGame(
|
||||
gameId: testGame.id,
|
||||
);
|
||||
expect(count, 0);
|
||||
|
||||
final allMatches = await database.matchDao.getAllMatches();
|
||||
expect(allMatches, isEmpty);
|
||||
});
|
||||
final allMatches = await database.matchDao.getAllMatches();
|
||||
expect(allMatches, isEmpty);
|
||||
});
|
||||
|
||||
test('deleteMatchesByGame() returns 0 for non-existent game', () async {
|
||||
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||
gameId: 'non-existent-game-id',
|
||||
);
|
||||
expect(deletedCount, 0);
|
||||
test('deleteMatchesByGame() returns 0 for non-existent game', () async {
|
||||
final deletedCount = await database.matchDao.deleteMatchesByGame(
|
||||
gameId: 'non-existent-game-id',
|
||||
);
|
||||
expect(deletedCount, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:core' hide Match;
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/drift.dart' hide isNotNull, isNull;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
@@ -327,5 +327,200 @@ void main() {
|
||||
expect(deleted, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('SCORE', () {
|
||||
test('updateTeamScore() works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
final updated = await database.teamDao.updateTeamScore(
|
||||
teamId: testTeam1.id,
|
||||
matchId: testMatch1.id,
|
||||
score: 5,
|
||||
);
|
||||
expect(updated, isTrue);
|
||||
final team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, 5);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 5);
|
||||
}
|
||||
});
|
||||
|
||||
test('set-/removeWinnerTeam() works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
final set = await database.teamDao.setWinnerTeam(
|
||||
teamId: testTeam1.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(set, isTrue);
|
||||
|
||||
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, 1);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 1);
|
||||
}
|
||||
|
||||
final removed = await database.teamDao.removeWinnerTeam(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(removed, isTrue);
|
||||
|
||||
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, isNull);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNull);
|
||||
}
|
||||
});
|
||||
|
||||
test('set-/removeLoserTeam() works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
final set = await database.teamDao.setLoserTeam(
|
||||
teamId: testTeam1.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(set, isTrue);
|
||||
|
||||
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, 0);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 0);
|
||||
}
|
||||
|
||||
final removed = await database.teamDao.removeLoserTeam(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(removed, isTrue);
|
||||
|
||||
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, isNull);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNull);
|
||||
}
|
||||
});
|
||||
|
||||
test('set-/removeWinnerTeams() works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
final set = await database.teamDao.setWinnerTeams(
|
||||
winners: [testTeam1, testTeam2],
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(set, isTrue);
|
||||
|
||||
// check both teams got the winner score
|
||||
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, 1);
|
||||
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
|
||||
expect(team.score, 1);
|
||||
|
||||
// check all members of both teams got the winner score
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 1);
|
||||
}
|
||||
|
||||
for (final member in testTeam2.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 1);
|
||||
}
|
||||
|
||||
final removed = await database.teamDao.removeWinnerTeam(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(removed, isTrue);
|
||||
|
||||
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, isNull);
|
||||
|
||||
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
|
||||
expect(team.score, isNull);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNull);
|
||||
}
|
||||
|
||||
for (final member in testTeam2.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNull);
|
||||
}
|
||||
});
|
||||
|
||||
test('setTeamPlacements() works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
final set = await database.teamDao.setTeamPlacements(
|
||||
teams: [testTeam1, testTeam2],
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(set, isTrue);
|
||||
|
||||
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
|
||||
expect(team.score, 2);
|
||||
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
|
||||
expect(team.score, 1);
|
||||
|
||||
for (final member in testTeam1.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 2);
|
||||
}
|
||||
|
||||
for (final member in testTeam2.members) {
|
||||
final entry = await database.scoreEntryDao.getScore(
|
||||
playerId: member.id,
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(entry, isNotNull);
|
||||
expect(entry!.score, 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,95 +233,6 @@ void main() {
|
||||
expect(allPlayers, isEmpty);
|
||||
});
|
||||
|
||||
test('updatePlayerName() sets correct nameCount with 2 player', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
await database.playerDao.addPlayer(player: testPlayer2);
|
||||
|
||||
final newName = testPlayer1.name;
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer2.id,
|
||||
name: newName,
|
||||
);
|
||||
|
||||
var player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 1);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 2);
|
||||
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer1.id,
|
||||
name: 'different name',
|
||||
);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 0);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 0);
|
||||
});
|
||||
|
||||
test('updatePlayerName() sets correct nameCount with 3 player', () async {
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||
);
|
||||
|
||||
// Changing both names to player 1's name
|
||||
final newName = testPlayer1.name;
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer2.id,
|
||||
name: newName,
|
||||
);
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer3.id,
|
||||
name: newName,
|
||||
);
|
||||
|
||||
var player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 1);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 2);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer3.id,
|
||||
);
|
||||
expect(player.nameCount, 3);
|
||||
|
||||
// Changing the middle players name
|
||||
await database.playerDao.updatePlayerName(
|
||||
playerId: testPlayer2.id,
|
||||
name: 'different name',
|
||||
);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer1.id,
|
||||
);
|
||||
expect(player.nameCount, 1);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer2.id,
|
||||
);
|
||||
expect(player.nameCount, 0);
|
||||
|
||||
player = await database.playerDao.getPlayerById(
|
||||
playerId: testPlayer3.id,
|
||||
);
|
||||
expect(player.nameCount, 2);
|
||||
});
|
||||
|
||||
test('updatePlayerDescription() works correctly', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
@@ -461,22 +372,14 @@ void main() {
|
||||
final player1 = Player(name: testPlayer1.name, description: '');
|
||||
await database.playerDao.addPlayer(player: player1);
|
||||
|
||||
final player2 = Player(name: testPlayer1.name, description: '');
|
||||
await database.playerDao.addPlayer(player: player2);
|
||||
|
||||
var players = await database.playerDao.getAllPlayers();
|
||||
|
||||
expect(players.length, 3);
|
||||
expect(players.length, 2);
|
||||
players.sort((a, b) => a.nameCount.compareTo(b.nameCount));
|
||||
|
||||
for (int i = 0; i < players.length - 1; i++) {
|
||||
expect(players[i].nameCount, i + 1);
|
||||
}
|
||||
|
||||
// ids are correct in the right order
|
||||
expect(players[0].id, testPlayer1.id);
|
||||
expect(players[1].id, player1.id);
|
||||
expect(players[2].id, player2.id);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -501,62 +404,24 @@ void main() {
|
||||
for (int i = 0; i < players.length - 1; i++) {
|
||||
expect(players[i].nameCount, i + 1);
|
||||
}
|
||||
|
||||
// ids are correct in the right order
|
||||
expect(players[0].id, testPlayer1.id);
|
||||
expect(players[1].id, player1.id);
|
||||
expect(players[2].id, player2.id);
|
||||
expect(players[3].id, player3.id);
|
||||
},
|
||||
);
|
||||
|
||||
test('getNameCount works correctly', () async {
|
||||
final player1 = Player(name: testPlayer1.name);
|
||||
test('getNameCount works correctly', () async {
|
||||
final player2 = Player(name: testPlayer1.name);
|
||||
final player3 = Player(name: testPlayer1.name);
|
||||
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
var nameCount = await database.playerDao.getNameCount(
|
||||
name: testPlayer1.name,
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, player2, player3],
|
||||
);
|
||||
|
||||
expect(nameCount, 1);
|
||||
|
||||
await database.playerDao.addPlayersAsList(players: [player1, player2]);
|
||||
|
||||
nameCount = await database.playerDao.getNameCount(
|
||||
final nameCount = await database.playerDao.getNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
|
||||
expect(nameCount, 3);
|
||||
});
|
||||
|
||||
test('calculateNameCount works correctly', () async {
|
||||
final player1 = Player(name: testPlayer1.name);
|
||||
final player2 = Player(name: testPlayer1.name);
|
||||
|
||||
// Case 1: No existing players with the name
|
||||
var nameCount = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(nameCount, 0);
|
||||
|
||||
// Case 2: One existing player with the name. Should return 2 for
|
||||
// the new player
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
nameCount = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(nameCount, 2);
|
||||
|
||||
// Case 3: Multiple existing players with the name. Should return count + 1
|
||||
await database.playerDao.addPlayersAsList(players: [player1, player2]);
|
||||
nameCount = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(nameCount, 4);
|
||||
});
|
||||
|
||||
test('updateNameCount works correctly', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
@@ -576,24 +441,14 @@ void main() {
|
||||
final player2 = Player(name: testPlayer1.name, description: '');
|
||||
final player3 = Player(name: testPlayer1.name, description: '');
|
||||
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
var player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
name: testPlayer1.name,
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, player2, player3],
|
||||
);
|
||||
expect(player, isNotNull);
|
||||
expect(player!.nameCount, 0);
|
||||
|
||||
await database.playerDao.addPlayer(player: player2);
|
||||
player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
final player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(player, isNotNull);
|
||||
expect(player!.nameCount, 2);
|
||||
|
||||
await database.playerDao.addPlayer(player: player3);
|
||||
player = await database.playerDao.getPlayerWithHighestNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(player, isNotNull);
|
||||
expect(player!.nameCount, 3);
|
||||
});
|
||||
@@ -605,6 +460,32 @@ void main() {
|
||||
expect(player, isNull);
|
||||
});
|
||||
|
||||
test('calculateNameCount works correctly', () async {
|
||||
// Case 1: No existing players with the name
|
||||
var count = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(count, 0);
|
||||
|
||||
// Case 2: One existing player with the name. Should update that
|
||||
// player's nameCount to 1 and return 2 for the new player
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
|
||||
count = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(count, 2);
|
||||
|
||||
// Case 3: Multiple existing players with the name.
|
||||
final player2 = Player(name: testPlayer1.name, nameCount: count);
|
||||
await database.playerDao.addPlayer(player: player2);
|
||||
|
||||
count = await database.playerDao.calculateNameCount(
|
||||
name: testPlayer1.name,
|
||||
);
|
||||
expect(count, 3);
|
||||
});
|
||||
|
||||
test('getPlayerWithHighestNameCount with non existing player', () async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
await database.playerDao.initializeNameCount(name: testPlayer1.name);
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import 'dart:core';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart' hide isNotNull;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/models/game.dart';
|
||||
import 'package:tallee/data/models/group.dart';
|
||||
import 'package:tallee/data/models/player.dart';
|
||||
import 'package:tallee/data/models/statistic.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase database;
|
||||
late Player testPlayer1;
|
||||
late Player testPlayer2;
|
||||
late Player testPlayer3;
|
||||
late Player testPlayer4;
|
||||
late Player testPlayer5;
|
||||
late Group testGroup1;
|
||||
late Group testGroup2;
|
||||
late Game testGame;
|
||||
/*late Match testMatch1;
|
||||
late Match testMatch2;
|
||||
late Match testMatchOnlyPlayers;
|
||||
late Match testMatchOnlyGroup;*/
|
||||
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
|
||||
final fakeClock = Clock(() => fixedDate);
|
||||
|
||||
setUp(() async {
|
||||
database = AppDatabase(
|
||||
DatabaseConnection(
|
||||
NativeDatabase.memory(),
|
||||
// Recommended for widget tests to avoid test errors.
|
||||
closeStreamsSynchronously: true,
|
||||
),
|
||||
);
|
||||
|
||||
withClock(fakeClock, () {
|
||||
testPlayer1 = Player(name: 'Alice');
|
||||
testPlayer2 = Player(name: 'Bob');
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testPlayer5 = Player(name: 'Eve');
|
||||
testGroup1 = Group(
|
||||
name: 'Test Group 1',
|
||||
description: '',
|
||||
members: [testPlayer1, testPlayer2, testPlayer3],
|
||||
);
|
||||
testGroup2 = Group(
|
||||
name: 'Test Group 2',
|
||||
description: '',
|
||||
members: [testPlayer4, testPlayer5],
|
||||
);
|
||||
testGame = Game(
|
||||
name: 'Test Game',
|
||||
ruleset: Ruleset.singleWinner,
|
||||
description: 'A test game',
|
||||
color: AppColor.blue,
|
||||
icon: '',
|
||||
);
|
||||
/*testMatch1 = Match(
|
||||
name: 'First Test Match',
|
||||
game: testGame,
|
||||
group: testGroup1,
|
||||
players: [testPlayer4, testPlayer5],
|
||||
scores: {testPlayer4.id: ScoreEntry(score: 1)},
|
||||
);
|
||||
testMatch2 = Match(
|
||||
name: 'Second Test Match',
|
||||
game: testGame,
|
||||
group: testGroup2,
|
||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||
);
|
||||
testMatchOnlyPlayers = Match(
|
||||
name: 'Test Match with Players',
|
||||
game: testGame,
|
||||
players: [testPlayer1, testPlayer2, testPlayer3],
|
||||
);
|
||||
testMatchOnlyGroup = Match(
|
||||
name: 'Test Match with Group',
|
||||
game: testGame,
|
||||
group: testGroup2,
|
||||
players: testGroup2.members,
|
||||
);*/
|
||||
});
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [
|
||||
testPlayer1,
|
||||
testPlayer2,
|
||||
testPlayer3,
|
||||
testPlayer4,
|
||||
testPlayer5,
|
||||
],
|
||||
);
|
||||
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
|
||||
await database.gameDao.addGame(game: testGame);
|
||||
});
|
||||
tearDown(() async {
|
||||
await database.close();
|
||||
});
|
||||
|
||||
test('Adding/fetching statistic works correclty', () async {
|
||||
final statistic = Statistic(
|
||||
type: StatisticType.averageScore,
|
||||
scopes: [StatisticScope.allPlayers, StatisticScope.selectedGames],
|
||||
timeframe: Timeframe.allTime,
|
||||
selectedGames: [testGame],
|
||||
selectedGroups: [testGroup1],
|
||||
);
|
||||
|
||||
final added = await database.statisticDao.addStatistic(
|
||||
statistic: statistic,
|
||||
);
|
||||
expect(added, isTrue);
|
||||
|
||||
final fetched = await database.statisticDao.getStatisticById(statistic.id);
|
||||
expect(fetched, isNotNull);
|
||||
expect(fetched!.type, statistic.type);
|
||||
});
|
||||
}
|
||||
@@ -55,7 +55,12 @@ void main() {
|
||||
members: [testPlayer1, testPlayer2],
|
||||
);
|
||||
|
||||
testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]);
|
||||
testTeam = Team(
|
||||
name: 'Test Team',
|
||||
color: AppColor.yellow,
|
||||
score: 5,
|
||||
members: [testPlayer1, testPlayer2],
|
||||
);
|
||||
|
||||
testMatch = Match(
|
||||
name: 'Test Match',
|
||||
@@ -137,9 +142,6 @@ void main() {
|
||||
await database.playerDao.addPlayer(player: testPlayer2);
|
||||
await database.gameDao.addGame(game: testGame);
|
||||
await database.groupDao.addGroup(group: testGroup);
|
||||
/*
|
||||
await database.teamDao.addTeam(team: testTeam);
|
||||
*/
|
||||
await database.matchDao.addMatch(match: testMatch);
|
||||
|
||||
final ctx = await getContext(tester);
|
||||
@@ -669,6 +671,8 @@ void main() {
|
||||
'name': testTeam.name,
|
||||
'memberIds': [testPlayer1.id],
|
||||
'createdAt': testTeam.createdAt.toIso8601String(),
|
||||
'color': testTeam.color.name,
|
||||
'score': testTeam.score,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -682,6 +686,8 @@ void main() {
|
||||
expect(teams[0].name, testTeam.name);
|
||||
expect(teams[0].members.length, 1);
|
||||
expect(teams[0].members[0].id, testPlayer1.id);
|
||||
expect(teams[0].color, testTeam.color);
|
||||
expect(teams[0].score, testTeam.score);
|
||||
});
|
||||
|
||||
test('parseTeamsFromJson() empty list', () {
|
||||
@@ -718,6 +724,9 @@ void main() {
|
||||
'gameId': testGame.id,
|
||||
'groupId': testGroup.id,
|
||||
'playerIds': [testPlayer1.id, testPlayer2.id],
|
||||
'isTeamMatch': false,
|
||||
'teams': null,
|
||||
'scores': null,
|
||||
'notes': testMatch.notes,
|
||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||
},
|
||||
@@ -773,6 +782,9 @@ void main() {
|
||||
'name': testMatch.name,
|
||||
'gameId': 'non-existent-game-id',
|
||||
'playerIds': [testPlayer1.id],
|
||||
'isTeamMatch': false,
|
||||
'teams': null,
|
||||
'scores': null,
|
||||
'notes': '',
|
||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||
},
|
||||
@@ -804,6 +816,9 @@ void main() {
|
||||
'gameId': testGame.id,
|
||||
'groupId': null,
|
||||
'playerIds': [testPlayer1.id],
|
||||
'isTeamMatch': false,
|
||||
'teams': null,
|
||||
'scores': null,
|
||||
'notes': '',
|
||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||
},
|
||||
@@ -834,6 +849,9 @@ void main() {
|
||||
'name': testMatch.name,
|
||||
'gameId': testGame.id,
|
||||
'playerIds': [testPlayer1.id],
|
||||
'isTeamMatch': false,
|
||||
'teams': null,
|
||||
'scores': null,
|
||||
'notes': '',
|
||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||
'endedAt': endedDate.toIso8601String(),
|
||||
@@ -853,7 +871,7 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('validateJsonSchema()', () async {
|
||||
test('validateJsonSchema() works correctly', () async {
|
||||
final validJson = json.encode({
|
||||
'players': [
|
||||
{
|
||||
@@ -897,6 +915,15 @@ void main() {
|
||||
},
|
||||
'createdAt': testMatch.createdAt.toIso8601String(),
|
||||
'endedAt': null,
|
||||
'isTeamMatch': true,
|
||||
'teams': [
|
||||
{
|
||||
'id': testTeam.id,
|
||||
'name': testTeam.name,
|
||||
'memberIds': [testPlayer1.id, testPlayer2.id],
|
||||
'createdAt': testTeam.createdAt.toIso8601String(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -904,5 +931,28 @@ void main() {
|
||||
final isValid = await DataTransferService.validateJsonSchema(validJson);
|
||||
expect(isValid, true);
|
||||
});
|
||||
|
||||
testWidgets('validateJsonSchema() validates exported json file', (
|
||||
tester,
|
||||
) async {
|
||||
await database.playerDao.addPlayer(player: testPlayer1);
|
||||
await database.playerDao.addPlayer(player: testPlayer2);
|
||||
await database.gameDao.addGame(game: testGame);
|
||||
await database.groupDao.addGroup(group: testGroup);
|
||||
await database.matchDao.addMatch(match: testMatch);
|
||||
|
||||
final ctx = await getContext(tester);
|
||||
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
|
||||
|
||||
expect(jsonString, isNotEmpty);
|
||||
|
||||
// Schema validation requires real async operations (rootBundle,
|
||||
// HttpClient within json_schema). These must run via
|
||||
// tester.runAsync, otherwise the test hangs due to a pending timer.
|
||||
final isValid = await tester.runAsync(
|
||||
() => DataTransferService.validateJsonSchema(jsonString),
|
||||
);
|
||||
expect(isValid, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user