42 Commits

Author SHA1 Message Date
eaf7822732 Revert to 4bd2f97
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 45s
Pull Request Pipeline / localizations (pull_request) Successful in 29s
Pull Request Pipeline / test (pull_request) Successful in 1m31s
2026-05-25 19:22:59 +02:00
6679a0f942 Updated licenses [skip ci] 2026-05-25 12:56:12 +00:00
bf6c352d54 Updated version number [skip ci] 2026-05-25 12:55:35 +00:00
9b208f4780 Revert "Merge branch 'feature/193-statisticsview-rework' into development"
All checks were successful
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 38s
Push Pipeline / generate_localizations (push) Successful in 29s
Push Pipeline / test (push) Successful in 1m35s
Push Pipeline / sort_arb_files (push) Successful in 31s
Push Pipeline / format (push) Successful in 55s
Push Pipeline / build (push) Successful in 4m58s
This reverts commit 24f49e17b9, reversing
changes made to dba6c218d6.

# Conflicts:
#	pubspec.yaml
2026-05-25 14:55:19 +02:00
5659dc36c2 Updated licenses [skip ci] 2026-05-25 12:52:27 +00:00
712d48b1d7 Updated version number [skip ci] 2026-05-25 12:51:48 +00:00
24f49e17b9 Merge branch 'feature/193-statisticsview-rework' into development
Some checks failed
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 40s
Push Pipeline / generate_localizations (push) Successful in 30s
Push Pipeline / test (push) Successful in 1m34s
Push Pipeline / sort_arb_files (push) Failing after 32s
Push Pipeline / format (push) Has been skipped
Push Pipeline / build (push) Has been skipped
# Conflicts:
#	pubspec.lock
#	pubspec.yaml
2026-05-25 14:51:36 +02:00
dba6c218d6 Updated licenses [skip ci] 2026-05-25 12:12:46 +00:00
82325ea271 Updated version number [skip ci] 2026-05-25 12:12:09 +00:00
f7973a4bc2 fix: build job
All checks were successful
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 37s
Push Pipeline / generate_localizations (push) Successful in 28s
Push Pipeline / test (push) Successful in 1m32s
Push Pipeline / sort_arb_files (push) Successful in 31s
Push Pipeline / format (push) Successful in 54s
Push Pipeline / build (push) Successful in 4m55s
2026-05-25 14:12:01 +02:00
258e668a5e Updated licenses [skip ci] 2026-05-25 12:08:58 +00:00
a951f3c9b2 Updated version number [skip ci] 2026-05-25 12:08:20 +00:00
ad5cd98327 fix: build job
Some checks failed
Push Pipeline / update_version (push) Successful in 5s
Push Pipeline / generate_licenses (push) Successful in 39s
Push Pipeline / generate_localizations (push) Successful in 28s
Push Pipeline / test (push) Successful in 1m32s
Push Pipeline / sort_arb_files (push) Successful in 30s
Push Pipeline / format (push) Successful in 53s
Push Pipeline / build (push) Failing after 14s
2026-05-25 14:08:10 +02:00
250c647fb2 Updated licenses [skip ci] 2026-05-25 12:04:06 +00:00
e7f904296d Updated version number [skip ci] 2026-05-25 12:03:30 +00:00
362ab2a945 Updated test job
Some checks failed
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 38s
Push Pipeline / generate_localizations (push) Successful in 28s
Push Pipeline / test (push) Successful in 1m33s
Push Pipeline / sort_arb_files (push) Successful in 30s
Push Pipeline / format (push) Successful in 54s
Push Pipeline / build (push) Failing after 20s
2026-05-25 14:03:18 +02:00
d4a67f4086 Updated licenses [skip ci] 2026-05-25 12:01:43 +00:00
9fe74c291c Updated version number [skip ci] 2026-05-25 12:01:06 +00:00
84bb8ccccc fix(deps): update dart dependencies (non-major) (#248)
Some checks failed
Push Pipeline / update_version (push) Successful in 6s
Push Pipeline / generate_licenses (push) Successful in 39s
Push Pipeline / test (push) Failing after 47s
Push Pipeline / generate_localizations (push) Successful in 28s
Push Pipeline / sort_arb_files (push) Successful in 33s
Push Pipeline / format (push) Successful in 53s
Push Pipeline / build (push) Has been cancelled
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [build_runner](https://github.com/dart-lang/build) ([source](https://github.com/dart-lang/build/tree/HEAD/build_runner)) | dev_dependencies | minor | `^2.7.0` -> `^2.15.0` |
| [cupertino_icons](https://github.com/flutter/packages) ([source](https://github.com/flutter/packages/tree/HEAD/third_party/packages/cupertino_icons)) | dependencies | patch | `^1.0.6` -> `^1.0.9` |
| [dart](https://dart.dev/) ([source](https://github.com/dart-lang/sdk)) |  | minor | `^3.8.1` -> `^3.12.0` |
| [dart_pubspec_licenses](https://github.com/espresso3389/flutter_oss_licenses/tree/master/packages/dart_pubspec_licenses) ([source](https://github.com/espresso3389/flutter_oss_licenses)) | dev_dependencies | minor | `^3.0.14` -> `^3.2.0` |
| [drift](https://drift.simonbinder.eu/) ([source](https://github.com/simolus3/drift)) | dependencies | minor | `^2.27.0` -> `^2.33.0` |
| [drift_dev](https://drift.simonbinder.eu/) ([source](https://github.com/simolus3/drift)) | dev_dependencies | minor | `^2.27.0` -> `^2.33.0` |
| [drift_flutter](https://drift.simonbinder.eu/) ([source](https://github.com/simolus3/drift)) | dependencies | minor | `^0.2.4` -> `^0.3.0` |
| [file_saver](https://hassanansari.dev) ([source](https://github.com/incrediblezayed/file_saver)) | dependencies | minor | `^0.3.1` -> `^0.4.0` |
| [package_info_plus](https://github.com/fluttercommunity/plus_plugins) ([source](https://github.com/fluttercommunity/plus_plugins/tree/HEAD/packages/package_info_plus/package_info_plus)) | dependencies | patch | `^9.0.0` -> `^9.0.1` |
| [skeletonizer](https://github.com/Milad-Akarie/skeletonizer) | dependencies | patch | `^2.1.0+1` -> `^2.1.3` |
| [uuid](https://github.com/Daegalus/dart-uuid) | dependencies | patch | `^4.5.2` -> `^4.5.3` |

>  **Important**
>
> Release Notes retrieval for this PR were skipped because no github.com credentials were available.
> If you are self-hosted, please see [this instruction](https://github.com/renovatebot/renovate/blob/master/docs/usage/examples/self-hosting.md#githubcom-token-for-release-notes).

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 **Immortal**: This PR will be recreated if closed unmerged. Get [config help](https://github.com/renovatebot/renovate/discussions) if that's undesired.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzOS4yNjQuMSIsInVwZGF0ZWRJblZlciI6IjM5LjI2NC4xIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcG1lbnQiLCJsYWJlbHMiOltdfQ==-->

Co-authored-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
Reviewed-on: #248
Co-authored-by: Gitea Actions <actions@yannick-weigert.de>
Co-committed-by: Gitea Actions <actions@yannick-weigert.de>
2026-05-25 12:00:57 +00:00
e1d0eb4bd4 Merge remote-tracking branch 'origin/feature/193-statisticsview-rework' into feature/193-statisticsview-rework
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 46s
Pull Request Pipeline / test (pull_request) Successful in 48s
Pull Request Pipeline / localizations (pull_request) Successful in 26s
# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations_de.dart
#	lib/l10n/generated/app_localizations_en.dart
2026-05-25 13:25:09 +02:00
4bd2f972df fix: localizations 2026-05-25 13:24:23 +02:00
730341dc7e fix: localizations
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 47s
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / localizations (pull_request) Successful in 28s
2026-05-25 13:17:02 +02:00
fb2f6d3adc feat: dynamic display count shown in tile
Some checks failed
Pull Request Pipeline / lint (pull_request) Successful in 47s
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / localizations (pull_request) Failing after 29s
2026-05-25 13:06:37 +02:00
b9710ed851 fix: pixel overflow 2026-05-25 12:55:36 +02:00
efd1097d5a feat: changing display count 2026-05-25 12:51:24 +02:00
bfb40d2eab feat: statistic detail view
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 47s
Pull Request Pipeline / test (pull_request) Successful in 48s
Pull Request Pipeline / localizations (pull_request) Failing after 27s
2026-05-25 00:39:01 +02:00
72442b5375 fix: added delete function 2026-05-24 23:34:53 +02:00
bccd47e20e Refactoring 2026-05-24 23:27:14 +02:00
428f967010 feat: displayCount 2026-05-24 23:09:08 +02:00
f65ea09cbe Added spacing 2026-05-24 17:33:41 +02:00
ffd52055fa fixed bar length 2026-05-24 17:28:29 +02:00
398c7a4168 Refactoring 2026-05-24 17:07:09 +02:00
d82206319a Renamed GameColor -> AppColor 2026-05-24 17:04:14 +02:00
5a2cc790dd Renamed GameColor -> AppColor 2026-05-24 17:03:58 +02:00
18a5dcfdd5 Changed colors 2026-05-24 17:03:43 +02:00
2e3b462533 feat: added statistic tile factory
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 47s
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / localizations (pull_request) Successful in 27s
2026-05-24 15:11:56 +02:00
807ae61df7 feat: basic database functionality
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 47s
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / localizations (pull_request) Successful in 27s
2026-05-24 13:52:27 +02:00
37031d66c9 Updated attribute order 2026-05-24 12:16:44 +02:00
d389b93cc5 Updated method with join 2026-05-24 12:16:36 +02:00
134f77c5a3 feat: create statistics view
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 48s
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / localizations (pull_request) Successful in 27s
2026-05-24 01:26:08 +02:00
57ebea1eb7 Merge branch 'feature/180-Spielerprofile-implementieren' into feature/193-statisticsview-rework 2026-05-24 01:10:45 +02:00
9ad50c9f9c Removed bg color 2026-05-23 21:52:32 +02:00
59 changed files with 6190 additions and 782 deletions

View File

@@ -31,20 +31,16 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: ghcr.io/cirruslabs/flutter:stable
steps: steps:
- name: Checkout code - name: Install Node
uses: actions/checkout@v4
# Required for Flutter action
- name: Install jq
run: | run: |
apt-get update apt-get update
apt-get install -y jq apt-get install -y nodejs npm
- name: Set up Flutter - name: Checkout code
uses: subosito/flutter-action@v2 uses: actions/checkout@v4
with:
channel: stable
- name: Get dependencies - name: Get dependencies
run: | run: |

View File

@@ -7,22 +7,19 @@ on:
- "main" - "main"
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: ghcr.io/cirruslabs/flutter:stable
steps: steps:
- name: Checkout code - name: Install Node
uses: actions/checkout@v4
# Required for Flutter action
- name: Install jq
run: | run: |
apt-get update apt-get update
apt-get install -y jq apt-get install -y nodejs npm
- name: Set up Flutter - name: Checkout code
uses: subosito/flutter-action@v2 uses: actions/checkout@v4
with:
channel: stable
- name: Get dependencies - name: Get dependencies
run: | run: |
@@ -295,6 +292,8 @@ jobs:
- name: Setup Android SDK - name: Setup Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v3
with:
packages: "platform-tools platforms;android-34 build-tools;34.0.0"
# Required for Flutter action # Required for Flutter action
- name: Install jq - name: Install jq

View File

@@ -1,13 +1,5 @@
{ {
"pins" : [ "pins" : [
{
"identity" : "csqlite",
"kind" : "remoteSourceControl",
"location" : "https://github.com/simolus3/CSQLite.git",
"state" : {
"revision" : "1ee46d19a4f451a7aa64ffc64fc99b4748131e62"
}
},
{ {
"identity" : "dkcamera", "identity" : "dkcamera",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -24,48 +24,48 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
} }
} }
/// Translates a [GameColor] enum value to its corresponding localized string. /// Translates a [AppColor] enum value to its corresponding localized string.
String translateGameColorToString(GameColor color, BuildContext context) { String translateAppColorToString(AppColor color, BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
switch (color) { switch (color) {
case GameColor.red: case AppColor.red:
return loc.color_red; return loc.color_red;
case GameColor.blue: case AppColor.blue:
return loc.color_blue; return loc.color_blue;
case GameColor.green: case AppColor.green:
return loc.color_green; return loc.color_green;
case GameColor.yellow: case AppColor.yellow:
return loc.color_yellow; return loc.color_yellow;
case GameColor.purple: case AppColor.purple:
return loc.color_purple; return loc.color_purple;
case GameColor.orange: case AppColor.orange:
return loc.color_orange; return loc.color_orange;
case GameColor.pink: case AppColor.pink:
return loc.color_pink; return loc.color_pink;
case GameColor.teal: case AppColor.teal:
return loc.color_teal; return loc.color_teal;
} }
} }
/// Returns the [Color] object corresponding to a [GameColor] enum value. /// Returns the [Color] object corresponding to a [AppColor] enum value.
Color getColorFromGameColor(GameColor color) { Color getColorFromAppColor(AppColor color) {
switch (color) { switch (color) {
case GameColor.red: case AppColor.red:
return Colors.red; return Colors.red;
case GameColor.blue: case AppColor.blue:
return Colors.blue; return Colors.blue;
case GameColor.green: case AppColor.green:
return Colors.green; return Colors.green;
case GameColor.yellow: case AppColor.yellow:
return const Color(0xFFF7CA28); return const Color(0xFFF7CA28);
case GameColor.purple: case AppColor.purple:
return Colors.purple; return Colors.purple;
case GameColor.orange: case AppColor.orange:
return const Color(0xFFef681f); return const Color(0xFFef681f);
case GameColor.pink: case AppColor.pink:
return Colors.pink; return const Color(0xFFE91E63);
case GameColor.teal: case AppColor.teal:
return Colors.teal; return const Color(0xFF00BCD4);
} }
} }

View File

@@ -43,4 +43,32 @@ enum Ruleset {
} }
/// Different colors for highlighting games /// Different colors for highlighting games
enum GameColor { red, orange, yellow, green, teal, blue, purple, pink } 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,
}

View File

@@ -77,8 +77,8 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Returns `true` if the game exists, `false` otherwise. /// Returns `true` if the game exists, `false` otherwise.
Future<bool> gameExists({required String gameId}) async { Future<bool> gameExists({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingleOrNull(); final row = await query.getSingleOrNull();
return result != null; return row != null;
} }
/// Retrieves all games from the database. /// Retrieves all games from the database.
@@ -92,7 +92,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
name: row.name, name: row.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset), ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
description: row.description, description: row.description,
color: GameColor.values.firstWhere((e) => e.name == row.color), color: AppColor.values.firstWhere((e) => e.name == row.color),
icon: row.icon, icon: row.icon,
createdAt: row.createdAt, createdAt: row.createdAt,
), ),
@@ -103,15 +103,15 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Retrieves a [Game] by its [gameId]. /// Retrieves a [Game] by its [gameId].
Future<Game> getGameById({required String gameId}) async { Future<Game> getGameById({required String gameId}) async {
final query = select(gameTable)..where((g) => g.id.equals(gameId)); final query = select(gameTable)..where((g) => g.id.equals(gameId));
final result = await query.getSingle(); final row = await query.getSingle();
return Game( return Game(
id: result.id, id: row.id,
name: result.name, name: row.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset), ruleset: Ruleset.values.firstWhere((e) => e.name == row.ruleset),
description: result.description, description: row.description,
color: GameColor.values.firstWhere((e) => e.name == result.color), color: AppColor.values.firstWhere((e) => e.name == row.color),
icon: result.icon, icon: row.icon,
createdAt: result.createdAt, createdAt: row.createdAt,
); );
} }
@@ -123,7 +123,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required String name, required String name,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
GameTableCompanion(name: Value(name)), GameTableCompanion(name: Value(name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -135,7 +135,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required Ruleset ruleset, required Ruleset ruleset,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
GameTableCompanion(ruleset: Value(ruleset.name)), GameTableCompanion(ruleset: Value(ruleset.name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -147,7 +147,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required String description, required String description,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
GameTableCompanion(description: Value(description)), GameTableCompanion(description: Value(description)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -156,10 +156,10 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Updates the color of the game with the given [gameId]. /// Updates the color of the game with the given [gameId].
Future<bool> updateGameColor({ Future<bool> updateGameColor({
required String gameId, required String gameId,
required GameColor color, required AppColor color,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
GameTableCompanion(color: Value(color.name)), GameTableCompanion(color: Value(color.name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -171,7 +171,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
required String icon, required String icon,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write( await (update(gameTable)..where((tbl) => tbl.id.equals(gameId))).write(
GameTableCompanion(icon: Value(icon)), GameTableCompanion(icon: Value(icon)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -182,7 +182,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Deletes the game with the given [gameId] from the database. /// Deletes the game with the given [gameId] from the database.
/// Returns `true` if the game was deleted, `false` if the game did not exist. /// Returns `true` if the game was deleted, `false` if the game did not exist.
Future<bool> deleteGame({required String gameId}) async { Future<bool> deleteGame({required String gameId}) async {
final query = delete(gameTable)..where((g) => g.id.equals(gameId)); final query = delete(gameTable)..where((tbl) => tbl.id.equals(gameId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -143,16 +143,16 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
final query = select(groupTable); final query = select(groupTable);
final result = await query.get(); final result = await query.get();
return Future.wait( return Future.wait(
result.map((groupData) async { result.map((row) async {
final members = await db.playerGroupDao.getPlayersOfGroup( final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupData.id, groupId: row.id,
); );
return Group( return Group(
id: groupData.id, id: row.id,
name: groupData.name, name: row.name,
description: groupData.description, description: row.description,
members: members, members: members,
createdAt: groupData.createdAt, createdAt: row.createdAt,
); );
}), }),
); );
@@ -161,18 +161,18 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Retrieves a [Group] by its [groupId], including its members. /// Retrieves a [Group] by its [groupId], including its members.
Future<Group> getGroupById({required String groupId}) async { Future<Group> getGroupById({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
final result = await query.getSingle(); final row = await query.getSingle();
List<Player> members = await db.playerGroupDao.getPlayersOfGroup( List<Player> members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupId, groupId: groupId,
); );
return Group( return Group(
id: result.id, id: row.id,
name: result.name, name: row.name,
description: result.description, description: row.description,
members: members, members: members,
createdAt: result.createdAt, createdAt: row.createdAt,
); );
} }
@@ -180,7 +180,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
Future<int> getGroupCount() async { Future<int> getGroupCount() async {
final count = final count =
await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) await (selectOnly(groupTable)..addColumns([groupTable.id.count()]))
.map((row) => row.read(groupTable.id.count())) .map((tbl) => tbl.read(groupTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -190,28 +190,28 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
Future<List<Group>> getGroupsByPlayer({required String playerId}) async { Future<List<Group>> getGroupsByPlayer({required String playerId}) async {
final playerGroups = await (select( final playerGroups = await (select(
playerGroupTable, playerGroupTable,
)..where((pg) => pg.playerId.equals(playerId))).get(); )..where((tbl) => tbl.playerId.equals(playerId))).get();
if (playerGroups.isEmpty) return []; if (playerGroups.isEmpty) return [];
final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList(); final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList();
final rows = final result =
await (select(groupTable) await (select(groupTable)
..where((g) => g.id.isIn(groupIds)) ..where((tbl) => tbl.id.isIn(groupIds))
..orderBy([(g) => OrderingTerm.desc(g.createdAt)])) ..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
.get(); .get();
return Future.wait( return Future.wait(
rows.map((groupData) async { result.map((row) async {
final members = await db.playerGroupDao.getPlayersOfGroup( final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupData.id, groupId: row.id,
); );
return Group( return Group(
id: groupData.id, id: row.id,
name: groupData.name, name: row.name,
description: groupData.description, description: row.description,
members: members, members: members,
createdAt: groupData.createdAt, createdAt: row.createdAt,
); );
}), }),
); );
@@ -221,8 +221,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Returns `true` if the group exists, `false` otherwise. /// Returns `true` if the group exists, `false` otherwise.
Future<bool> groupExists({required String groupId}) async { Future<bool> groupExists({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId)); final query = select(groupTable)..where((g) => g.id.equals(groupId));
final result = await query.getSingleOrNull(); final row = await query.getSingleOrNull();
return result != null; return row != null;
} }
/* Delete */ /* Delete */
@@ -252,9 +252,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
required String name, required String name,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(groupId))).write( await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
GroupTableCompanion(name: Value(name)), .write(GroupTableCompanion(name: Value(name)));
);
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -265,9 +264,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
required String description, required String description,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(groupId))).write( await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
GroupTableCompanion(description: Value(description)), .write(GroupTableCompanion(description: Value(description)));
);
return rowsAffected > 0; return rowsAffected > 0;
} }
} }

View File

@@ -258,15 +258,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Returns `true` if the match exists, otherwise `false`. /// Returns `true` if the match exists, otherwise `false`.
Future<bool> matchExists({required String matchId}) async { Future<bool> matchExists({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId)); final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingleOrNull(); final row = await query.getSingleOrNull();
return result != null; return row != null;
} }
/// Retrieves the number of matches in the database. /// Retrieves the number of matches in the database.
Future<int> getMatchCount() async { Future<int> getMatchCount() async {
final count = final count =
await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) await (selectOnly(matchTable)..addColumns([matchTable.id.count()]))
.map((row) => row.read(matchTable.id.count())) .map((tbl) => tbl.read(matchTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -279,10 +279,12 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
return Future.wait( return Future.wait(
result.map((row) async { result.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId); final game = await db.gameDao.getGameById(gameId: row.gameId);
Group? group; Group? group;
if (row.groupId != null) { if (row.groupId != null) {
group = await db.groupDao.getGroupById(groupId: row.groupId!); group = await db.groupDao.getGroupById(groupId: row.groupId!);
} }
final players = await db.playerMatchDao.getPlayersOfMatch( final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id, matchId: row.id,
); );
@@ -312,13 +314,13 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Retrieves a [Match] by its [matchId]. /// Retrieves a [Match] by its [matchId].
Future<Match> getMatchById({required String matchId}) async { Future<Match> getMatchById({required String matchId}) async {
final query = select(matchTable)..where((g) => g.id.equals(matchId)); final query = select(matchTable)..where((g) => g.id.equals(matchId));
final result = await query.getSingle(); final row = await query.getSingle();
final game = await db.gameDao.getGameById(gameId: result.gameId); final game = await db.gameDao.getGameById(gameId: row.gameId);
Group? group; Group? group;
if (result.groupId != null) { if (row.groupId != null) {
group = await db.groupDao.getGroupById(groupId: result.groupId!); group = await db.groupDao.getGroupById(groupId: row.groupId!);
} }
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId);
@@ -328,15 +330,15 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
final teams = await _getMatchTeams(matchId: matchId); final teams = await _getMatchTeams(matchId: matchId);
return Match( return Match(
id: result.id, id: row.id,
name: result.name, name: row.name,
game: game, game: game,
group: group, group: group,
players: players, players: players,
teams: teams.isEmpty ? null : teams, teams: teams.isEmpty ? null : teams,
notes: result.notes, notes: row.notes,
createdAt: result.createdAt, createdAt: row.createdAt,
endedAt: result.endedAt, endedAt: row.endedAt,
scores: scores, scores: scores,
); );
} }
@@ -347,7 +349,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
await (selectOnly(matchTable) await (selectOnly(matchTable)
..where(matchTable.gameId.equals(gameId)) ..where(matchTable.gameId.equals(gameId))
..addColumns([matchTable.id.count()])) ..addColumns([matchTable.id.count()]))
.map((row) => row.read(matchTable.id.count())) .map((tbl) => tbl.read(matchTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -355,19 +357,19 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
Future<List<Match>> getMatchesByPlayer({required String playerId}) async { Future<List<Match>> getMatchesByPlayer({required String playerId}) async {
final playerMatches = await (select( final playerMatches = await (select(
playerMatchTable, playerMatchTable,
)..where((pm) => pm.playerId.equals(playerId))).get(); )..where((tbl) => tbl.playerId.equals(playerId))).get();
if (playerMatches.isEmpty) return []; if (playerMatches.isEmpty) return [];
final matchIds = playerMatches.map((pm) => pm.matchId).toSet().toList(); final matchIds = playerMatches.map((tbl) => tbl.matchId).toSet().toList();
final rows = final result =
await (select(matchTable) await (select(matchTable)
..where((m) => m.id.isIn(matchIds)) ..where((tbl) => tbl.id.isIn(matchIds))
..orderBy([(m) => OrderingTerm.desc(m.createdAt)])) ..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
.get(); .get();
return Future.wait( return Future.wait(
rows.map((row) async { result.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId); final game = await db.gameDao.getGameById(gameId: row.gameId);
Group? group; Group? group;
@@ -403,16 +405,17 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Queries the database directly, filtering by [groupId]. /// Queries the database directly, filtering by [groupId].
Future<List<Match>> getMatchesByGroup({required String groupId}) async { Future<List<Match>> getMatchesByGroup({required String groupId}) async {
final query = select(matchTable)..where((m) => m.groupId.equals(groupId)); final query = select(matchTable)..where((m) => m.groupId.equals(groupId));
final rows = await query.get(); final result = await query.get();
return Future.wait( return Future.wait(
rows.map((row) async { result.map((row) async {
final game = await db.gameDao.getGameById(gameId: row.gameId); final game = await db.gameDao.getGameById(gameId: row.gameId);
final group = await db.groupDao.getGroupById(groupId: groupId); final group = await db.groupDao.getGroupById(groupId: groupId);
final players = await db.playerMatchDao.getPlayersOfMatch( final players = await db.playerMatchDao.getPlayersOfMatch(
matchId: row.id, matchId: row.id,
); );
final teams = await _getMatchTeams(matchId: row.id); final teams = await _getMatchTeams(matchId: row.id);
return Match( return Match(
id: row.id, id: row.id,
name: row.name, name: row.name,
@@ -432,7 +435,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
Future<List<Team>> _getMatchTeams({required String matchId}) async { Future<List<Team>> _getMatchTeams({required String matchId}) async {
// Get all unique team IDs from PlayerMatchTable for this match // Get all unique team IDs from PlayerMatchTable for this match
final playerMatchQuery = select(db.playerMatchTable) final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.matchId.equals(matchId) & pm.teamId.isNotNull()); ..where((tbl) => tbl.matchId.equals(matchId) & tbl.teamId.isNotNull());
final playerMatches = await playerMatchQuery.get(); final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return []; if (playerMatches.isEmpty) return [];
@@ -459,7 +462,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required String name, required String name,
}) async { }) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId)); final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(name: Value(name)), MatchTableCompanion(name: Value(name)),
); );
@@ -474,7 +477,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required String? groupId, required String? groupId,
}) async { }) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId)); final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(groupId: Value(groupId)), MatchTableCompanion(groupId: Value(groupId)),
); );
@@ -487,7 +490,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required String notes, required String notes,
}) async { }) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId)); final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(notes: Value(notes)), MatchTableCompanion(notes: Value(notes)),
); );
@@ -498,7 +501,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Sets the groupId to null. /// Sets the groupId to null.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeMatchGroup({required String matchId}) async { Future<bool> removeMatchGroup({required String matchId}) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId)); final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
const MatchTableCompanion(groupId: Value(null)), const MatchTableCompanion(groupId: Value(null)),
); );
@@ -512,7 +515,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
required String matchId, required String matchId,
required DateTime endedAt, required DateTime endedAt,
}) async { }) async {
final query = update(matchTable)..where((g) => g.id.equals(matchId)); final query = update(matchTable)..where((tbl) => tbl.id.equals(matchId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
MatchTableCompanion(endedAt: Value(endedAt)), MatchTableCompanion(endedAt: Value(endedAt)),
); );
@@ -524,7 +527,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Deletes the match with the given [matchId] from the database. /// Deletes the match with the given [matchId] from the database.
/// Returns `true` if more than 0 rows were affected, otherwise `false`. /// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> deleteMatch({required String matchId}) async { Future<bool> deleteMatch({required String matchId}) async {
final query = delete(matchTable)..where((g) => g.id.equals(matchId)); final query = delete(matchTable)..where((tbl) => tbl.id.equals(matchId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -540,7 +543,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Deletes all matches associated with a specific game. /// Deletes all matches associated with a specific game.
/// Returns the number of matches deleted. /// Returns the number of matches deleted.
Future<int> deleteMatchesByGame({required String gameId}) async { Future<int> deleteMatchesByGame({required String gameId}) async {
final query = delete(matchTable)..where((m) => m.gameId.equals(gameId)); final query = delete(matchTable)..where((tbl) => tbl.gameId.equals(gameId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected; return rowsAffected;
} }

View File

@@ -113,7 +113,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
Future<int> getPlayerCount() async { Future<int> getPlayerCount() async {
final count = final count =
await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) await (selectOnly(playerTable)..addColumns([playerTable.id.count()]))
.map((row) => row.read(playerTable.id.count())) .map((tbl) => tbl.read(playerTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -122,8 +122,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Returns `true` if the player exists, `false` otherwise. /// Returns `true` if the player exists, `false` otherwise.
Future<bool> playerExists({required String playerId}) async { Future<bool> playerExists({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingleOrNull(); final row = await query.getSingleOrNull();
return result != null; return row != null;
} }
/// Retrieves all players from the database. /// Retrieves all players from the database.
@@ -146,13 +146,13 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Retrieves a [Player] by their [id]. /// Retrieves a [Player] by their [id].
Future<Player> getPlayerById({required String playerId}) async { Future<Player> getPlayerById({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId)); final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingle(); final row = await query.getSingle();
return Player( return Player(
id: result.id, id: row.id,
name: result.name, name: row.name,
description: result.description, description: row.description,
createdAt: result.createdAt, createdAt: row.createdAt,
nameCount: result.nameCount, nameCount: row.nameCount,
); );
} }
@@ -174,7 +174,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return transaction(() async { return transaction(() async {
final previousPlayer = await (select( final previousPlayer = await (select(
playerTable, playerTable,
)..where((p) => p.id.equals(playerId))).getSingleOrNull(); )..where((tbl) => tbl.id.equals(playerId))).getSingleOrNull();
if (previousPlayer == null) return false; if (previousPlayer == null) return false;
final previousName = previousPlayer.name; final previousName = previousPlayer.name;
@@ -186,7 +186,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
final rowsAffected = final rowsAffected =
await (update( await (update(
playerTable, playerTable,
)..where((p) => p.id.equals(playerId))).write( )..where((tbl) => tbl.id.equals(playerId))).write(
PlayerTableCompanion( PlayerTableCompanion(
name: Value(name), name: Value(name),
nameCount: Value(newNameCount), nameCount: Value(newNameCount),
@@ -203,9 +203,9 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
} else if (remainingCount > 1 && previousCount > 0) { } else if (remainingCount > 1 && previousCount > 0) {
// Shift every player above the gap down by one to keep numbering in order. // Shift every player above the gap down by one to keep numbering in order.
await (update(playerTable)..where( await (update(playerTable)..where(
(p) => (tbl) =>
p.name.equals(previousName) & tbl.name.equals(previousName) &
p.nameCount.isBiggerThanValue(previousCount), tbl.nameCount.isBiggerThanValue(previousCount),
)) ))
.write( .write(
PlayerTableCompanion.custom( PlayerTableCompanion.custom(
@@ -226,9 +226,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String description, required String description,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(playerTable)..where((g) => g.id.equals(playerId))).write( await (update(playerTable)..where((tbl) => tbl.id.equals(playerId)))
PlayerTableCompanion(description: Value(description)), .write(PlayerTableCompanion(description: Value(description)));
);
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -237,7 +236,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Deletes the player with the given [id] from the database. /// Deletes the player with the given [id] from the database.
/// Returns `true` if the player was deleted, `false` if the player did not exist. /// Returns `true` if the player was deleted, `false` if the player did not exist.
Future<bool> deletePlayer({required String playerId}) async { Future<bool> deletePlayer({required String playerId}) async {
final query = delete(playerTable)..where((p) => p.id.equals(playerId)); final query = delete(playerTable)..where((tbl) => tbl.id.equals(playerId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -248,7 +247,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Returns the highest name count if players with the same name exist, /// Returns the highest name count if players with the same name exist,
/// otherwise `null`. /// otherwise `null`.
Future<int> getNameCount({required String name}) async { Future<int> getNameCount({required String name}) async {
final query = select(playerTable)..where((p) => p.name.equals(name)); final query = select(playerTable)..where((tbl) => tbl.name.equals(name));
final result = await query.get(); final result = await query.get();
return result.length; return result.length;
} }
@@ -259,7 +258,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String playerId, required String playerId,
required int nameCount, required int nameCount,
}) async { }) async {
final query = update(playerTable)..where((p) => p.id.equals(playerId)); final query = update(playerTable)..where((tbl) => tbl.id.equals(playerId));
final rowsAffected = await query.write( final rowsAffected = await query.write(
PlayerTableCompanion(nameCount: Value(nameCount)), PlayerTableCompanion(nameCount: Value(nameCount)),
); );
@@ -269,8 +268,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
@visibleForTesting @visibleForTesting
Future<Player?> getPlayerWithHighestNameCount({required String name}) async { Future<Player?> getPlayerWithHighestNameCount({required String name}) async {
final query = select(playerTable) final query = select(playerTable)
..where((p) => p.name.equals(name)) ..where((tbl) => tbl.name.equals(name))
..orderBy([(p) => OrderingTerm.desc(p.nameCount)]) ..orderBy([(tbl) => OrderingTerm.desc(tbl.nameCount)])
..limit(1); ..limit(1);
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
if (result != null) { if (result != null) {
@@ -324,9 +323,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
@visibleForTesting @visibleForTesting
Future<bool> initializeNameCount({required String name}) async { Future<bool> initializeNameCount({required String name}) async {
final rowsAffected = final rowsAffected =
await (update(playerTable)..where((p) => p.name.equals(name))).write( await (update(playerTable)..where((tbl) => tbl.name.equals(name)))
const PlayerTableCompanion(nameCount: Value(1)), .write(const PlayerTableCompanion(nameCount: Value(1)));
);
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -39,18 +39,25 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
/// Retrieves all players belonging to a specific group by [groupId]. /// Retrieves all players belonging to a specific group by [groupId].
Future<List<Player>> getPlayersOfGroup({required String groupId}) async { Future<List<Player>> getPlayersOfGroup({required String groupId}) async {
final query = select(playerGroupTable) final query = select(playerGroupTable).join([
..where((pG) => pG.groupId.equals(groupId)); innerJoin(
final result = await query.get(); playerTable,
playerTable.id.equalsExp(playerGroupTable.playerId),
),
])..where(playerGroupTable.groupId.equals(groupId));
List<Player> groupMembers = List.empty(growable: true); final result = await query.map((row) => row.readTable(playerTable)).get();
return result
for (var entry in result) { .map(
final player = await db.playerDao.getPlayerById(playerId: entry.playerId); (row) => Player(
groupMembers.add(player); id: row.id,
} createdAt: row.createdAt,
name: row.name,
return groupMembers; nameCount: row.nameCount,
description: row.description,
),
)
.toList();
} }
/// Checks if a player with [playerId] is in the group with [groupId]. /// Checks if a player with [playerId] is in the group with [groupId].
@@ -60,7 +67,9 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
required String groupId, required String groupId,
}) async { }) async {
final query = select(playerGroupTable) final query = select(playerGroupTable)
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); ..where(
(tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId),
);
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
return result != null; return result != null;
} }
@@ -81,7 +90,7 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
await db.transaction(() async { await db.transaction(() async {
// Remove all existing players from the group // Remove all existing players from the group
final deleteQuery = delete(db.playerGroupTable) final deleteQuery = delete(db.playerGroupTable)
..where((p) => p.groupId.equals(groupId)); ..where((tbl) => tbl.groupId.equals(groupId));
await deleteQuery.go(); await deleteQuery.go();
// Add new players to the player table if they don't exist // Add new players to the player table if they don't exist
@@ -121,7 +130,9 @@ class PlayerGroupDao extends DatabaseAccessor<AppDatabase>
required String groupId, required String groupId,
}) async { }) async {
final query = delete(playerGroupTable) final query = delete(playerGroupTable)
..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); ..where(
(tbl) => tbl.playerId.equals(playerId) & tbl.groupId.equals(groupId),
);
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -40,7 +40,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
await (selectOnly(playerMatchTable) await (selectOnly(playerMatchTable)
..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.matchId.equals(matchId))
..addColumns([playerMatchTable.playerId.count()])) ..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count())) .map((tbl) => tbl.read(playerMatchTable.playerId.count()))
.getSingle(); .getSingle();
return (count ?? 0) > 0; return (count ?? 0) > 0;
} }
@@ -56,7 +56,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
..where(playerMatchTable.matchId.equals(matchId)) ..where(playerMatchTable.matchId.equals(matchId))
..where(playerMatchTable.playerId.equals(playerId)) ..where(playerMatchTable.playerId.equals(playerId))
..addColumns([playerMatchTable.playerId.count()])) ..addColumns([playerMatchTable.playerId.count()]))
.map((row) => row.read(playerMatchTable.playerId.count())) .map((tbl) => tbl.read(playerMatchTable.playerId.count()))
.getSingle(); .getSingle();
return (count ?? 0) > 0; return (count ?? 0) > 0;
} }
@@ -66,7 +66,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
Future<List<Player>> getPlayersOfMatch({required String matchId}) async { Future<List<Player>> getPlayersOfMatch({required String matchId}) async {
final result = await (select( final result = await (select(
playerMatchTable, playerMatchTable,
)..where((p) => p.matchId.equals(matchId))).get(); )..where((tbl) => tbl.matchId.equals(matchId))).get();
if (result.isEmpty) return []; if (result.isEmpty) return [];
@@ -85,8 +85,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final result = final result =
await (select(playerMatchTable) await (select(playerMatchTable)
..where((p) => p.matchId.equals(matchId)) ..where((tbl) => tbl.matchId.equals(matchId))
..where((p) => p.teamId.equals(teamId))) ..where((tbl) => tbl.teamId.equals(teamId)))
.get(); .get();
if (result.isEmpty) return []; if (result.isEmpty) return [];
@@ -109,7 +109,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(playerMatchTable)..where( await (update(playerMatchTable)..where(
(p) => p.matchId.equals(matchId) & p.playerId.equals(playerId), (tbl) =>
tbl.matchId.equals(matchId) & tbl.playerId.equals(playerId),
)) ))
.write(PlayerMatchTableCompanion(teamId: Value(teamId))); .write(PlayerMatchTableCompanion(teamId: Value(teamId)));
return rowsAffected > 0; return rowsAffected > 0;
@@ -143,9 +144,9 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
// Remove old players // Remove old players
if (playersToRemove.isNotEmpty) { if (playersToRemove.isNotEmpty) {
await (delete(playerMatchTable)..where( await (delete(playerMatchTable)..where(
(pg) => (tbl) =>
pg.matchId.equals(matchId) & tbl.matchId.equals(matchId) &
pg.playerId.isIn(playersToRemove.toList()), tbl.playerId.isIn(playersToRemove.toList()),
)) ))
.go(); .go();
} }
@@ -182,8 +183,8 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
}) async { }) async {
final query = delete(playerMatchTable) final query = delete(playerMatchTable)
..where((pg) => pg.matchId.equals(matchId)) ..where((tbl) => tbl.matchId.equals(matchId))
..where((pg) => pg.playerId.equals(playerId)); ..where((tbl) => tbl.playerId.equals(playerId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -70,10 +70,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where( ..where(
(s) => (tbl) =>
s.playerId.equals(playerId) & tbl.playerId.equals(playerId) &
s.matchId.equals(matchId) & tbl.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber), tbl.roundNumber.equals(roundNumber),
); );
final result = await query.getSingleOrNull(); final result = await query.getSingleOrNull();
@@ -91,7 +91,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String matchId, required String matchId,
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId)); ..where((tbl) => tbl.matchId.equals(matchId));
final result = await query.get(); final result = await query.get();
final Map<String, ScoreEntry?> scoresByPlayer = {}; final Map<String, ScoreEntry?> scoresByPlayer = {};
@@ -113,8 +113,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String matchId, required String matchId,
}) async { }) async {
final query = select(scoreEntryTable) final query = select(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId)) ..where(
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]); (tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
)
..orderBy([(tbl) => OrderingTerm.asc(tbl.roundNumber)]);
final result = await query.get(); final result = await query.get();
return result return result
.map( .map(
@@ -136,8 +138,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
final query = selectOnly(scoreEntryTable) final query = selectOnly(scoreEntryTable)
..where(scoreEntryTable.matchId.equals(matchId)) ..where(scoreEntryTable.matchId.equals(matchId))
..addColumns([scoreEntryTable.roundNumber.max()]); ..addColumns([scoreEntryTable.roundNumber.max()]);
final result = await query.getSingle(); final row = await query.getSingle();
return result.read(scoreEntryTable.roundNumber.max()); return row.read(scoreEntryTable.roundNumber.max());
} }
/// Aggregates the total score for a player in a match by summing all their /// Aggregates the total score for a player in a match by summing all their
@@ -166,10 +168,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(scoreEntryTable)..where( await (update(scoreEntryTable)..where(
(s) => (tbl) =>
s.playerId.equals(playerId) & tbl.playerId.equals(playerId) &
s.matchId.equals(matchId) & tbl.matchId.equals(matchId) &
s.roundNumber.equals(entry.roundNumber), tbl.roundNumber.equals(entry.roundNumber),
)) ))
.write( .write(
ScoreEntryTableCompanion( ScoreEntryTableCompanion(
@@ -190,10 +192,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
final query = delete(scoreEntryTable) final query = delete(scoreEntryTable)
..where( ..where(
(s) => (tbl) =>
s.playerId.equals(playerId) & tbl.playerId.equals(playerId) &
s.matchId.equals(matchId) & tbl.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber), tbl.roundNumber.equals(roundNumber),
); );
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
@@ -201,7 +203,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
Future<bool> deleteAllScoresForMatch({required String matchId}) async { Future<bool> deleteAllScoresForMatch({required String matchId}) async {
final query = delete(scoreEntryTable) final query = delete(scoreEntryTable)
..where((s) => s.matchId.equals(matchId)); ..where((tbl) => tbl.matchId.equals(matchId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }
@@ -211,7 +213,9 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String playerId, required String playerId,
}) async { }) async {
final query = delete(scoreEntryTable) final query = delete(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId)); ..where(
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
);
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -0,0 +1,127 @@
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;
}
}

View File

@@ -0,0 +1,19 @@
// 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,
);
}

View File

@@ -0,0 +1,61 @@
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;
});
}
}

View File

@@ -0,0 +1,29 @@
// 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,
);
}

View File

@@ -0,0 +1,67 @@
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;
});
}
}

View File

@@ -0,0 +1,29 @@
// 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,
);
}

View File

@@ -0,0 +1,55 @@
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;
});
}
}

View File

@@ -0,0 +1,26 @@
// 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,
);
}

View File

@@ -86,7 +86,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
Future<int> getTeamCount() async { Future<int> getTeamCount() async {
final count = final count =
await (selectOnly(teamTable)..addColumns([teamTable.id.count()])) await (selectOnly(teamTable)..addColumns([teamTable.id.count()]))
.map((row) => row.read(teamTable.id.count())) .map((tbl) => tbl.read(teamTable.id.count()))
.getSingle(); .getSingle();
return count ?? 0; return count ?? 0;
} }
@@ -95,8 +95,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Returns `true` if the team exists, `false` otherwise. /// Returns `true` if the team exists, `false` otherwise.
Future<bool> teamExists({required String teamId}) async { Future<bool> teamExists({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
final result = await query.getSingleOrNull(); final row = await query.getSingleOrNull();
return result != null; return row != null;
} }
/// Retrieves all teams from the database. /// Retrieves all teams from the database.
@@ -119,12 +119,12 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Retrieves a [Team] by its [teamId], including its members. /// Retrieves a [Team] by its [teamId], including its members.
Future<Team> getTeamById({required String teamId}) async { Future<Team> getTeamById({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId)); final query = select(teamTable)..where((t) => t.id.equals(teamId));
final result = await query.getSingle(); final row = await query.getSingle();
final members = await _getTeamMembers(teamId: teamId); final members = await _getTeamMembers(teamId: teamId);
return Team( return Team(
id: result.id, id: row.id,
name: result.name, name: row.name,
createdAt: result.createdAt, createdAt: row.createdAt,
members: members, members: members,
); );
} }
@@ -133,13 +133,13 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
Future<List<Player>> _getTeamMembers({required String teamId}) async { Future<List<Player>> _getTeamMembers({required String teamId}) async {
// Get all player_match entries with this teamId // Get all player_match entries with this teamId
final playerMatchQuery = select(db.playerMatchTable) final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.teamId.equals(teamId)); ..where((tbl) => tbl.teamId.equals(teamId));
final playerMatches = await playerMatchQuery.get(); final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return []; if (playerMatches.isEmpty) return [];
// Get unique player IDs // Get unique player IDs
final playerIds = playerMatches.map((pm) => pm.playerId).toSet(); final playerIds = playerMatches.map((tbl) => tbl.playerId).toSet();
// Fetch all players // Fetch all players
final players = await Future.wait( final players = await Future.wait(
@@ -156,7 +156,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
required String name, required String name,
}) async { }) async {
final rowsAffected = final rowsAffected =
await (update(teamTable)..where((t) => t.id.equals(teamId))).write( await (update(teamTable)..where((tbl) => tbl.id.equals(teamId))).write(
TeamTableCompanion(name: Value(name)), TeamTableCompanion(name: Value(name)),
); );
return rowsAffected > 0; return rowsAffected > 0;
@@ -175,7 +175,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Deletes the team with the given [teamId] from the database. /// Deletes the team with the given [teamId] from the database.
/// Returns `true` if the team was deleted, `false` otherwise. /// Returns `true` if the team was deleted, `false` otherwise.
Future<bool> deleteTeam({required String teamId}) async { Future<bool> deleteTeam({required String teamId}) async {
final query = delete(teamTable)..where((t) => t.id.equals(teamId)); final query = delete(teamTable)..where((tbl) => tbl.id.equals(teamId));
final rowsAffected = await query.go(); final rowsAffected = await query.go();
return rowsAffected > 0; return rowsAffected > 0;
} }

View File

@@ -8,6 +8,10 @@ import 'package:tallee/data/dao/player_dao.dart';
import 'package:tallee/data/dao/player_group_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/player_match_dao.dart';
import 'package:tallee/data/dao/score_entry_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/dao/team_dao.dart';
import 'package:tallee/data/db/tables/game_table.dart'; import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart'; import 'package:tallee/data/db/tables/group_table.dart';
@@ -16,6 +20,10 @@ 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_match_table.dart';
import 'package:tallee/data/db/tables/player_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/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'; import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart'; part 'database.g.dart';
@@ -30,6 +38,10 @@ part 'database.g.dart';
GameTable, GameTable,
TeamTable, TeamTable,
ScoreEntryTable, ScoreEntryTable,
StatisticTable,
StatisticScopeTable,
StatisticGameTable,
StatisticGroupTable,
], ],
daos: [ daos: [
PlayerDao, PlayerDao,
@@ -40,6 +52,10 @@ part 'database.g.dart';
GameDao, GameDao,
ScoreEntryDao, ScoreEntryDao,
TeamDao, TeamDao,
StatisticDao,
StatisticScopeDao,
StatisticGameDao,
StatisticGroupDao,
], ],
) )
class AppDatabase extends _$AppDatabase { class AppDatabase extends _$AppDatabase {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
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};
}

View File

@@ -0,0 +1,13 @@
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};
}

View File

@@ -0,0 +1,11 @@
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};
}

View File

@@ -0,0 +1,11 @@
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};
}

View File

@@ -8,13 +8,13 @@ class Game {
final String name; final String name;
final Ruleset ruleset; final Ruleset ruleset;
final String description; final String description;
final GameColor color; final AppColor color;
final String icon; final String icon;
Game({ Game({
required this.name, required this.name,
required this.ruleset, required this.ruleset,
this.color = GameColor.orange, this.color = AppColor.orange,
this.description = '', this.description = '',
this.icon = '', this.icon = '',
String? id, String? id,
@@ -33,7 +33,7 @@ class Game {
String? name, String? name,
Ruleset? ruleset, Ruleset? ruleset,
String? description, String? description,
GameColor? color, AppColor? color,
String? icon, String? icon,
}) { }) {
return Game( return Game(
@@ -73,7 +73,7 @@ class Game {
orElse: () => Ruleset.singleWinner, orElse: () => Ruleset.singleWinner,
), ),
description = json['description'], description = json['description'],
color = GameColor.values.firstWhere((e) => e.name == json['color']), color = AppColor.values.firstWhere((e) => e.name == json['color']),
icon = json['icon']; icon = json['icon'];
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {

View File

@@ -5,17 +5,17 @@ import 'package:uuid/uuid.dart';
class Group { class Group {
final String id; final String id;
final String name;
final String description;
final DateTime createdAt; final DateTime createdAt;
final String name;
final List<Player> members; final List<Player> members;
final String description;
Group({ Group({
required this.name,
required this.members,
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
required this.name,
String? description, String? description,
required this.members,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now(), createdAt = createdAt ?? clock.now(),
description = description ?? ''; description = description ?? '';

View File

@@ -107,7 +107,7 @@ class Match {
name: '', name: '',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
), ),
group = null, group = null,

View File

@@ -0,0 +1,48 @@
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,
);
}
}

View File

@@ -2,14 +2,18 @@
"@@locale": "de", "@@locale": "de",
"all_players": "Alle Spieler:innen", "all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"all_time": "Gesamter Zeitraum",
"amount_of_matches": "Anzahl der Spiele", "amount_of_matches": "Anzahl der Spiele",
"app_name": "Tallee", "app_name": "Tallee",
"average_score": "Durchschnittliche Punktzahl",
"best_player": "Beste:r Spieler:in", "best_player": "Beste:r Spieler:in",
"best_score": "Beste Punktzahl",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_color": "Farbe wählen", "choose_color": "Farbe wählen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen", "choose_ruleset": "Regelwerk wählen",
"classifier": "Klassifikator",
"color": "Farbe", "color": "Farbe",
"color_blue": "Blau", "color_blue": "Blau",
"color_green": "Grün", "color_green": "Grün",
@@ -21,11 +25,29 @@
"color_yellow": "Gelb", "color_yellow": "Gelb",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "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_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel 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",
"created_on": "Erstellt am", "created_on": "Erstellt am",
"data": "Daten", "data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_deleted": "Daten erfolgreich gelöscht",
@@ -47,6 +69,7 @@
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"delete_player": "Spieler:in löschen", "delete_player": "Spieler:in löschen",
"description": "Beschreibung", "description": "Beschreibung",
"displayed_entries": "Angezeigte Einträge",
"drag_to_set_placement": "Ziehen um Platzierung zu setzen", "drag_to_set_placement": "Ziehen um Platzierung zu setzen",
"edit_game": "Spielvorlage bearbeiten", "edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
@@ -63,9 +86,11 @@
"exit_view": "Ansicht verlassen", "exit_view": "Ansicht verlassen",
"export_canceled": "Export abgebrochen", "export_canceled": "Export abgebrochen",
"export_data": "Daten exportieren", "export_data": "Daten exportieren",
"filter": "Filter",
"format_exception": "Formatfehler (siehe Konsole)", "format_exception": "Formatfehler (siehe Konsole)",
"game": "Spielvorlage", "game": "Spielvorlage",
"game_name": "Spielvorlagenname", "game_name": "Spielvorlagenname",
"games": "Spielvorlagen",
"group": "Gruppe", "group": "Gruppe",
"group_name": "Gruppenname", "group_name": "Gruppenname",
"group_profile": "Gruppenprofil", "group_profile": "Gruppenprofil",
@@ -77,11 +102,17 @@
"import_data": "Daten importieren", "import_data": "Daten importieren",
"info": "Info", "info": "Info",
"invalid_schema": "Ungültiges Schema", "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", "least_points": "Niedrigste Punkte",
"legal": "Rechtliches", "legal": "Rechtliches",
"legal_notice": "Impressum", "legal_notice": "Impressum",
"licenses": "Lizenzen", "licenses": "Lizenzen",
"live_edit_mode": "Live-Bearbeitungsmodus", "live_edit_mode": "Live-Bearbeitungsmodus",
"loading": "Lädt...",
"loser": "Verlierer:in", "loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte", "lowest_score": "Niedrigste Punkte",
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
@@ -108,6 +139,7 @@
"no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_results_entered_yet": "Noch keine Ergebnisse eingetragen",
"no_second_match_available": "Kein zweites Spiel verfügbar", "no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar", "no_statistics_available": "Keine Statistiken verfügbar",
"no_statistics_created_yet": "Noch keine Statistiken erstellt",
"none": "Kein", "none": "Kein",
"none_group": "Keine", "none_group": "Keine",
"not_available": "Nicht verfügbar", "not_available": "Nicht verfügbar",
@@ -132,16 +164,36 @@
"ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "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.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.",
"save_changes": "Änderungen speichern", "save_changes": "Änderungen speichern",
"scope": "Bereich",
"search_for_groups": "Nach Gruppen suchen", "search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen 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_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_winner": "Gewinner:in wählen",
"select_winners": "Gewinner:innen wählen", "select_winners": "Gewinner:innen wählen",
"selected_games": "Ausgewählte Spielvorlagen",
"selected_groups": "Ausgewählte Gruppen",
"selected_players": "Ausgewählte Spieler:innen", "selected_players": "Ausgewählte Spieler:innen",
"set_name": "Name setzen", "set_name": "Name setzen",
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner: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", "statistics": "Statistiken",
"stats": "Statistiken", "stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
@@ -149,12 +201,18 @@
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, 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.", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
"tie": "Unentschieden", "tie": "Unentschieden",
"timeframe": "Zeitraum",
"today_at": "Heute um", "today_at": "Heute um",
"total_losses": "Niederlagen insgesamt",
"total_matches": "Spiele insgesamt",
"total_score": "Punktzahl insgesamt",
"total_wins": "Siege insgesamt",
"undo": "Rückgängig", "undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)", "unknown_exception": "Unbekannter Fehler (siehe Konsole)",
"winner": "Gewinner:in", "winner": "Gewinner:in",
"winners": "Gewinner:innen", "winners": "Gewinner:innen",
"winrate": "Siegquote", "winrate": "Siegquote",
"wins": "Siege", "wins": "Siege",
"worst_score": "Schlechteste Punktzahl",
"yesterday_at": "Gestern um" "yesterday_at": "Gestern um"
} }

View File

@@ -18,14 +18,30 @@
"color_purple": "Purple", "color_purple": "Purple",
"color_red": "Red", "color_red": "Red",
"color_teal": "Teal", "color_teal": "Teal",
"displayed_entries": "Displayed entries",
"color_yellow": "Yellow", "color_yellow": "Yellow",
"confirm": "Confirm", "confirm": "Confirm",
"could_not_add_player": "Could not add player", "could_not_add_player": "Could not add player {playerName}",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Create Game", "create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
"create_new_match": "Create new match", "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",
"created_on": "Created on", "created_on": "Created on",
"data": "Data", "data": "Data",
"data_successfully_deleted": "Data successfully deleted", "data_successfully_deleted": "Data successfully deleted",
@@ -43,6 +59,7 @@
} }
} }
}, },
"filter": "Filter",
"delete_group": "Delete Group", "delete_group": "Delete Group",
"delete_match": "Delete Match", "delete_match": "Delete Match",
"delete_player": "Delete player?", "delete_player": "Delete player?",
@@ -82,6 +99,7 @@
"legal_notice": "Legal Notice", "legal_notice": "Legal Notice",
"licenses": "Licenses", "licenses": "Licenses",
"live_edit_mode": "Live Edit Mode", "live_edit_mode": "Live Edit Mode",
"loading": "Loading...",
"loser": "Loser", "loser": "Loser",
"lowest_score": "Lowest Score", "lowest_score": "Lowest Score",
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
@@ -108,6 +126,7 @@
"no_results_entered_yet": "No results entered yet", "no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available", "no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available", "no_statistics_available": "No statistics available",
"no_statistics_created_yet": "No statistics created yet",
"none": "None", "none": "None",
"none_group": "None", "none_group": "None",
"not_available": "Not available", "not_available": "Not available",
@@ -139,10 +158,24 @@
"selected_players": "Selected players", "selected_players": "Selected players",
"set_name": "Set name", "set_name": "Set name",
"settings": "Settings", "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_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",
"statistics": "Statistics", "statistics": "Statistics",
"stats": "Stats", "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": "Successfully added player {playerName}",
"@successfully_added_player": { "@successfully_added_player": {
"description": "Success message when adding a player", "description": "Success message when adding a player",
@@ -157,6 +190,12 @@
"there_is_no_group_matching_your_search": "There is no group 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.", "this_cannot_be_undone": "This can't be undone.",
"tie": "Tie", "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", "today_at": "Today at",
"undo": "Undo", "undo": "Undo",
"unknown_exception": "Unknown Exception (see console)", "unknown_exception": "Unknown Exception (see console)",

View File

@@ -206,6 +206,12 @@ abstract class AppLocalizations {
/// **'Teal'** /// **'Teal'**
String get color_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. /// No description provided for @color_yellow.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -221,8 +227,8 @@ abstract class AppLocalizations {
/// No description provided for @could_not_add_player. /// No description provided for @could_not_add_player.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Could not add player'** /// **'Could not add player {playerName}'**
String could_not_add_player(Object playerName); String could_not_add_player(String playerName);
/// No description provided for @create_game. /// No description provided for @create_game.
/// ///
@@ -254,6 +260,54 @@ abstract class AppLocalizations {
/// **'Create new match'** /// **'Create new match'**
String get create_new_match; String get create_new_match;
/// No description provided for @create_statistic.
///
/// 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;
/// No description provided for @created_on. /// No description provided for @created_on.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -314,6 +368,12 @@ 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.'** /// **'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); 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. /// No description provided for @delete_group.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -548,6 +608,12 @@ abstract class AppLocalizations {
/// **'Live Edit Mode'** /// **'Live Edit Mode'**
String get 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. /// No description provided for @loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -704,6 +770,12 @@ abstract class AppLocalizations {
/// **'No statistics available'** /// **'No statistics available'**
String get no_statistics_available; String get no_statistics_available;
/// No description provided for @no_statistics_created_yet.
///
/// In en, this message translates to:
/// **'No statistics created yet'**
String get no_statistics_created_yet;
/// No description provided for @none. /// No description provided for @none.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -890,6 +962,36 @@ abstract class AppLocalizations {
/// **'Settings'** /// **'Settings'**
String get 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. /// No description provided for @single_loser.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -914,6 +1016,60 @@ abstract class AppLocalizations {
/// **'Stats'** /// **'Stats'**
String get 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 /// Success message when adding a player
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -944,6 +1100,42 @@ abstract class AppLocalizations {
/// **'Tie'** /// **'Tie'**
String get 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. /// No description provided for @today_at.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -62,6 +62,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get color_teal => 'Türkis'; String get color_teal => 'Türkis';
@override
String get displayed_entries => 'Angezeigte Einträge';
@override @override
String get color_yellow => 'Gelb'; String get color_yellow => 'Gelb';
@@ -69,7 +72,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get confirm => 'Bestätigen'; String get confirm => 'Bestätigen';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(String playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@@ -88,6 +91,33 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get create_new_match => 'Neues Spiel erstellen'; 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';
@override @override
String get created_on => 'Erstellt am'; String get created_on => 'Erstellt am';
@@ -128,6 +158,9 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
} }
@override
String get filter => 'Filter';
@override @override
String get delete_group => 'Gruppe löschen'; String get delete_group => 'Gruppe löschen';
@@ -249,6 +282,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get live_edit_mode => 'Live-Bearbeitungsmodus'; String get live_edit_mode => 'Live-Bearbeitungsmodus';
@override
String get loading => 'Lädt...';
@override @override
String get loser => 'Verlierer:in'; String get loser => 'Verlierer:in';
@@ -328,6 +364,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get no_statistics_available => 'Keine Statistiken verfügbar'; String get no_statistics_available => 'Keine Statistiken verfügbar';
@override
String get no_statistics_created_yet => 'Noch keine Statistiken erstellt';
@override @override
String get none => 'Kein'; String get none => 'Kein';
@@ -426,6 +465,21 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settings => 'Einstellungen'; 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 @override
String get single_loser => 'Ein:e Verlierer:in'; String get single_loser => 'Ein:e Verlierer:in';
@@ -438,6 +492,33 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get stats => 'Statistiken'; 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 @override
String successfully_added_player(String playerName) { String successfully_added_player(String playerName) {
return 'Spieler:in $playerName erfolgreich hinzugefügt'; return 'Spieler:in $playerName erfolgreich hinzugefügt';
@@ -458,6 +539,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get tie => 'Unentschieden'; 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 @override
String get today_at => 'Heute um'; String get today_at => 'Heute um';

View File

@@ -62,6 +62,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get color_teal => 'Teal'; String get color_teal => 'Teal';
@override
String get displayed_entries => 'Displayed entries';
@override @override
String get color_yellow => 'Yellow'; String get color_yellow => 'Yellow';
@@ -69,8 +72,8 @@ class AppLocalizationsEn extends AppLocalizations {
String get confirm => 'Confirm'; String get confirm => 'Confirm';
@override @override
String could_not_add_player(Object playerName) { String could_not_add_player(String playerName) {
return 'Could not add player'; return 'Could not add player $playerName';
} }
@override @override
@@ -88,6 +91,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get create_new_match => 'Create new match'; 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';
@override @override
String get created_on => 'Created on'; String get created_on => 'Created on';
@@ -128,6 +158,9 @@ class AppLocalizationsEn extends AppLocalizations {
return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
} }
@override
String get filter => 'Filter';
@override @override
String get delete_group => 'Delete Group'; String get delete_group => 'Delete Group';
@@ -249,6 +282,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get live_edit_mode => 'Live Edit Mode'; String get live_edit_mode => 'Live Edit Mode';
@override
String get loading => 'Loading...';
@override @override
String get loser => 'Loser'; String get loser => 'Loser';
@@ -328,6 +364,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get no_statistics_available => 'No statistics available'; String get no_statistics_available => 'No statistics available';
@override
String get no_statistics_created_yet => 'No statistics created yet';
@override @override
String get none => 'None'; String get none => 'None';
@@ -426,6 +465,21 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings => 'Settings'; 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 @override
String get single_loser => 'Single Loser'; String get single_loser => 'Single Loser';
@@ -438,6 +492,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get stats => 'Stats'; 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 @override
String successfully_added_player(String playerName) { String successfully_added_player(String playerName) {
return 'Successfully added player $playerName'; return 'Successfully added player $playerName';
@@ -457,6 +538,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get tie => 'Tie'; 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 @override
String get today_at => 'Today at'; String get today_at => 'Today at';

View File

@@ -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/group_view/group_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_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/settings_view/settings_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/statistics_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/navbar_item.dart'; import 'package:tallee/presentation/widgets/navbar_item.dart';

View File

@@ -164,7 +164,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
game.ruleset, game.ruleset,
context, context,
), ),
badgeColor: getColorFromGameColor(game.color), badgeColor: getColorFromAppColor(game.color),
isHighlighted: selectedGameId == game.id, isHighlighted: selectedGameId == game.id,
onTap: () async { onTap: () async {
setState(() { setState(() {

View File

@@ -49,10 +49,10 @@ class _CreateGameViewState extends State<CreateGameView> {
late final AppDatabase db; late final AppDatabase db;
late List<(Ruleset, String)> _rulesets; late List<(Ruleset, String)> _rulesets;
late List<(GameColor, String)> _colors; late List<(AppColor, String)> _colors;
Ruleset? selectedRuleset = Ruleset.singleWinner; Ruleset? selectedRuleset = Ruleset.singleWinner;
GameColor? selectedColor = GameColor.orange; AppColor? selectedColor = AppColor.orange;
/// Controller for the game name input field. /// Controller for the game name input field.
final _gameNameController = TextEditingController(); final _gameNameController = TextEditingController();
@@ -87,10 +87,10 @@ class _CreateGameViewState extends State<CreateGameView> {
), ),
); );
_colors = List.generate( _colors = List.generate(
GameColor.values.length, AppColor.values.length,
(index) => ( (index) => (
GameColor.values[index], AppColor.values[index],
translateGameColorToString(GameColor.values[index], context), translateAppColorToString(AppColor.values[index], context),
), ),
); );
@@ -117,7 +117,6 @@ class _CreateGameViewState extends State<CreateGameView> {
return ScaffoldMessenger( return ScaffoldMessenger(
child: Scaffold( child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
title: Text(isEditing ? loc.edit_game : loc.create_game), title: Text(isEditing ? loc.edit_game : loc.create_game),
actions: [ actions: [
@@ -468,7 +467,7 @@ class _CreateGameViewState extends State<CreateGameView> {
height: 16, height: 16,
margin: const EdgeInsets.only(left: 12), margin: const EdgeInsets.only(left: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: getColorFromGameColor( color: getColorFromAppColor(
_colors[index].$1, _colors[index].$1,
), ),
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -502,13 +501,13 @@ class _CreateGameViewState extends State<CreateGameView> {
width: 16, width: 16,
height: 16, height: 16,
decoration: BoxDecoration( decoration: BoxDecoration(
color: getColorFromGameColor(selectedColor!), color: getColorFromAppColor(selectedColor!),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(right: 5), padding: const EdgeInsets.only(right: 5),
child: Text(translateGameColorToString(selectedColor!, context)), child: Text(translateAppColorToString(selectedColor!, context)),
), ),
Transform.rotate( Transform.rotate(
angle: pi / 2, angle: pi / 2,

View File

@@ -39,7 +39,7 @@ class _MatchViewState extends State<MatchView> {
game: Game( game: Game(
name: 'Game name', name: 'Game name',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
), ),
group: Group( group: Group(
@@ -79,7 +79,7 @@ class _MatchViewState extends State<MatchView> {
visible: matches.isNotEmpty, visible: matches.isNotEmpty,
replacement: Center( replacement: Center(
child: TopCenteredMessage( child: TopCenteredMessage(
icon: Icons.report, icon: Icons.info,
title: loc.info, title: loc.info,
message: loc.no_matches_created_yet, message: loc.no_matches_created_yet,
), ),

View File

@@ -34,7 +34,6 @@ const allDependencies = <Package>[
_cli_util, _cli_util,
_clock, _clock,
_code_assets, _code_assets,
_code_builder,
_collection, _collection,
_convert, _convert,
_coverage, _coverage,
@@ -154,6 +153,7 @@ const allDependencies = <Package>[
_source_map_stack_trace, _source_map_stack_trace,
_source_maps, _source_maps,
_source_span, _source_span,
_sqlcipher_flutter_libs,
_sqlite3, _sqlite3,
_sqlite3_flutter_libs, _sqlite3_flutter_libs,
_sqlparser, _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.''', OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
); );
/// build_runner 2.13.1 /// build_runner 2.15.0
const _build_runner = Package( const _build_runner = Package(
name: 'build_runner', name: 'build_runner',
description: 'A build system for Dart code generation and modular compilation.', description: 'A build system for Dart code generation and modular compilation.',
repository: 'https://github.com/dart-lang/build/tree/master/build_runner', repository: 'https://github.com/dart-lang/build/tree/master/build_runner',
authors: [], authors: [],
version: '2.13.1', version: '2.15.0',
spdxIdentifiers: ['BSD-3-Clause'], spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false, isMarkdown: false,
isSdk: 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('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')], 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')],
devDependencies: [PackageRef('stream_channel'), PackageRef('test')], devDependencies: [PackageRef('stream_channel'), PackageRef('test')],
license: '''Copyright 2016, the Dart project authors. license: '''Copyright 2016, the Dart project authors.
@@ -1510,47 +1510,6 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', 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 /// collection 1.19.1
const _collection = Package( const _collection = Package(
name: 'collection', name: 'collection',
@@ -2581,14 +2540,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// drift 2.31.0 /// drift 2.33.0
const _drift = Package( const _drift = Package(
name: 'drift', name: 'drift',
description: 'Drift is a reactive library to store relational data in Dart and Flutter applications.', description: 'Drift is a reactive library to store relational data in Dart and Flutter applications.',
homepage: 'https://drift.simonbinder.eu/', homepage: 'https://drift.simonbinder.eu/',
repository: 'https://github.com/simolus3/drift', repository: 'https://github.com/simolus3/drift',
authors: [], authors: [],
version: '2.31.0', version: '2.33.0',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
@@ -2617,14 +2576,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// drift_dev 2.31.0 /// drift_dev 2.33.0
const _drift_dev = Package( const _drift_dev = Package(
name: 'drift_dev', name: 'drift_dev',
description: 'Dev-dependency for users of drift. Contains the generator and development tools.', description: 'Dev-dependency for users of drift. Contains the generator and development tools.',
homepage: 'https://drift.simonbinder.eu/', homepage: 'https://drift.simonbinder.eu/',
repository: 'https://github.com/simolus3/drift', repository: 'https://github.com/simolus3/drift',
authors: [], authors: [],
version: '2.31.0', version: '2.33.0',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
@@ -2653,18 +2612,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// drift_flutter 0.2.8 /// drift_flutter 0.3.0
const _drift_flutter = Package( const _drift_flutter = Package(
name: 'drift_flutter', name: 'drift_flutter',
description: 'Easily set up drift databases across platforms in Flutter apps.', description: 'Easily set up drift databases across platforms in Flutter apps.',
homepage: 'https://drift.simonbinder.eu/', homepage: 'https://drift.simonbinder.eu/',
repository: 'https://github.com/simolus3/drift', repository: 'https://github.com/simolus3/drift',
authors: [], authors: [],
version: '0.2.8', version: '0.3.0',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
dependencies: [PackageRef('drift'), PackageRef('flutter'), PackageRef('meta'), PackageRef('path'), PackageRef('path_provider'), PackageRef('sqlite3'), PackageRef('sqlite3_flutter_libs')], dependencies: [PackageRef('drift'), PackageRef('flutter'), PackageRef('meta'), PackageRef('path'), PackageRef('path_provider'), PackageRef('sqlite3'), PackageRef('sqlite3_flutter_libs'), PackageRef('sqlcipher_flutter_libs')],
devDependencies: [PackageRef('build_runner'), PackageRef('drift_dev'), PackageRef('lints'), PackageRef('test'), PackageRef('flutter_test'), PackageRef('async')], devDependencies: [PackageRef('build_runner'), PackageRef('drift_dev'), PackageRef('lints'), PackageRef('test'), PackageRef('flutter_test'), PackageRef('async')],
license: '''MIT License license: '''MIT License
@@ -3057,18 +3016,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// file_saver 0.3.1 /// file_saver 0.4.0
const _file_saver = Package( const _file_saver = Package(
name: 'file_saver', name: 'file_saver',
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.', description: 'Save files from bytes, paths, streams, and URLs across Android, iOS, Web, Windows, macOS, and Linux.',
homepage: 'https://hassanansari.dev', homepage: 'https://hassanansari.dev',
repository: 'https://github.com/incrediblezayed/file_saver', repository: 'https://github.com/incrediblezayed/file_saver',
authors: [], authors: [],
version: '0.3.1', version: '0.4.0',
spdxIdentifiers: ['BSD-3-Clause'], spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
dependencies: [PackageRef('collection'), PackageRef('dio'), PackageRef('flutter'), PackageRef('flutter_web_plugins'), PackageRef('path_provider'), PackageRef('path_provider_linux'), PackageRef('path_provider_windows'), PackageRef('web')], dependencies: [PackageRef('collection'), PackageRef('dio'), PackageRef('flutter'), PackageRef('flutter_web_plugins'), PackageRef('path_provider'), PackageRef('web')],
devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')], devDependencies: [PackageRef('flutter_lints'), PackageRef('flutter_test')],
license: '''BSD 3-Clause License license: '''BSD 3-Clause License
@@ -37683,18 +37642,264 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''', OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
); );
/// sqlite3 2.9.4 /// 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
const _sqlite3 = Package( const _sqlite3 = Package(
name: 'sqlite3', name: 'sqlite3',
description: 'Provides lightweight yet convenient bindings to SQLite by using dart:ffi', description: 'Provides lightweight yet convenient bindings to SQLite by using dart:ffi',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3', homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3',
authors: [], authors: [],
version: '2.9.4', version: '3.3.1',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
dependencies: [PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('path'), PackageRef('web'), PackageRef('typed_data')], 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')], 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')],
license: '''MIT License license: '''MIT License
Copyright (c) 2020 Simon Binder Copyright (c) 2020 Simon Binder
@@ -37718,17 +37923,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// sqlite3_flutter_libs 0.5.42 /// sqlite3_flutter_libs 0.6.0+eol
const _sqlite3_flutter_libs = Package( const _sqlite3_flutter_libs = Package(
name: 'sqlite3_flutter_libs', name: 'sqlite3_flutter_libs',
description: 'Flutter plugin to include native sqlite3 libraries with your app', description: 'Not used anymore, update to version 3.x of package:sqlite3 instead',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/v2/sqlite3_flutter_libs', homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/legacy/sqlite3_flutter_libs',
authors: [], authors: [],
version: '0.5.42', version: '0.6.0+eol',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
dependencies: [PackageRef('flutter')], dependencies: [],
devDependencies: [], devDependencies: [],
license: '''MIT License license: '''MIT License
@@ -37753,14 +37958,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// sqlparser 0.43.1 /// sqlparser 0.44.4
const _sqlparser = Package( const _sqlparser = Package(
name: 'sqlparser', name: 'sqlparser',
description: 'Parses sqlite statements and performs static analysis on them', description: 'Parses sqlite statements and performs static analysis on them',
homepage: 'https://github.com/simolus3/drift/tree/develop/sqlparser', homepage: 'https://github.com/simolus3/drift/tree/develop/sqlparser',
repository: 'https://github.com/simolus3/drift', repository: 'https://github.com/simolus3/drift',
authors: [], authors: [],
version: '0.43.1', version: '0.44.4',
spdxIdentifiers: ['MIT'], spdxIdentifiers: ['MIT'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,
@@ -39415,12 +39620,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''', SOFTWARE.''',
); );
/// tallee 0.0.33+273 /// tallee 0.0.35+277
const _tallee = Package( const _tallee = Package(
name: 'tallee', name: 'tallee',
description: 'Tracking App for Card Games', description: 'Tracking App for Card Games',
authors: [], authors: [],
version: '0.0.33+273', version: '0.0.35+277',
spdxIdentifiers: ['LGPL-3.0'], spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false, isMarkdown: false,
isSdk: false, isSdk: false,

View File

@@ -1,311 +0,0 @@
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;
}
}

View File

@@ -0,0 +1,635 @@
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;
}
}

View File

@@ -0,0 +1,191 @@
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);
}
}

View File

@@ -0,0 +1,322 @@
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,
);
}

View File

@@ -0,0 +1,190 @@
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,
),
);
}
}

View File

@@ -12,41 +12,44 @@ class GameLabel extends StatelessWidget {
final String title; final String title;
final String description; final String description;
final GameColor color; final AppColor color;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final backgroundColor = getColorFromGameColor(color); final backgroundColor = getColorFromAppColor(color);
final fontColor = backgroundColor.computeLuminance() > 0.5 final fontColor = backgroundColor.computeLuminance() > 0.5
? Colors.black ? Colors.black
: Colors.white; : Colors.white;
return IntrinsicHeight( return Row(
child: Row( mainAxisSize: MainAxisSize.min,
mainAxisSize: MainAxisSize.min, children: [
children: [ // Title
// Title Container(
Container( decoration: BoxDecoration(
decoration: BoxDecoration( color: backgroundColor.withAlpha(230),
color: backgroundColor.withAlpha(230), borderRadius: const BorderRadius.only(
borderRadius: const BorderRadius.only( topLeft: Radius.circular(8),
topLeft: Radius.circular(8), bottomLeft: 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 // Description
Container( Flexible(
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: backgroundColor.withAlpha(140), color: backgroundColor.withAlpha(140),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -57,6 +60,9 @@ class GameLabel extends StatelessWidget {
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Text( child: Text(
description, description,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: fontColor, color: fontColor,
@@ -64,8 +70,8 @@ class GameLabel extends StatelessWidget {
), ),
), ),
), ),
], ),
), ],
); );
} }
} }

View File

@@ -51,7 +51,7 @@ class GameTile extends StatelessWidget {
? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white) ? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white)
: Colors.white; : Colors.white;
final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange); final gameColor = badgeColor ?? getColorFromAppColor(AppColor.orange);
return GestureDetector( return GestureDetector(
onTap: () async { onTap: () async {

View File

@@ -1,8 +1,12 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:tallee/core/common.dart'; import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
@@ -21,8 +25,11 @@ class StatisticsTile extends StatelessWidget {
required this.title, required this.title,
required this.width, required this.width,
required this.values, required this.values,
required this.itemCount,
required this.barColor, required this.barColor,
required this.displayCount,
this.selectedGroups,
this.selectedGames,
this.showAllValues = false,
}); });
/// The icon displayed next to the title. /// The icon displayed next to the title.
@@ -37,12 +44,16 @@ class StatisticsTile extends StatelessWidget {
/// A list of tuples containing labels and their corresponding numeric values. /// A list of tuples containing labels and their corresponding numeric values.
final List<(Player, num)> values; final List<(Player, num)> values;
/// The maximum number of items to display.
final int itemCount;
/// The color of the bars representing the values. /// The color of the bars representing the values.
final Color barColor; final Color barColor;
// statistic data
final int displayCount;
final List<Group>? selectedGroups;
final List<Game>? selectedGames;
final bool showAllValues;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -52,91 +63,202 @@ class StatisticsTile extends StatelessWidget {
title: title, title: title,
icon: icon, icon: icon,
content: Padding( content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Visibility( child: Visibility(
visible: values.isNotEmpty, visible: values.isNotEmpty,
// No data avaiable message
replacement: Center( replacement: Center(
heightFactor: 4, heightFactor: 4,
child: Text(loc.no_data_available), child: Text(loc.no_data_available),
), ),
// Bar chart
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final maxBarWidth = constraints.maxWidth * 0.65; 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;
return Column( return Column(
children: List.generate(min(values.length, itemCount), (index) { children: [
/// The maximum wins among all players // Bars
final maxMatches = values.isNotEmpty ? values[0].$2 : 0; ...List.generate(valuesShown, (index) {
/// Fraction of wins
final double fraction = (maxVal > 0)
? (displayValues[index].$2 / maxVal)
: 0.0;
/// Fraction of wins /// Calculated width for current the bar
final double fraction = (maxMatches > 0) final double barWidth = (maxBarWidth * fraction).clamp(
? (values[index].$2 / maxMatches) 0.0,
: 0.0; maxBarWidth,
);
/// Calculated width for current the bar final barClr = index >= displayCount
final double barWidth = maxBarWidth * fraction; ? barColor.withAlpha(150)
: barColor;
return Padding( var textClr = barColor.computeLuminance() > 0.5
padding: const EdgeInsets.symmetric(vertical: 2.0), ? const Color(0xFF101010)
child: Row( : CustomTheme.textColor;
mainAxisAlignment: MainAxisAlignment.start, textClr = textClr.withAlpha(
children: [ index >= displayCount ? 220 : 255,
Stack( );
children: [
Container( return Padding(
height: 24, padding: const EdgeInsets.symmetric(vertical: 2.0),
width: barWidth, child: Row(
decoration: BoxDecoration( mainAxisAlignment: MainAxisAlignment.start,
borderRadius: BorderRadius.circular(4), children: [
color: barColor, SizedBox(
), width: maxBarWidth,
), child: Stack(
Padding( clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.only(left: 4.0), children: [
child: RichText( // Bar
overflow: TextOverflow.ellipsis, Container(
text: TextSpan( height: 24,
style: DefaultTextStyle.of(context).style, width: barWidth,
children: [ decoration: BoxDecoration(
TextSpan( borderRadius: BorderRadius.circular(4),
text: values[index].$1.name, color: barClr,
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,
),
),
),
],
), ),
),
), // Player
], Padding(
), padding: const EdgeInsets.only(left: 4.0),
const Spacer(), child: RichText(
Center( maxLines: 1,
child: Text( softWrap: false,
values[index].$2 <= 1 && values[index].$2 is double overflow: TextOverflow.ellipsis,
? values[index].$2.toStringAsFixed(2) text: TextSpan(
: values[index].$2.toString(), style: DefaultTextStyle.of(context).style,
textAlign: TextAlign.center, children: [
style: const TextStyle( TextSpan(
fontSize: 16, text: displayValues[index].$1.name,
fontWeight: FontWeight.bold, 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,
),
),
),
],
),
);
}),
// 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,
),
],
),
],
),
), ),
); ],
}),
); );
}, },
), ),
@@ -144,4 +266,24 @@ 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;
} }

View File

@@ -20,6 +20,7 @@ class DataTransferService {
static Future<void> deleteAllData(BuildContext context) async { static Future<void> deleteAllData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
await db.statisticDao.deleteAllStatistics();
await db.matchDao.deleteAllMatches(); await db.matchDao.deleteAllMatches();
await db.teamDao.deleteAllTeams(); await db.teamDao.deleteAllTeams();
await db.groupDao.deleteAllGroups(); await db.groupDao.deleteAllGroups();
@@ -278,7 +279,7 @@ class DataTransferService {
name: 'Unknown', name: 'Unknown',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: '', description: '',
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
); );
} }

View File

@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.1" 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: arb_utils:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -353,6 +361,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.2.8" 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: equatable:
dependency: transitive dependency: transitive
description: description:

View File

@@ -1,19 +1,21 @@
name: tallee name: tallee
description: "Tracking App for Card Games" description: "Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.33+273 version: 0.0.33+281
environment: environment:
sdk: ^3.8.1 sdk: ^3.12.0
dependencies: dependencies:
animated_custom_dropdown: ^3.1.1
clock: ^1.1.2 clock: ^1.1.2
collection: ^1.19.1 collection: ^1.19.1
cupertino_icons: ^1.0.6 dropdown_flutter: ^1.0.3
drift: ^2.27.0 cupertino_icons: ^1.0.9
drift_flutter: ^0.2.4 drift: ^2.33.0
drift_flutter: ^0.3.0
file_picker: ^11.0.2 file_picker: ^11.0.2
file_saver: ^0.3.1 file_saver: ^0.4.0
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
@@ -24,20 +26,20 @@ dependencies:
font_awesome_flutter: ^11.0.0 font_awesome_flutter: ^11.0.0
intl: any intl: any
json_schema: ^5.2.2 json_schema: ^5.2.2
package_info_plus: ^9.0.0 package_info_plus: ^9.0.1
path_provider: ^2.1.5 path_provider: ^2.1.5
provider: ^6.1.5 provider: ^6.1.5
skeletonizer: ^2.1.0+1 skeletonizer: ^2.1.3
url_launcher: ^6.3.2 url_launcher: ^6.3.2
uuid: ^4.5.2 uuid: ^4.5.3
dev_dependencies: dev_dependencies:
arb_utils: ^0.11.0 arb_utils: ^0.11.0
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.7.0 build_runner: ^2.15.0
dart_pubspec_licenses: ^3.0.14 dart_pubspec_licenses: ^3.2.0
drift_dev: ^2.27.0 drift_dev: ^2.33.0
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
flutter: flutter:

View File

@@ -56,7 +56,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -49,7 +49,7 @@ void main() {
testGame = Game( testGame = Game(
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -28,7 +28,7 @@ void main() {
name: 'Chess', name: 'Chess',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A classic strategy game', description: 'A classic strategy game',
color: GameColor.blue, color: AppColor.blue,
icon: 'chess_icon', icon: 'chess_icon',
); );
testGame2 = Game( testGame2 = Game(
@@ -36,7 +36,7 @@ void main() {
name: 'Poker', name: 'Poker',
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Card game with multiple winners', description: 'Card game with multiple winners',
color: GameColor.red, color: AppColor.red,
icon: 'poker_icon', icon: 'poker_icon',
); );
testGame3 = Game( testGame3 = Game(
@@ -44,7 +44,7 @@ void main() {
name: 'Monopoly', name: 'Monopoly',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
description: 'A board game about real estate', description: 'A board game about real estate',
color: GameColor.orange, color: AppColor.orange,
icon: '', icon: '',
); );
}); });
@@ -124,7 +124,7 @@ void main() {
name: 'Game\'s & "Special" <Name>', name: 'Game\'s & "Special" <Name>',
ruleset: Ruleset.multipleWinners, ruleset: Ruleset.multipleWinners,
description: 'Description with émojis 🎮🎲', description: 'Description with émojis 🎮🎲',
color: GameColor.purple, color: AppColor.purple,
icon: '', icon: '',
); );
await database.gameDao.addGame(game: specialGame); await database.gameDao.addGame(game: specialGame);
@@ -280,19 +280,19 @@ void main() {
await database.gameDao.updateGameColor( await database.gameDao.updateGameColor(
gameId: testGame1.id, gameId: testGame1.id,
color: GameColor.green, color: AppColor.green,
); );
final updatedGame = await database.gameDao.getGameById( final updatedGame = await database.gameDao.getGameById(
gameId: testGame1.id, gameId: testGame1.id,
); );
expect(updatedGame.color, GameColor.green); expect(updatedGame.color, AppColor.green);
}); });
test('updateGameColor() does nothing for non-existent game', () async { test('updateGameColor() does nothing for non-existent game', () async {
final updated = await database.gameDao.updateGameColor( final updated = await database.gameDao.updateGameColor(
gameId: 'non-existent-id', gameId: 'non-existent-id',
color: GameColor.green, color: AppColor.green,
); );
expect(updated, isFalse); expect(updated, isFalse);
@@ -336,7 +336,7 @@ void main() {
name: newName, name: newName,
); );
const newGameColor = GameColor.teal; const newGameColor = AppColor.teal;
await database.gameDao.updateGameColor( await database.gameDao.updateGameColor(
gameId: testGame1.id, gameId: testGame1.id,
color: newGameColor, color: newGameColor,

View File

@@ -42,7 +42,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -0,0 +1,122 @@
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);
});
}

View File

@@ -40,7 +40,7 @@ void main() {
name: 'Test Game', name: 'Test Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'A test game', description: 'A test game',
color: GameColor.blue, color: AppColor.blue,
icon: '', icon: '',
); );
testMatch1 = Match( testMatch1 = Match(

View File

@@ -45,7 +45,7 @@ void main() {
name: 'Chess', name: 'Chess',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
description: 'Strategic board game', description: 'Strategic board game',
color: GameColor.blue, color: AppColor.blue,
icon: 'chess_icon', icon: 'chess_icon',
); );
@@ -448,19 +448,19 @@ void main() {
Game( Game(
name: 'Red Game', name: 'Red Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: GameColor.red, color: AppColor.red,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Blue Game', name: 'Blue Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: GameColor.blue, color: AppColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Green Game', name: 'Green Game',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: GameColor.green, color: AppColor.green,
icon: 'icon', icon: 'icon',
), ),
]; ];
@@ -484,19 +484,19 @@ void main() {
Game( Game(
name: 'Highest Score Game', name: 'Highest Score Game',
ruleset: Ruleset.highestScore, ruleset: Ruleset.highestScore,
color: GameColor.blue, color: AppColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Lowest Score Game', name: 'Lowest Score Game',
ruleset: Ruleset.lowestScore, ruleset: Ruleset.lowestScore,
color: GameColor.blue, color: AppColor.blue,
icon: 'icon', icon: 'icon',
), ),
Game( Game(
name: 'Single Winner', name: 'Single Winner',
ruleset: Ruleset.singleWinner, ruleset: Ruleset.singleWinner,
color: GameColor.blue, color: AppColor.blue,
icon: 'icon', icon: 'icon',
), ),
]; ];