58 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
1f17b80f64 Merge branch 'development' into feature/180-Spielerprofile-implementieren
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 51s
Pull Request Pipeline / localizations (pull_request) Successful in 28s
Pull Request Pipeline / test (pull_request) Successful in 49s
2026-05-23 00:42:59 +02:00
fad5a392cd Merge branch 'development' into feature/180-Spielerprofile-implementieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 53s
Pull Request Pipeline / localizations (pull_request) Successful in 25s
# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations.dart
#	lib/l10n/generated/app_localizations_de.dart
#	lib/l10n/generated/app_localizations_en.dart
2026-05-23 00:22:03 +02:00
5a652a5f2c feat: updatePlayerName keeps created order in nameCount
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-22 20:06:27 +02:00
4dcd4f0f71 fix: name count 3 player issue 2026-05-22 20:00:28 +02:00
25bc213769 Merge remote-tracking branch 'origin/feature/180-Spielerprofile-implementieren' into feature/180-Spielerprofile-implementieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-22 08:45:43 +02:00
9adcc29cda fix: updatePlayerName corrects the name count after renaming to different name
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 47s
Pull Request Pipeline / lint (pull_request) Successful in 55s
2026-05-21 23:57:59 +02:00
78c59a9b52 feat: add localization for no matches played and not part of any group 2026-05-21 16:20:48 +02:00
bf2cd2bf58 feat: add player creation callbacks to update member and match lists when group/match creation is canceled but player created
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 55s
2026-05-21 16:08:59 +02:00
ccb0d32c54 fix: tests for name count
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 54s
2026-05-21 15:45:29 +02:00
82095ab41a fix: name count 2026-05-21 15:33:26 +02:00
2a38462c57 fix linter issues
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 44s
Pull Request Pipeline / lint (pull_request) Successful in 56s
2026-05-21 10:34:01 +02:00
9909d959b0 implement missing localization
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 47s
Pull Request Pipeline / lint (pull_request) Failing after 56s
2026-05-21 10:33:16 +02:00
b61a93328f made alertDialog Confirm Button deactivate based on input, fix app skeleton alignment issue, implement correct nameCount Display
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 42s
Pull Request Pipeline / lint (pull_request) Failing after 51s
2026-05-21 09:47:49 +02:00
679e869229 fix: player count calc error
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 44s
Pull Request Pipeline / lint (pull_request) Failing after 52s
2026-05-20 19:58:59 +02:00
869c70ff63 add player change callbacks and improve player detail view
Some checks failed
Pull Request Pipeline / test (pull_request) Failing after 47s
Pull Request Pipeline / lint (pull_request) Failing after 53s
2026-05-20 19:50:34 +02:00
b305145d34 implement basic player_detail_view.dart
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 45s
Pull Request Pipeline / lint (pull_request) Failing after 51s
2026-05-20 15:15:47 +02:00
73 changed files with 7242 additions and 919 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -143,16 +143,16 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
final query = select(groupTable);
final result = await query.get();
return Future.wait(
result.map((groupData) async {
result.map((row) async {
final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: groupData.id,
groupId: row.id,
);
return Group(
id: groupData.id,
name: groupData.name,
description: groupData.description,
id: row.id,
name: row.name,
description: row.description,
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.
Future<Group> getGroupById({required String groupId}) async {
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(
groupId: groupId,
);
return Group(
id: result.id,
name: result.name,
description: result.description,
id: row.id,
name: row.name,
description: row.description,
members: members,
createdAt: result.createdAt,
createdAt: row.createdAt,
);
}
@@ -180,17 +180,49 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
Future<int> getGroupCount() async {
final count =
await (selectOnly(groupTable)..addColumns([groupTable.id.count()]))
.map((row) => row.read(groupTable.id.count()))
.map((tbl) => tbl.read(groupTable.id.count()))
.getSingle();
return count ?? 0;
}
/// Retrieves all groups a specific player belongs to.
/// Returns an empty list if the player is not part of any group.
Future<List<Group>> getGroupsByPlayer({required String playerId}) async {
final playerGroups = await (select(
playerGroupTable,
)..where((tbl) => tbl.playerId.equals(playerId))).get();
if (playerGroups.isEmpty) return [];
final groupIds = playerGroups.map((pg) => pg.groupId).toSet().toList();
final result =
await (select(groupTable)
..where((tbl) => tbl.id.isIn(groupIds))
..orderBy([(tbl) => OrderingTerm.desc(tbl.createdAt)]))
.get();
return Future.wait(
result.map((row) async {
final members = await db.playerGroupDao.getPlayersOfGroup(
groupId: row.id,
);
return Group(
id: row.id,
name: row.name,
description: row.description,
members: members,
createdAt: row.createdAt,
);
}),
);
}
/// Checks if a group with the given [groupId] exists in the database.
/// Returns `true` if the group exists, `false` otherwise.
Future<bool> groupExists({required String groupId}) async {
final query = select(groupTable)..where((g) => g.id.equals(groupId));
final result = await query.getSingleOrNull();
return result != null;
final row = await query.getSingleOrNull();
return row != null;
}
/* Delete */
@@ -220,9 +252,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
required String name,
}) async {
final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
GroupTableCompanion(name: Value(name)),
);
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
.write(GroupTableCompanion(name: Value(name)));
return rowsAffected > 0;
}
@@ -233,9 +264,8 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
required String description,
}) async {
final rowsAffected =
await (update(groupTable)..where((g) => g.id.equals(groupId))).write(
GroupTableCompanion(description: Value(description)),
);
await (update(groupTable)..where((tbl) => tbl.id.equals(groupId)))
.write(GroupTableCompanion(description: Value(description)));
return rowsAffected > 0;
}
}

View File

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

View File

@@ -17,7 +17,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// the new one.
Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) {
final int nameCount = await calculateNameCount(name: player.name);
final int nameCount = await _processNameCount(name: player.name);
await into(playerTable).insert(
PlayerTableCompanion.insert(
@@ -64,7 +64,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
final playersWithName = entry.value;
// Get the current nameCount
var nameCount = await calculateNameCount(name: name);
var nameCount = await _processNameCount(name: name);
// One player with the same name
if (playersWithName.length == 1) {
@@ -113,7 +113,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
Future<int> getPlayerCount() async {
final count =
await (selectOnly(playerTable)..addColumns([playerTable.id.count()]))
.map((row) => row.read(playerTable.id.count()))
.map((tbl) => tbl.read(playerTable.id.count()))
.getSingle();
return count ?? 0;
}
@@ -122,8 +122,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Returns `true` if the player exists, `false` otherwise.
Future<bool> playerExists({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingleOrNull();
return result != null;
final row = await query.getSingleOrNull();
return row != null;
}
/// Retrieves all players from the database.
@@ -146,57 +146,76 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Retrieves a [Player] by their [id].
Future<Player> getPlayerById({required String playerId}) async {
final query = select(playerTable)..where((p) => p.id.equals(playerId));
final result = await query.getSingle();
final row = await query.getSingle();
return Player(
id: result.id,
name: result.name,
description: result.description,
createdAt: result.createdAt,
nameCount: result.nameCount,
id: row.id,
name: row.name,
description: row.description,
createdAt: row.createdAt,
nameCount: row.nameCount,
);
}
/* Update */
/// Updates the name of the player with the given [playerId] to [name].
///
/// Keeps the `nameCount` values of the affected name groups consistent:
/// - The renamed player gets a fresh `nameCount` for the new name group.
/// - All players in the previous name group whose `nameCount` was greater
/// than the removed one get decremented by 1, so the numbering stays
/// contiguous (1..N) in `createdAt` order.
/// - If only one player remains in the previous name group, their
/// `nameCount` is reset to 0.
Future<bool> updatePlayerName({
required String playerId,
required String name,
}) async {
// Get previous name and name count for the player before updating
final previousPlayerName =
await (select(playerTable)..where((p) => p.id.equals(playerId)))
.map((row) => row.name)
.getSingleOrNull() ??
'';
final previousNameCount = await getNameCount(name: previousPlayerName);
return transaction(() async {
final previousPlayer = await (select(
playerTable,
)..where((tbl) => tbl.id.equals(playerId))).getSingleOrNull();
if (previousPlayer == null) return false;
final rowsAffected =
await (update(playerTable)..where((p) => p.id.equals(playerId))).write(
PlayerTableCompanion(name: Value(name)),
);
final previousName = previousPlayer.name;
final previousCount = previousPlayer.nameCount;
// Update name count for the new name
final count = await calculateNameCount(name: name);
if (count > 0) {
await (update(playerTable)..where((p) => p.name.equals(name))).write(
PlayerTableCompanion(nameCount: Value(count)),
);
}
// Determine the nameCount for the renamed player in the new group.
final newNameCount = await _processNameCount(name: name);
if (previousNameCount > 0) {
// Get the player with that name and the hightest nameCount, and update their nameCount to previousNameCount
final player = await getPlayerWithHighestNameCount(
name: previousPlayerName,
);
if (player != null) {
await updateNameCount(
playerId: player.id,
nameCount: previousNameCount,
);
final rowsAffected =
await (update(
playerTable,
)..where((tbl) => tbl.id.equals(playerId))).write(
PlayerTableCompanion(
name: Value(name),
nameCount: Value(newNameCount),
),
);
// Consolidate the previous name group.
final remainingCount = await getNameCount(name: previousName);
if (remainingCount == 1) {
// Only one player left
await (update(playerTable)..where((p) => p.name.equals(previousName)))
.write(const PlayerTableCompanion(nameCount: Value(0)));
} else if (remainingCount > 1 && previousCount > 0) {
// Shift every player above the gap down by one to keep numbering in order.
await (update(playerTable)..where(
(tbl) =>
tbl.name.equals(previousName) &
tbl.nameCount.isBiggerThanValue(previousCount),
))
.write(
PlayerTableCompanion.custom(
nameCount: playerTable.nameCount - const Constant(1),
),
);
}
}
return rowsAffected > 0;
return rowsAffected > 0;
});
}
/// Updates the description of the player with the given [playerId] to
@@ -207,9 +226,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String description,
}) async {
final rowsAffected =
await (update(playerTable)..where((g) => g.id.equals(playerId))).write(
PlayerTableCompanion(description: Value(description)),
);
await (update(playerTable)..where((tbl) => tbl.id.equals(playerId)))
.write(PlayerTableCompanion(description: Value(description)));
return rowsAffected > 0;
}
@@ -218,7 +236,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Deletes the player with the given [id] from the database.
/// Returns `true` if the player was deleted, `false` if the player did not exist.
Future<bool> deletePlayer({required String playerId}) async {
final query = delete(playerTable)..where((p) => p.id.equals(playerId));
final query = delete(playerTable)..where((tbl) => tbl.id.equals(playerId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
@@ -226,8 +244,10 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/* Name count management */
/// Retrieves the count of players with the given [name].
/// Returns the highest name count if players with the same name exist,
/// otherwise `null`.
Future<int> getNameCount({required String name}) async {
final query = select(playerTable)..where((p) => p.name.equals(name));
final query = select(playerTable)..where((tbl) => tbl.name.equals(name));
final result = await query.get();
return result.length;
}
@@ -238,7 +258,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
required String playerId,
required int nameCount,
}) 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(
PlayerTableCompanion(nameCount: Value(nameCount)),
);
@@ -248,8 +268,8 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
@visibleForTesting
Future<Player?> getPlayerWithHighestNameCount({required String name}) async {
final query = select(playerTable)
..where((p) => p.name.equals(name))
..orderBy([(p) => OrderingTerm.desc(p.nameCount)])
..where((tbl) => tbl.name.equals(name))
..orderBy([(tbl) => OrderingTerm.desc(tbl.nameCount)])
..limit(1);
final result = await query.getSingleOrNull();
if (result != null) {
@@ -264,34 +284,47 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
return null;
}
/// Processes the name count for a new player with the given [name].
///- 0 Player: returning 0
///- 1 Player: returning 2, and initializes the nameCount for the existing player to 1
///- Other: returning the existing count + 1
Future<int> _processNameCount({required String name}) async {
final nameCount = await calculateNameCount(name: name);
if (nameCount == 2) {
// If one other player exists with the same name, initialize the nameCount
await initializeNameCount(name: name);
}
return nameCount;
}
@visibleForTesting
/// Calculates the name count for a new player with the given [name].
/// - 0 Players: Name count is 0
/// - 1 Player: Name count is 2 (since the existing player will be 1)
/// - Other: Name count is the existing count + 1
Future<int> calculateNameCount({required String name}) async {
final count = await getNameCount(name: name);
final int nameCount;
if (count == 1) {
// If one other player exists with the same name, initialize the nameCount
await initializeNameCount(name: name);
// And for the new player, set nameCount to 2
if (count == 0) {
// If no other players exist with the same name, the returned nameCount is 0
nameCount = 0;
} else if (count == 1) {
// If one other player with the name count exists, the returned name count is 2
nameCount = 2;
} else if (count > 1) {
} else {
// If more than one player exists with the same name, just increment
// the nameCount for the new player
nameCount = count + 1;
} else {
// If no other players exist with the same name, set nameCount to 0
nameCount = 0;
}
return nameCount;
}
@visibleForTesting
Future<bool> initializeNameCount({required String name}) async {
final rowsAffected =
await (update(playerTable)..where((p) => p.name.equals(name))).write(
const PlayerTableCompanion(nameCount: Value(1)),
);
await (update(playerTable)..where((tbl) => tbl.name.equals(name)))
.write(const PlayerTableCompanion(nameCount: Value(1)));
return rowsAffected > 0;
}

View File

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

View File

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

View File

@@ -70,10 +70,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async {
final query = select(scoreEntryTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
(tbl) =>
tbl.playerId.equals(playerId) &
tbl.matchId.equals(matchId) &
tbl.roundNumber.equals(roundNumber),
);
final result = await query.getSingleOrNull();
@@ -91,7 +91,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
..where((tbl) => tbl.matchId.equals(matchId));
final result = await query.get();
final Map<String, ScoreEntry?> scoresByPlayer = {};
@@ -113,8 +113,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String matchId,
}) async {
final query = select(scoreEntryTable)
..where((s) => s.playerId.equals(playerId) & s.matchId.equals(matchId))
..orderBy([(s) => OrderingTerm.asc(s.roundNumber)]);
..where(
(tbl) => tbl.playerId.equals(playerId) & tbl.matchId.equals(matchId),
)
..orderBy([(tbl) => OrderingTerm.asc(tbl.roundNumber)]);
final result = await query.get();
return result
.map(
@@ -136,8 +138,8 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
final query = selectOnly(scoreEntryTable)
..where(scoreEntryTable.matchId.equals(matchId))
..addColumns([scoreEntryTable.roundNumber.max()]);
final result = await query.getSingle();
return result.read(scoreEntryTable.roundNumber.max());
final row = await query.getSingle();
return row.read(scoreEntryTable.roundNumber.max());
}
/// Aggregates the total score for a player in a match by summing all their
@@ -166,10 +168,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async {
final rowsAffected =
await (update(scoreEntryTable)..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(entry.roundNumber),
(tbl) =>
tbl.playerId.equals(playerId) &
tbl.matchId.equals(matchId) &
tbl.roundNumber.equals(entry.roundNumber),
))
.write(
ScoreEntryTableCompanion(
@@ -190,10 +192,10 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
}) async {
final query = delete(scoreEntryTable)
..where(
(s) =>
s.playerId.equals(playerId) &
s.matchId.equals(matchId) &
s.roundNumber.equals(roundNumber),
(tbl) =>
tbl.playerId.equals(playerId) &
tbl.matchId.equals(matchId) &
tbl.roundNumber.equals(roundNumber),
);
final rowsAffected = await query.go();
return rowsAffected > 0;
@@ -201,7 +203,7 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
Future<bool> deleteAllScoresForMatch({required String matchId}) async {
final query = delete(scoreEntryTable)
..where((s) => s.matchId.equals(matchId));
..where((tbl) => tbl.matchId.equals(matchId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
@@ -211,7 +213,9 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
required String playerId,
}) async {
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();
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 {
final count =
await (selectOnly(teamTable)..addColumns([teamTable.id.count()]))
.map((row) => row.read(teamTable.id.count()))
.map((tbl) => tbl.read(teamTable.id.count()))
.getSingle();
return count ?? 0;
}
@@ -95,8 +95,8 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Returns `true` if the team exists, `false` otherwise.
Future<bool> teamExists({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId));
final result = await query.getSingleOrNull();
return result != null;
final row = await query.getSingleOrNull();
return row != null;
}
/// 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.
Future<Team> getTeamById({required String teamId}) async {
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);
return Team(
id: result.id,
name: result.name,
createdAt: result.createdAt,
id: row.id,
name: row.name,
createdAt: row.createdAt,
members: members,
);
}
@@ -133,13 +133,13 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
Future<List<Player>> _getTeamMembers({required String teamId}) async {
// Get all player_match entries with this teamId
final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.teamId.equals(teamId));
..where((tbl) => tbl.teamId.equals(teamId));
final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return [];
// Get unique player IDs
final playerIds = playerMatches.map((pm) => pm.playerId).toSet();
final playerIds = playerMatches.map((tbl) => tbl.playerId).toSet();
// Fetch all players
final players = await Future.wait(
@@ -156,7 +156,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
required String name,
}) async {
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)),
);
return rowsAffected > 0;
@@ -175,7 +175,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
/// Deletes the team with the given [teamId] from the database.
/// Returns `true` if the team was deleted, `false` otherwise.
Future<bool> deleteTeam({required String teamId}) async {
final query = delete(teamTable)..where((t) => t.id.equals(teamId));
final query = delete(teamTable)..where((tbl) => tbl.id.equals(teamId));
final rowsAffected = await query.go();
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_match_dao.dart';
import 'package:tallee/data/dao/score_entry_dao.dart';
import 'package:tallee/data/dao/statistic_dao.dart';
import 'package:tallee/data/dao/statistic_game_dao.dart';
import 'package:tallee/data/dao/statistic_group_dao.dart';
import 'package:tallee/data/dao/statistic_scope_dao.dart';
import 'package:tallee/data/dao/team_dao.dart';
import 'package:tallee/data/db/tables/game_table.dart';
import 'package:tallee/data/db/tables/group_table.dart';
@@ -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_table.dart';
import 'package:tallee/data/db/tables/score_entry_table.dart';
import 'package:tallee/data/db/tables/statistic_game_table.dart';
import 'package:tallee/data/db/tables/statistic_group_table.dart';
import 'package:tallee/data/db/tables/statistic_scope_table.dart';
import 'package:tallee/data/db/tables/statistic_table.dart';
import 'package:tallee/data/db/tables/team_table.dart';
part 'database.g.dart';
@@ -30,6 +38,10 @@ part 'database.g.dart';
GameTable,
TeamTable,
ScoreEntryTable,
StatisticTable,
StatisticScopeTable,
StatisticGameTable,
StatisticGroupTable,
],
daos: [
PlayerDao,
@@ -40,6 +52,10 @@ part 'database.g.dart';
GameDao,
ScoreEntryDao,
TeamDao,
StatisticDao,
StatisticScopeDao,
StatisticGameDao,
StatisticGroupDao,
],
)
class AppDatabase extends _$AppDatabase {

File diff suppressed because it is too large Load Diff

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

View File

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

View File

@@ -107,7 +107,7 @@ class Match {
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: GameColor.blue,
color: AppColor.blue,
icon: '',
),
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",
"all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt",
"all_time": "Gesamter Zeitraum",
"amount_of_matches": "Anzahl der Spiele",
"app_name": "Tallee",
"average_score": "Durchschnittliche Punktzahl",
"best_player": "Beste:r Spieler:in",
"best_score": "Beste Punktzahl",
"cancel": "Abbrechen",
"choose_color": "Farbe wählen",
"choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen",
"classifier": "Klassifikator",
"color": "Farbe",
"color_blue": "Blau",
"color_green": "Grün",
@@ -19,12 +23,31 @@
"color_red": "Rot",
"color_teal": "Türkis",
"color_yellow": "Gelb",
"confirm": "Bestätigen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel erstellen",
"create_statistic": "Statistik erstellen",
"create_statistic_classifier_subtitle": "Wähle die anzuzeigende Hauptmetrik aus",
"create_statistic_classifier_title": "Klassifikator",
"create_statistic_games_subtitle": "Wähle die gefilterten Spielvorlagen",
"create_statistic_games_title": "Spielvorlagen",
"create_statistic_groups_subtitle": "Wähle die gefilterten Gruppen",
"create_statistic_groups_title": "Gruppen",
"create_statistic_scope_subtitle": "Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.",
"create_statistic_scope_title": "Bereich",
"create_statistic_timeframe_subtitle": "Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.",
"create_statistic_timeframe_title": "Zeitraum",
"created_on": "Erstellt am",
"data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht",
@@ -44,11 +67,15 @@
},
"delete_group": "Gruppe löschen",
"delete_match": "Spiel löschen",
"delete_player": "Spieler:in löschen",
"description": "Beschreibung",
"displayed_entries": "Angezeigte Einträge",
"drag_to_set_placement": "Ziehen um Platzierung zu setzen",
"edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten",
"edit_name": "Name ändern",
"edit_player": "Spieler bearbeiten",
"enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
@@ -59,30 +86,42 @@
"exit_view": "Ansicht verlassen",
"export_canceled": "Export abgebrochen",
"export_data": "Daten exportieren",
"filter": "Filter",
"format_exception": "Formatfehler (siehe Konsole)",
"game": "Spielvorlage",
"game_name": "Spielvorlagenname",
"games": "Spielvorlagen",
"group": "Gruppe",
"group_name": "Gruppenname",
"group_profile": "Gruppenprofil",
"groups": "Gruppen",
"groups_part_of": "Gruppen Teil von",
"highest_score": "Höchste Punkte",
"home": "Startseite",
"import_canceled": "Import abgebrochen",
"import_data": "Daten importieren",
"info": "Info",
"invalid_schema": "Ungültiges Schema",
"last_180_days": "Letzte 180 Tage",
"last_30_days": "Letzte 30 Tage",
"last_7_days": "Letzte 7 Tage",
"last_90_days": "Letzte 90 Tage",
"last_year": "Letztes Jahr",
"least_points": "Niedrigste Punkte",
"legal": "Rechtliches",
"legal_notice": "Impressum",
"licenses": "Lizenzen",
"live_edit_mode": "Live-Bearbeitungsmodus",
"loading": "Lädt...",
"loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte",
"match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel",
"match_profile": "Spielprofil",
"matches": "Spiele",
"matches_part_of": "Spiele Teil von",
"matches_played": "Spiele gespielt",
"matches_won": "Spiele gewonnen",
"members": "Mitglieder",
"most_points": "Höchste Punkte",
"multiple_winners": "Mehrere Gewinner:innen",
@@ -92,6 +131,7 @@
"no_license_text_available": "Kein Lizenztext verfügbar",
"no_licenses_found": "Keine Lizenzen gefunden",
"no_matches_created_yet": "Noch keine Spiele erstellt",
"no_matches_played_yet": "Noch kein Spiel gespielt",
"no_players_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:innen ausgewählt",
@@ -99,13 +139,16 @@
"no_results_entered_yet": "Noch keine Ergebnisse eingetragen",
"no_second_match_available": "Kein zweites Spiel verfügbar",
"no_statistics_available": "Keine Statistiken verfügbar",
"no_statistics_created_yet": "Noch keine Statistiken erstellt",
"none": "Kein",
"none_group": "Keine",
"not_available": "Nicht verfügbar",
"not_part_of_any_group": "Noch keiner Gruppe hinzugefügt",
"place": "Platz",
"placement": "Platzierung",
"played_matches": "Gespielte Spiele",
"player_name": "Spieler:innenname",
"player_profile": "Spieler:in-Profil",
"players": "Spieler:innen",
"point": "Punkt",
"points": "Punkte",
@@ -121,15 +164,36 @@
"ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.",
"ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.",
"save_changes": "Änderungen speichern",
"scope": "Bereich",
"search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen",
"select_a_classifier": "Klassifikator auswählen",
"select_a_game": "Spielvorlage auswählen",
"select_a_group": "Gruppe auswählen",
"select_a_scope": "Bereich auswählen",
"select_a_timeframe": "Zeitraum auswählen",
"select_a_timeframe_for_which_data_will_be_filtered": "Wähle einen Zeitraum, für den die Daten gefiltert werden sollen",
"select_loser": "Verlierer:in wählen",
"select_the_filtered_games": "Wähle Spiele, nach denen gefiltert werden soll.",
"select_the_filtered_groups": "Wähle Gruppen, nach denen gefiltert werden soll.",
"select_the_filtered_timeframe": "Wähle einen Zeitraum, nach dem gefiltert werden soll.",
"select_winner": "Gewinner:in wählen",
"select_winners": "Gewinner:innen wählen",
"selected_games": "Ausgewählte Spielvorlagen",
"selected_groups": "Ausgewählte Gruppen",
"selected_players": "Ausgewählte Spieler:innen",
"set_name": "Name setzen",
"settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in",
"statistic_type_average_score": "Durchschnittliche Punktzahl",
"statistic_type_best_score": "Beste Punktzahl",
"statistic_type_total_losses": "Niederlagen insgesamt",
"statistic_type_total_matches": "Spiele insgesamt",
"statistic_type_total_score": "Punktzahl insgesamt",
"statistic_type_total_wins": "Siege insgesamt",
"statistic_type_winrate": "Siegquote",
"statistic_type_worst_score": "Schlechteste Punktzahl",
"statistics": "Statistiken",
"stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
@@ -137,12 +201,18 @@
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",
"tie": "Unentschieden",
"timeframe": "Zeitraum",
"today_at": "Heute um",
"total_losses": "Niederlagen insgesamt",
"total_matches": "Spiele insgesamt",
"total_score": "Punktzahl insgesamt",
"total_wins": "Siege insgesamt",
"undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)",
"winner": "Gewinner:in",
"winners": "Gewinner:innen",
"winrate": "Siegquote",
"wins": "Siege",
"worst_score": "Schlechteste Punktzahl",
"yesterday_at": "Gestern um"
}

View File

@@ -18,13 +18,30 @@
"color_purple": "Purple",
"color_red": "Red",
"color_teal": "Teal",
"displayed_entries": "Displayed entries",
"color_yellow": "Yellow",
"could_not_add_player": "Could not add player",
"confirm": "Confirm",
"could_not_add_player": "Could not add player {playerName}",
"@could_not_add_player": {
"placeholders": {
"playerName": {
"type": "String"
}
}
},
"create_game": "Create Game",
"create_group": "Create Group",
"create_match": "Create match",
"create_new_group": "Create new group",
"create_new_match": "Create new match",
"create_statistic": "Create statistic",
"classifier": "Classifier",
"select_the_filtered_timeframe": "Select the timeframe you want to filter by.",
"select_the_filtered_games": "Select the games you want to filter by.",
"games": "Games",
"select_the_filtered_groups": "Select the groups you want to filter by.",
"scope": "Scope",
"timeframe": "Timeframe",
"created_on": "Created on",
"data": "Data",
"data_successfully_deleted": "Data successfully deleted",
@@ -42,13 +59,17 @@
}
}
},
"filter": "Filter",
"delete_group": "Delete Group",
"delete_match": "Delete Match",
"delete_player": "Delete player?",
"description": "Description",
"drag_to_set_placement": "Drag to set placement",
"edit_game": "Edit Game",
"edit_group": "Edit Group",
"edit_match": "Edit Match",
"edit_name": "Edit name",
"edit_player": "Edit player",
"enter_points": "Enter points",
"enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again",
@@ -66,6 +87,7 @@
"group_name": "Group name",
"group_profile": "Group Profile",
"groups": "Groups",
"groups_part_of": "Groups part of",
"highest_score": "Highest Score",
"home": "Home",
"import_canceled": "Import canceled",
@@ -77,12 +99,16 @@
"legal_notice": "Legal Notice",
"licenses": "Licenses",
"live_edit_mode": "Live Edit Mode",
"loading": "Loading...",
"loser": "Loser",
"lowest_score": "Lowest Score",
"match_in_progress": "Match in progress...",
"match_name": "Match name",
"match_profile": "Match Profile",
"matches": "Matches",
"matches_part_of": "Matches part of",
"matches_played": "Matches played",
"matches_won": "Matches won",
"members": "Members",
"most_points": "Most Points",
"multiple_winners": "Multiple Winners",
@@ -92,6 +118,7 @@
"no_license_text_available": "No license text available",
"no_licenses_found": "No licenses found",
"no_matches_created_yet": "No matches created yet",
"no_matches_played_yet": "No games played yet",
"no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected",
@@ -99,13 +126,16 @@
"no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available",
"no_statistics_created_yet": "No statistics created yet",
"none": "None",
"none_group": "None",
"not_available": "Not available",
"not_part_of_any_group": "Not part of any group yet",
"place": "place",
"placement": "Placement",
"played_matches": "Played Matches",
"player_name": "Player name",
"player_profile": "Player Profile",
"players": "Players",
"point": "Point",
"points": "Points",
@@ -126,11 +156,26 @@
"select_winner": "Select Winner",
"select_winners": "Select Winners",
"selected_players": "Selected players",
"set_name": "Set name",
"settings": "Settings",
"select_a_classifier": "Select a classifier",
"select_a_game": "Select a game",
"select_a_group": "Select a group",
"select_a_scope": "Select a scope",
"select_a_timeframe": "Select a timeframe",
"single_loser": "Single Loser",
"single_winner": "Single Winner",
"statistics": "Statistics",
"stats": "Stats",
"selected_games": "Selected games",
"selected_groups": "Selected groups",
"average_score": "Average score",
"best_score": "Best score",
"total_losses": "Total losses",
"total_matches": "Total matches",
"total_score": "Total score",
"total_wins": "Total wins",
"worst_score": "Worst score",
"successfully_added_player": "Successfully added player {playerName}",
"@successfully_added_player": {
"description": "Success message when adding a player",
@@ -145,6 +190,12 @@
"there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone.",
"tie": "Tie",
"all_time": "All time",
"last_180_days": "Last 180 days",
"last_30_days": "Last 30 days",
"last_7_days": "Last 7 days",
"last_90_days": "Last 90 days",
"last_year": "Last year",
"today_at": "Today at",
"undo": "Undo",
"unknown_exception": "Unknown Exception (see console)",

View File

@@ -206,17 +206,29 @@ abstract class AppLocalizations {
/// **'Teal'**
String get color_teal;
/// No description provided for @displayed_entries.
///
/// In en, this message translates to:
/// **'Displayed entries'**
String get displayed_entries;
/// No description provided for @color_yellow.
///
/// In en, this message translates to:
/// **'Yellow'**
String get color_yellow;
/// No description provided for @confirm.
///
/// In en, this message translates to:
/// **'Confirm'**
String get confirm;
/// No description provided for @could_not_add_player.
///
/// In en, this message translates to:
/// **'Could not add player'**
String could_not_add_player(Object playerName);
/// **'Could not add player {playerName}'**
String could_not_add_player(String playerName);
/// No description provided for @create_game.
///
@@ -248,6 +260,54 @@ abstract class AppLocalizations {
/// **'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.
///
/// In en, this message translates to:
@@ -308,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.'**
String delete_game_with_matches_warning(int count);
/// No description provided for @filter.
///
/// In en, this message translates to:
/// **'Filter'**
String get filter;
/// No description provided for @delete_group.
///
/// In en, this message translates to:
@@ -320,6 +386,12 @@ abstract class AppLocalizations {
/// **'Delete Match'**
String get delete_match;
/// No description provided for @delete_player.
///
/// In en, this message translates to:
/// **'Delete player?'**
String get delete_player;
/// No description provided for @description.
///
/// In en, this message translates to:
@@ -350,6 +422,18 @@ abstract class AppLocalizations {
/// **'Edit Match'**
String get edit_match;
/// No description provided for @edit_name.
///
/// In en, this message translates to:
/// **'Edit name'**
String get edit_name;
/// No description provided for @edit_player.
///
/// In en, this message translates to:
/// **'Edit player'**
String get edit_player;
/// No description provided for @enter_points.
///
/// In en, this message translates to:
@@ -452,6 +536,12 @@ abstract class AppLocalizations {
/// **'Groups'**
String get groups;
/// No description provided for @groups_part_of.
///
/// In en, this message translates to:
/// **'Groups part of'**
String get groups_part_of;
/// No description provided for @highest_score.
///
/// In en, this message translates to:
@@ -518,6 +608,12 @@ abstract class AppLocalizations {
/// **'Live Edit Mode'**
String get live_edit_mode;
/// No description provided for @loading.
///
/// In en, this message translates to:
/// **'Loading...'**
String get loading;
/// No description provided for @loser.
///
/// In en, this message translates to:
@@ -554,6 +650,24 @@ abstract class AppLocalizations {
/// **'Matches'**
String get matches;
/// No description provided for @matches_part_of.
///
/// In en, this message translates to:
/// **'Matches part of'**
String get matches_part_of;
/// No description provided for @matches_played.
///
/// In en, this message translates to:
/// **'Matches played'**
String get matches_played;
/// No description provided for @matches_won.
///
/// In en, this message translates to:
/// **'Matches won'**
String get matches_won;
/// No description provided for @members.
///
/// In en, this message translates to:
@@ -608,6 +722,12 @@ abstract class AppLocalizations {
/// **'No matches created yet'**
String get no_matches_created_yet;
/// No description provided for @no_matches_played_yet.
///
/// In en, this message translates to:
/// **'No games played yet'**
String get no_matches_played_yet;
/// No description provided for @no_players_created_yet.
///
/// In en, this message translates to:
@@ -650,6 +770,12 @@ abstract class AppLocalizations {
/// **'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.
///
/// In en, this message translates to:
@@ -668,6 +794,12 @@ abstract class AppLocalizations {
/// **'Not available'**
String get not_available;
/// No description provided for @not_part_of_any_group.
///
/// In en, this message translates to:
/// **'Not part of any group yet'**
String get not_part_of_any_group;
/// No description provided for @place.
///
/// In en, this message translates to:
@@ -692,6 +824,12 @@ abstract class AppLocalizations {
/// **'Player name'**
String get player_name;
/// No description provided for @player_profile.
///
/// In en, this message translates to:
/// **'Player Profile'**
String get player_profile;
/// No description provided for @players.
///
/// In en, this message translates to:
@@ -812,12 +950,48 @@ abstract class AppLocalizations {
/// **'Selected players'**
String get selected_players;
/// No description provided for @set_name.
///
/// In en, this message translates to:
/// **'Set name'**
String get set_name;
/// No description provided for @settings.
///
/// In en, this message translates to:
/// **'Settings'**
String get settings;
/// No description provided for @select_a_classifier.
///
/// In en, this message translates to:
/// **'Select a classifier'**
String get select_a_classifier;
/// No description provided for @select_a_game.
///
/// In en, this message translates to:
/// **'Select a game'**
String get select_a_game;
/// No description provided for @select_a_group.
///
/// In en, this message translates to:
/// **'Select a group'**
String get select_a_group;
/// No description provided for @select_a_scope.
///
/// In en, this message translates to:
/// **'Select a scope'**
String get select_a_scope;
/// No description provided for @select_a_timeframe.
///
/// In en, this message translates to:
/// **'Select a timeframe'**
String get select_a_timeframe;
/// No description provided for @single_loser.
///
/// In en, this message translates to:
@@ -842,6 +1016,60 @@ abstract class AppLocalizations {
/// **'Stats'**
String get stats;
/// No description provided for @selected_games.
///
/// In en, this message translates to:
/// **'Selected games'**
String get selected_games;
/// No description provided for @selected_groups.
///
/// In en, this message translates to:
/// **'Selected groups'**
String get selected_groups;
/// No description provided for @average_score.
///
/// In en, this message translates to:
/// **'Average score'**
String get average_score;
/// No description provided for @best_score.
///
/// In en, this message translates to:
/// **'Best score'**
String get best_score;
/// No description provided for @total_losses.
///
/// In en, this message translates to:
/// **'Total losses'**
String get total_losses;
/// No description provided for @total_matches.
///
/// In en, this message translates to:
/// **'Total matches'**
String get total_matches;
/// No description provided for @total_score.
///
/// In en, this message translates to:
/// **'Total score'**
String get total_score;
/// No description provided for @total_wins.
///
/// In en, this message translates to:
/// **'Total wins'**
String get total_wins;
/// No description provided for @worst_score.
///
/// In en, this message translates to:
/// **'Worst score'**
String get worst_score;
/// Success message when adding a player
///
/// In en, this message translates to:
@@ -872,6 +1100,42 @@ abstract class AppLocalizations {
/// **'Tie'**
String get tie;
/// No description provided for @all_time.
///
/// In en, this message translates to:
/// **'All time'**
String get all_time;
/// No description provided for @last_180_days.
///
/// In en, this message translates to:
/// **'Last 180 days'**
String get last_180_days;
/// No description provided for @last_30_days.
///
/// In en, this message translates to:
/// **'Last 30 days'**
String get last_30_days;
/// No description provided for @last_7_days.
///
/// In en, this message translates to:
/// **'Last 7 days'**
String get last_7_days;
/// No description provided for @last_90_days.
///
/// In en, this message translates to:
/// **'Last 90 days'**
String get last_90_days;
/// No description provided for @last_year.
///
/// In en, this message translates to:
/// **'Last year'**
String get last_year;
/// No description provided for @today_at.
///
/// In en, this message translates to:

View File

@@ -62,11 +62,17 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get color_teal => 'Türkis';
@override
String get displayed_entries => 'Angezeigte Einträge';
@override
String get color_yellow => 'Gelb';
@override
String could_not_add_player(Object playerName) {
String get confirm => 'Bestätigen';
@override
String could_not_add_player(String playerName) {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
}
@@ -85,6 +91,33 @@ class AppLocalizationsDe extends AppLocalizations {
@override
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
String get created_on => 'Erstellt am';
@@ -125,12 +158,18 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.';
}
@override
String get filter => 'Filter';
@override
String get delete_group => 'Gruppe löschen';
@override
String get delete_match => 'Spiel löschen';
@override
String get delete_player => 'Spieler:in löschen';
@override
String get description => 'Beschreibung';
@@ -146,6 +185,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get edit_match => 'Gruppe bearbeiten';
@override
String get edit_name => 'Name ändern';
@override
String get edit_player => 'Spieler bearbeiten';
@override
String get enter_points => 'Punkte eingeben';
@@ -201,6 +246,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get groups => 'Gruppen';
@override
String get groups_part_of => 'Gruppen Teil von';
@override
String get highest_score => 'Höchste Punkte';
@@ -234,6 +282,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get live_edit_mode => 'Live-Bearbeitungsmodus';
@override
String get loading => 'Lädt...';
@override
String get loser => 'Verlierer:in';
@@ -252,6 +303,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get matches => 'Spiele';
@override
String get matches_part_of => 'Spiele Teil von';
@override
String get matches_played => 'Spiele gespielt';
@override
String get matches_won => 'Spiele gewonnen';
@override
String get members => 'Mitglieder';
@@ -279,6 +339,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@override
String get no_matches_played_yet => 'Noch kein Spiel gespielt';
@override
String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@@ -301,6 +364,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get no_statistics_available => 'Keine Statistiken verfügbar';
@override
String get no_statistics_created_yet => 'Noch keine Statistiken erstellt';
@override
String get none => 'Kein';
@@ -310,6 +376,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get not_available => 'Nicht verfügbar';
@override
String get not_part_of_any_group => 'Noch keiner Gruppe hinzugefügt';
@override
String get place => 'Platz';
@@ -322,6 +391,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get player_name => 'Spieler:innenname';
@override
String get player_profile => 'Spieler:in-Profil';
@override
String get players => 'Spieler:innen';
@@ -387,9 +459,27 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get selected_players => 'Ausgewählte Spieler:innen';
@override
String get set_name => 'Name setzen';
@override
String get settings => 'Einstellungen';
@override
String get select_a_classifier => 'Klassifikator auswählen';
@override
String get select_a_game => 'Spielvorlage auswählen';
@override
String get select_a_group => 'Gruppe auswählen';
@override
String get select_a_scope => 'Bereich auswählen';
@override
String get select_a_timeframe => 'Zeitraum auswählen';
@override
String get single_loser => 'Ein:e Verlierer:in';
@@ -402,6 +492,33 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get stats => 'Statistiken';
@override
String get selected_games => 'Ausgewählte Spielvorlagen';
@override
String get selected_groups => 'Ausgewählte Gruppen';
@override
String get average_score => 'Durchschnittliche Punktzahl';
@override
String get best_score => 'Beste Punktzahl';
@override
String get total_losses => 'Niederlagen insgesamt';
@override
String get total_matches => 'Spiele insgesamt';
@override
String get total_score => 'Punktzahl insgesamt';
@override
String get total_wins => 'Siege insgesamt';
@override
String get worst_score => 'Schlechteste Punktzahl';
@override
String successfully_added_player(String playerName) {
return 'Spieler:in $playerName erfolgreich hinzugefügt';
@@ -422,6 +539,24 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get tie => 'Unentschieden';
@override
String get all_time => 'Gesamter Zeitraum';
@override
String get last_180_days => 'Letzte 180 Tage';
@override
String get last_30_days => 'Letzte 30 Tage';
@override
String get last_7_days => 'Letzte 7 Tage';
@override
String get last_90_days => 'Letzte 90 Tage';
@override
String get last_year => 'Letztes Jahr';
@override
String get today_at => 'Heute um';

View File

@@ -62,12 +62,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get color_teal => 'Teal';
@override
String get displayed_entries => 'Displayed entries';
@override
String get color_yellow => 'Yellow';
@override
String could_not_add_player(Object playerName) {
return 'Could not add player';
String get confirm => 'Confirm';
@override
String could_not_add_player(String playerName) {
return 'Could not add player $playerName';
}
@override
@@ -85,6 +91,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override
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
String get created_on => 'Created on';
@@ -125,12 +158,18 @@ class AppLocalizationsEn extends AppLocalizations {
return 'If you delete this game template, $_temp0 using this game template will also be deleted.';
}
@override
String get filter => 'Filter';
@override
String get delete_group => 'Delete Group';
@override
String get delete_match => 'Delete Match';
@override
String get delete_player => 'Delete player?';
@override
String get description => 'Description';
@@ -146,6 +185,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get edit_match => 'Edit Match';
@override
String get edit_name => 'Edit name';
@override
String get edit_player => 'Edit player';
@override
String get enter_points => 'Enter points';
@@ -201,6 +246,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get groups => 'Groups';
@override
String get groups_part_of => 'Groups part of';
@override
String get highest_score => 'Highest Score';
@@ -234,6 +282,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get live_edit_mode => 'Live Edit Mode';
@override
String get loading => 'Loading...';
@override
String get loser => 'Loser';
@@ -252,6 +303,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get matches => 'Matches';
@override
String get matches_part_of => 'Matches part of';
@override
String get matches_played => 'Matches played';
@override
String get matches_won => 'Matches won';
@override
String get members => 'Members';
@@ -279,6 +339,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get no_matches_created_yet => 'No matches created yet';
@override
String get no_matches_played_yet => 'No games played yet';
@override
String get no_players_created_yet => 'No players created yet';
@@ -301,6 +364,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get no_statistics_available => 'No statistics available';
@override
String get no_statistics_created_yet => 'No statistics created yet';
@override
String get none => 'None';
@@ -310,6 +376,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get not_available => 'Not available';
@override
String get not_part_of_any_group => 'Not part of any group yet';
@override
String get place => 'place';
@@ -322,6 +391,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get player_name => 'Player name';
@override
String get player_profile => 'Player Profile';
@override
String get players => 'Players';
@@ -387,9 +459,27 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get selected_players => 'Selected players';
@override
String get set_name => 'Set name';
@override
String get settings => 'Settings';
@override
String get select_a_classifier => 'Select a classifier';
@override
String get select_a_game => 'Select a game';
@override
String get select_a_group => 'Select a group';
@override
String get select_a_scope => 'Select a scope';
@override
String get select_a_timeframe => 'Select a timeframe';
@override
String get single_loser => 'Single Loser';
@@ -402,6 +492,33 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get stats => 'Stats';
@override
String get selected_games => 'Selected games';
@override
String get selected_groups => 'Selected groups';
@override
String get average_score => 'Average score';
@override
String get best_score => 'Best score';
@override
String get total_losses => 'Total losses';
@override
String get total_matches => 'Total matches';
@override
String get total_score => 'Total score';
@override
String get total_wins => 'Total wins';
@override
String get worst_score => 'Worst score';
@override
String successfully_added_player(String playerName) {
return 'Successfully added player $playerName';
@@ -421,6 +538,24 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get tie => 'Tie';
@override
String get all_time => 'All time';
@override
String get last_180_days => 'Last 180 days';
@override
String get last_30_days => 'Last 30 days';
@override
String get last_7_days => 'Last 7 days';
@override
String get last_90_days => 'Last 90 days';
@override
String get last_year => 'Last year';
@override
String get today_at => 'Today at';

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

View File

@@ -89,6 +89,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
Expanded(
child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers,
onPlayerCreated: () => widget.onMembersChanged?.call(),
onChanged: (value) {
setState(() {
selectedPlayers = [...value];
@@ -134,6 +135,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
if (!mounted) return;
if (success) {
widget.onMembersChanged?.call();
await HapticFeedback.successNotification();
if (mounted) {
Navigator.pop(context, updatedGroup);
@@ -157,7 +159,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final success = await db.groupDao.addGroup(
group: Group(name: groupName, members: selectedPlayers),
);
return success;
}

View File

@@ -77,6 +77,7 @@ class _GroupViewState extends State<GroupView> {
);
}
return GroupTile(
onPlayerChanged: loadGroups,
group: groups[index],
onTap: () async {
await Navigator.push(
@@ -106,13 +107,10 @@ class _GroupViewState extends State<GroupView> {
context,
adaptivePageRoute(
builder: (context) {
return const CreateGroupView();
return CreateGroupView(onMembersChanged: loadGroups);
},
),
);
setState(() {
loadGroups();
});
},
),
),

View File

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

View File

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

View File

@@ -196,6 +196,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers,
onPlayerCreated: () => widget.onMatchesUpdated?.call(),
onChanged: (value) {
setState(() {
selectedPlayers = value;

View File

@@ -39,7 +39,7 @@ class _MatchViewState extends State<MatchView> {
game: Game(
name: 'Game name',
ruleset: Ruleset.singleWinner,
color: GameColor.blue,
color: AppColor.blue,
icon: '',
),
group: Group(
@@ -79,7 +79,7 @@ class _MatchViewState extends State<MatchView> {
visible: matches.isNotEmpty,
replacement: Center(
child: TopCenteredMessage(
icon: Icons.report,
icon: Icons.info,
title: loc.info,
message: loc.no_matches_created_yet,
),
@@ -97,6 +97,7 @@ class _MatchViewState extends State<MatchView> {
child: Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: MatchTile(
onPlayerEdited: loadMatches,
width: MediaQuery.sizeOf(context).width * 0.95,
onTap: () async {
Navigator.push(

View File

@@ -0,0 +1,394 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class PlayerDetailView extends StatefulWidget {
const PlayerDetailView({
super.key,
required this.player,
required this.callback,
});
/// The player to display
final Player player;
final VoidCallback callback;
@override
State<PlayerDetailView> createState() => _PlayerDetailViewState();
}
class _PlayerDetailViewState extends State<PlayerDetailView> {
late final AppDatabase db;
late Player _player;
late String playerNameCount;
bool isLoading = true;
/// Total matches played by this player
int totalMatches = 0;
/// Total matches won by this player
int matchesWon = 0;
/// Total groups this player belongs to
int totalGroups = 0;
/// Full list of groups this player belongs to
List<Group> playerGroups = List.filled(
4,
Group(name: 'Skeleton group', members: []),
);
/// Full list of matches this player played in
List<Match> playerMatches = List.filled(
4,
Match(
name: 'Skeleton match',
game: Game(name: 'Game name', ruleset: Ruleset.singleWinner),
players: [],
),
);
TextEditingController nameController = TextEditingController();
@override
void initState() {
super.initState();
_player = widget.player;
db = Provider.of<AppDatabase>(context, listen: false);
playerNameCount = getNameCountText(_player);
_loadData();
}
@override
void dispose() {
nameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.player_profile),
actions: [
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(
context: context,
builder: (context) => CustomAlertDialog(
title: loc.delete_player,
content: Text(loc.this_cannot_be_undone),
actions: [
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(true),
text: loc.delete,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
//TODO: implement player deletion in db
if (!context.mounted) return;
Navigator.pop(context);
widget.callback();
}
});
},
),
],
),
body: SafeArea(
child: Stack(
alignment: Alignment.center,
children: [
ListView(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 20,
bottom: 100,
),
children: [
const Center(
child: ColoredIconContainer(
icon: Icons.person,
containerSize: 55,
iconSize: 38,
),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_player.name,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
Text(
playerNameCount,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor.withAlpha(120),
),
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 5),
Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_player.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
InfoTile(
title: '${loc.matches_part_of} ($totalMatches)',
icon: Icons.sports_esports,
horizontalAlignment: CrossAxisAlignment.start,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
alignment: Alignment.topLeft,
child: playerMatches.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: playerMatches.map((match) {
return TextIconTile(
text: match.name,
iconEnabled: false,
);
}).toList(),
)
: Text(
loc.no_matches_played_yet,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
),
const SizedBox(height: 15),
InfoTile(
title: '${loc.groups_part_of} ($totalGroups)',
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
alignment: Alignment.topLeft,
child: playerGroups.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: playerGroups.map((group) {
return TextIconTile(
text: group.name,
iconEnabled: false,
);
}).toList(),
)
: Text(
loc.not_part_of_any_group,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
),
const SizedBox(height: 15),
InfoTile(
title: loc.statistics,
icon: Icons.bar_chart,
content: AppSkeleton(
enabled: isLoading,
fixLayoutBuilder: true,
child: Column(
children: [
_buildStatRow(
loc.matches_played,
totalMatches.toString(),
),
_buildStatRow(loc.matches_won, matchesWon.toString()),
_buildStatRow(
loc.winrate,
'${totalMatches == 0 ? 0 : ((matchesWon / totalMatches) * 100).round()}%',
),
],
),
),
),
],
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: MainMenuButton(
text: loc.edit_player,
icon: Icons.edit,
onPressed: () async {
nameController.text = _player.name;
showDialog<bool>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setDialogState) {
return CustomAlertDialog(
title: loc.edit_name,
content: TextInputField(
controller: nameController,
hintText: loc.set_name,
onChanged: (_) => setDialogState(() {}),
),
actions: [
CustomDialogAction(
onPressed: isConfirmButtonEnabled()
? () => Navigator.of(context).pop(true)
: null,
text: loc.confirm,
),
CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
buttonType: ButtonType.secondary,
text: loc.cancel,
),
],
);
},
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
final newName = nameController.text.trim();
if (newName != _player.name) {
final fetchedPlayerNameCount = await db.playerDao
.getNameCount(name: newName);
await db.playerDao.updatePlayerName(
playerId: _player.id,
name: newName,
);
widget.callback.call();
setState(() {
_player = Player(
name: newName,
createdAt: _player.createdAt,
id: _player.id,
nameCount: _player.nameCount,
description: _player.description,
);
// If there is already a player with the same name,
// the count of that player is 0, so we start counting from 2 to get the correct count for this player. If there are no players with the same name, we just show the name without a count.
playerNameCount = fetchedPlayerNameCount == 0
? ''
: ' #${fetchedPlayerNameCount + 1}';
});
}
}
});
},
),
),
],
),
),
);
}
/// Loads statistics for this player
Future<void> _loadData() async {
isLoading = true;
final fetchedMatches = await db.matchDao.getMatchesByPlayer(
playerId: _player.id,
);
final fetchedGroups = await db.groupDao.getGroupsByPlayer(
playerId: _player.id,
);
if (!mounted) return;
setState(() {
playerMatches = fetchedMatches;
totalMatches = fetchedMatches.length;
matchesWon = fetchedMatches
.where((match) => match.mvp.any((mvp) => mvp.id == _player.id))
.length;
playerGroups = fetchedGroups;
totalGroups = fetchedGroups.length;
isLoading = false;
});
}
/// Builds a single statistic row with a label and value
/// - [label]: The label of the statistic
/// - [value]: The value of the statistic
Widget _buildStatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
],
),
Text(
value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
bool isConfirmButtonEnabled() {
return nameController.text.trim().isNotEmpty;
}
}

View File

@@ -34,7 +34,6 @@ const allDependencies = <Package>[
_cli_util,
_clock,
_code_assets,
_code_builder,
_collection,
_convert,
_coverage,
@@ -154,6 +153,7 @@ const allDependencies = <Package>[
_source_map_stack_trace,
_source_maps,
_source_span,
_sqlcipher_flutter_libs,
_sqlite3,
_sqlite3_flutter_libs,
_sqlparser,
@@ -670,17 +670,17 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// build_runner 2.13.1
/// build_runner 2.15.0
const _build_runner = Package(
name: 'build_runner',
description: 'A build system for Dart code generation and modular compilation.',
repository: 'https://github.com/dart-lang/build/tree/master/build_runner',
authors: [],
version: '2.13.1',
version: '2.15.0',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('analyzer'), PackageRef('args'), PackageRef('async'), PackageRef('build'), PackageRef('build_config'), PackageRef('build_daemon'), PackageRef('built_collection'), PackageRef('built_value'), PackageRef('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')],
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.''',
);
/// code_builder 4.11.1
const _code_builder = Package(
name: 'code_builder',
description: 'A fluent, builder-based library for generating valid Dart code.',
repository: 'https://github.com/dart-lang/tools/tree/main/pkgs/code_builder',
authors: [],
version: '4.11.1',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('built_collection'), PackageRef('built_value'), PackageRef('collection'), PackageRef('matcher'), PackageRef('meta')],
devDependencies: [PackageRef('build'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('source_gen'), PackageRef('test')],
license: '''Copyright 2016, the Dart project authors.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Google LLC nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.''',
);
/// collection 1.19.1
const _collection = Package(
name: 'collection',
@@ -2581,14 +2540,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// drift 2.31.0
/// drift 2.33.0
const _drift = Package(
name: 'drift',
description: 'Drift is a reactive library to store relational data in Dart and Flutter applications.',
homepage: 'https://drift.simonbinder.eu/',
repository: 'https://github.com/simolus3/drift',
authors: [],
version: '2.31.0',
version: '2.33.0',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
@@ -2617,14 +2576,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// drift_dev 2.31.0
/// drift_dev 2.33.0
const _drift_dev = Package(
name: 'drift_dev',
description: 'Dev-dependency for users of drift. Contains the generator and development tools.',
homepage: 'https://drift.simonbinder.eu/',
repository: 'https://github.com/simolus3/drift',
authors: [],
version: '2.31.0',
version: '2.33.0',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
@@ -2653,18 +2612,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// drift_flutter 0.2.8
/// drift_flutter 0.3.0
const _drift_flutter = Package(
name: 'drift_flutter',
description: 'Easily set up drift databases across platforms in Flutter apps.',
homepage: 'https://drift.simonbinder.eu/',
repository: 'https://github.com/simolus3/drift',
authors: [],
version: '0.2.8',
version: '0.3.0',
spdxIdentifiers: ['MIT'],
isMarkdown: 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')],
license: '''MIT License
@@ -3057,18 +3016,18 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// file_saver 0.3.1
/// file_saver 0.4.0
const _file_saver = Package(
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',
repository: 'https://github.com/incrediblezayed/file_saver',
authors: [],
version: '0.3.1',
version: '0.4.0',
spdxIdentifiers: ['BSD-3-Clause'],
isMarkdown: 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')],
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.''',
);
/// 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(
name: 'sqlite3',
description: 'Provides lightweight yet convenient bindings to SQLite by using dart:ffi',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3',
authors: [],
version: '2.9.4',
version: '3.3.1',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('path'), PackageRef('web'), PackageRef('typed_data')],
devDependencies: [PackageRef('analyzer'), PackageRef('build_daemon'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('http'), PackageRef('lints'), PackageRef('shelf'), PackageRef('shelf_static'), PackageRef('stream_channel'), PackageRef('test'), PackageRef('pub_semver'), PackageRef('convert')],
dependencies: [PackageRef('collection'), PackageRef('ffi'), PackageRef('meta'), PackageRef('path'), PackageRef('web'), PackageRef('typed_data'), PackageRef('hooks'), PackageRef('code_assets'), PackageRef('native_toolchain_c'), PackageRef('crypto')],
devDependencies: [PackageRef('analyzer'), PackageRef('build_daemon'), PackageRef('build_runner'), PackageRef('dart_style'), PackageRef('http'), PackageRef('lints'), PackageRef('shelf'), PackageRef('shelf_static'), PackageRef('stream_channel'), PackageRef('test'), PackageRef('pub_semver'), PackageRef('convert'), PackageRef('package_config'), PackageRef('logging')],
license: '''MIT License
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.''',
);
/// sqlite3_flutter_libs 0.5.42
/// sqlite3_flutter_libs 0.6.0+eol
const _sqlite3_flutter_libs = Package(
name: 'sqlite3_flutter_libs',
description: 'Flutter plugin to include native sqlite3 libraries with your app',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/v2/sqlite3_flutter_libs',
description: 'Not used anymore, update to version 3.x of package:sqlite3 instead',
homepage: 'https://github.com/simolus3/sqlite3.dart/tree/main/legacy/sqlite3_flutter_libs',
authors: [],
version: '0.5.42',
version: '0.6.0+eol',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
dependencies: [PackageRef('flutter')],
dependencies: [],
devDependencies: [],
license: '''MIT License
@@ -37753,14 +37958,14 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// sqlparser 0.43.1
/// sqlparser 0.44.4
const _sqlparser = Package(
name: 'sqlparser',
description: 'Parses sqlite statements and performs static analysis on them',
homepage: 'https://github.com/simolus3/drift/tree/develop/sqlparser',
repository: 'https://github.com/simolus3/drift',
authors: [],
version: '0.43.1',
version: '0.44.4',
spdxIdentifiers: ['MIT'],
isMarkdown: false,
isSdk: false,
@@ -39415,12 +39620,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// tallee 0.0.33+273
/// tallee 0.0.35+277
const _tallee = Package(
name: 'tallee',
description: 'Tracking App for Card Games',
authors: [],
version: '0.0.33+273',
version: '0.0.35+277',
spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: 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

@@ -6,11 +6,13 @@ class AppSkeleton extends StatefulWidget {
/// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
/// - [alignment]: The alignment used for the custom layout builder and optional Align wrapper. Defaults to [Alignment.center].
const AppSkeleton({
super.key,
required this.child,
this.enabled = true,
this.fixLayoutBuilder = false,
this.alignment = Alignment.center,
});
/// The widget tree to apply the skeleton effect to.
@@ -22,6 +24,9 @@ class AppSkeleton extends StatefulWidget {
/// A boolean to fix the layout builder for AnimatedSwitcher.
final bool fixLayoutBuilder;
/// The alignment used for the custom layout builder and optional Align wrapper
final Alignment alignment;
@override
State<AppSkeleton> createState() => _AppSkeletonState();
}
@@ -45,13 +50,14 @@ class _AppSkeletonState extends State<AppSkeleton> {
layoutBuilder: !widget.fixLayoutBuilder
? AnimatedSwitcher.defaultLayoutBuilder
: (Widget? currentChild, List<Widget> previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [...previousChildren, ?currentChild],
);
final children = <Widget>[...previousChildren];
if (currentChild != null) children.add(currentChild);
return Stack(alignment: widget.alignment, children: children);
},
),
child: widget.child,
child: widget.fixLayoutBuilder
? Align(alignment: widget.alignment, child: widget.child)
: widget.child,
);
}
}

View File

@@ -11,7 +11,7 @@ class AnimatedDialogButton extends StatefulWidget {
const AnimatedDialogButton({
super.key,
required this.buttonText,
required this.onPressed,
this.onPressed,
this.buttonConstraints,
this.buttonType = ButtonType.primary,
this.isDescructive = false,
@@ -19,7 +19,7 @@ class AnimatedDialogButton extends StatefulWidget {
final String buttonText;
final VoidCallback onPressed;
final VoidCallback? onPressed;
final BoxConstraints? buttonConstraints;
@@ -38,28 +38,38 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
Widget build(BuildContext context) {
final textStyling = _getTextStyling();
final buttonDecoration = _getButtonDecoration();
bool isDisabled = widget.onPressed == null;
return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
onTap: widget.onPressed,
child: AnimatedScale(
scale: _isPressed ? 0.95 : 1.0,
duration: const Duration(milliseconds: 100),
child: AnimatedOpacity(
opacity: _isPressed ? 0.6 : 1.0,
duration: const Duration(milliseconds: 100),
child: Center(
child: Container(
constraints: widget.buttonConstraints,
decoration: buttonDecoration,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: const EdgeInsets.symmetric(vertical: 8),
child: Text(
widget.buttonText,
style: textStyling,
textAlign: TextAlign.center,
return IgnorePointer(
ignoring: isDisabled,
child: Opacity(
opacity: isDisabled ? 0.5 : 1.0,
child: GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
onTap: widget.onPressed,
child: AnimatedScale(
scale: _isPressed ? 0.95 : 1.0,
duration: const Duration(milliseconds: 100),
child: AnimatedOpacity(
opacity: _isPressed ? 0.6 : 1.0,
duration: const Duration(milliseconds: 100),
child: Center(
child: Container(
constraints: widget.buttonConstraints,
decoration: buttonDecoration,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
margin: const EdgeInsets.symmetric(vertical: 8),
child: Text(
widget.buttonText,
style: textStyling,
textAlign: TextAlign.center,
),
),
),
),
),

View File

@@ -19,7 +19,6 @@ class CustomAlertDialog extends StatelessWidget {
final String title;
final Widget content;
final List<CustomDialogAction> actions;
@override
Widget build(BuildContext context) {
return AlertDialog(

View File

@@ -10,7 +10,7 @@ class CustomDialogAction extends StatelessWidget {
/// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({
super.key,
required this.onPressed,
this.onPressed,
required this.text,
this.buttonType = ButtonType.primary,
this.isDestructive = false,
@@ -20,17 +20,18 @@ class CustomDialogAction extends StatelessWidget {
final ButtonType buttonType;
final VoidCallback onPressed;
final VoidCallback? onPressed;
final bool isDestructive;
@override
Widget build(BuildContext context) {
return AnimatedDialogButton(
onPressed: () async {
await HapticFeedback.selectionClick();
onPressed.call();
},
onPressed: onPressed != null
? () async {
await HapticFeedback.selectionClick();
onPressed?.call();
}
: null,
buttonText: text,
buttonType: buttonType,
isDescructive: isDestructive,

View File

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

View File

@@ -26,6 +26,7 @@ class PlayerSelection extends StatefulWidget {
this.availablePlayers,
this.initialSelectedPlayers,
required this.onChanged,
this.onPlayerCreated,
});
/// An optional list of players to choose from. If null, all players from the database are used.
@@ -37,6 +38,9 @@ class PlayerSelection extends StatefulWidget {
/// A callback function that is invoked whenever the selection changes,
final Function(List<Player> value) onChanged;
/// A callback function that is invoked when a player was created in this widget
final VoidCallback? onPlayerCreated;
@override
State<PlayerSelection> createState() => _PlayerSelectionState();
}
@@ -323,6 +327,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
/// Updates the state after successfully adding a new player.
void _handleSuccessfulPlayerCreation(Player player) {
widget.onPlayerCreated?.call();
selectedPlayers.insert(0, player);
widget.onChanged([...selectedPlayers]);
allPlayers.add(player);

View File

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

View File

@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatefulWidget {
@@ -15,6 +17,7 @@ class GroupTile extends StatefulWidget {
required this.group,
this.isHighlighted = false,
this.onTap,
this.onPlayerChanged,
});
/// The group data to be displayed.
@@ -26,6 +29,9 @@ class GroupTile extends StatefulWidget {
/// Callback function to be executed when the tile is tapped.
final VoidCallback? onTap;
/// Callback function to be executed when the players in the group are changed.
final VoidCallback? onPlayerChanged;
@override
State<GroupTile> createState() => _GroupTileState();
}
@@ -92,6 +98,19 @@ class _GroupTileState extends State<GroupTile> {
text: member.name,
suffixText: getNameCountText(member),
iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: member,
callback: () {
widget.onPlayerChanged?.call();
},
),
),
);
},
),
],
),

View File

@@ -3,11 +3,13 @@ import 'dart:core' hide Match;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/player_detail_view.dart';
import 'package:tallee/presentation/widgets/game_label.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -24,6 +26,7 @@ class MatchTile extends StatefulWidget {
required this.onTap,
this.width,
this.compact = false,
this.onPlayerEdited,
});
/// The match data to be displayed.
@@ -32,6 +35,9 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped.
final VoidCallback onTap;
/// The callback invoked when the players are edited
final VoidCallback? onPlayerEdited;
/// Optional width for the tile.
final double? width;
@@ -224,6 +230,19 @@ class _MatchTileState extends State<MatchTile> {
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
onTileTap: () {
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => PlayerDetailView(
player: player,
callback: () {
widget.onPlayerEdited?.call();
},
),
),
);
},
);
}).toList(),
),

View File

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

@@ -12,6 +12,7 @@ class TextIconTile extends StatelessWidget {
this.suffixText = '',
this.iconEnabled = true,
this.onIconTap,
this.onTileTap,
});
/// The text to display in the tile.
@@ -25,52 +26,58 @@ class TextIconTile extends StatelessWidget {
/// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap;
/// The callback to be invoked when the tile is tapped.
final VoidCallback? onTileTap;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
if (iconEnabled) const SizedBox(width: 3),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
return GestureDetector(
onTap: onTileTap,
child: Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: [
if (iconEnabled) const SizedBox(width: 3),
Flexible(
child: RichText(
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: DefaultTextStyle.of(context).style,
children: [
TextSpan(
text: text,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
TextSpan(
text: suffixText,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: CustomTheme.textColor.withAlpha(120),
TextSpan(
text: suffixText,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: CustomTheme.textColor.withAlpha(120),
),
),
),
],
],
),
),
),
),
if (iconEnabled) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
if (iconEnabled) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
],
],
],
),
),
);
}

View File

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

View File

@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "10.0.1"
animated_custom_dropdown:
dependency: "direct main"
description:
name: animated_custom_dropdown
sha256: "5a72dc209041bb53f6c7164bc2e366552d5197cdb032b1c9b2c36e3013024486"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
arb_utils:
dependency: "direct dev"
description:
@@ -353,6 +361,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.8"
dropdown_flutter:
dependency: "direct main"
description:
name: dropdown_flutter
sha256: "5ae3d05d768d0bb6030ff735e6b4b93f7b29be3cf3bec7c86cd4f444c8f067ff"
url: "https://pub.dev"
source: hosted
version: "1.0.3"
equatable:
dependency: transitive
description:

View File

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

View File

@@ -194,6 +194,31 @@ void main() {
expect(allGroups, isEmpty);
});
test('getGroupsByPlayer() works correctly', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup2],
);
final groups = await database.groupDao.getGroupsByPlayer(
playerId: testPlayer2.id,
);
expect(groups, hasLength(2));
expect(groups.any((group) => group.id == testGroup1.id), isTrue);
expect(groups.any((group) => group.id == testGroup2.id), isTrue);
});
test(
'getGroupsByPlayer() returns empty list for non-existent player',
() async {
final groups = await database.groupDao.getGroupsByPlayer(
playerId: 'non-existent-player-id',
);
expect(groups, isEmpty);
},
);
test('addGroupsAsList() with duplicate groups only adds once', () async {
await database.groupDao.addGroupsAsList(
groups: [testGroup1, testGroup1, testGroup1],

View File

@@ -56,7 +56,7 @@ void main() {
name: 'Test Game',
ruleset: Ruleset.singleWinner,
description: 'A test game',
color: GameColor.blue,
color: AppColor.blue,
icon: '',
);
testMatch1 = Match(
@@ -260,6 +260,34 @@ void main() {
expect(match.group!.id, testGroup1.id);
});
test('getMatchesByPlayer() works correctly', () async {
await database.matchDao.addMatchesAsList(
matches: [testMatch1, testMatch2],
);
final matches = await database.matchDao.getMatchesByPlayer(
playerId: testPlayer1.id,
);
expect(matches, hasLength(1));
expect(matches.first.id, testMatch2.id);
expect(
matches.first.players.any((p) => p.id == testPlayer1.id),
isTrue,
);
});
test(
'getMatchesByPlayer() returns empty list for non-existent player',
() async {
final matches = await database.matchDao.getMatchesByPlayer(
playerId: 'non-existing-player-id',
);
expect(matches, isEmpty);
},
);
test('getMatchCount() works correctly', () async {
var count = await database.matchDao.getMatchCount();
expect(count, 0);

View File

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

View File

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

View File

@@ -233,6 +233,95 @@ void main() {
expect(allPlayers, isEmpty);
});
test('updatePlayerName() sets correct nameCount with 2 player', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
final newName = testPlayer1.name;
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: newName,
);
var player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 2);
await database.playerDao.updatePlayerName(
playerId: testPlayer1.id,
name: 'different name',
);
player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 0);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 0);
});
test('updatePlayerName() sets correct nameCount with 3 player', () async {
await database.playerDao.addPlayersAsList(
players: [testPlayer1, testPlayer2, testPlayer3],
);
// Changing both names to player 1's name
final newName = testPlayer1.name;
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: newName,
);
await database.playerDao.updatePlayerName(
playerId: testPlayer3.id,
name: newName,
);
var player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 2);
player = await database.playerDao.getPlayerById(
playerId: testPlayer3.id,
);
expect(player.nameCount, 3);
// Changing the middle players name
await database.playerDao.updatePlayerName(
playerId: testPlayer2.id,
name: 'different name',
);
player = await database.playerDao.getPlayerById(
playerId: testPlayer1.id,
);
expect(player.nameCount, 1);
player = await database.playerDao.getPlayerById(
playerId: testPlayer2.id,
);
expect(player.nameCount, 0);
player = await database.playerDao.getPlayerById(
playerId: testPlayer3.id,
);
expect(player.nameCount, 2);
});
test('updatePlayerDescription() works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
@@ -372,14 +461,22 @@ void main() {
final player1 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player1);
final player2 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayer(player: player2);
var players = await database.playerDao.getAllPlayers();
expect(players.length, 2);
expect(players.length, 3);
players.sort((a, b) => a.nameCount.compareTo(b.nameCount));
for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1);
}
// ids are correct in the right order
expect(players[0].id, testPlayer1.id);
expect(players[1].id, player1.id);
expect(players[2].id, player2.id);
},
);
@@ -404,24 +501,62 @@ void main() {
for (int i = 0; i < players.length - 1; i++) {
expect(players[i].nameCount, i + 1);
}
// ids are correct in the right order
expect(players[0].id, testPlayer1.id);
expect(players[1].id, player1.id);
expect(players[2].id, player2.id);
expect(players[3].id, player3.id);
},
);
test('getNameCount works correctly', () async {
test('getNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name);
final player3 = Player(name: testPlayer1.name);
await database.playerDao.addPlayersAsList(
players: [testPlayer1, player2, player3],
await database.playerDao.addPlayer(player: testPlayer1);
var nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name,
);
final nameCount = await database.playerDao.getNameCount(
expect(nameCount, 1);
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.getNameCount(
name: testPlayer1.name,
);
expect(nameCount, 3);
});
test('calculateNameCount works correctly', () async {
final player1 = Player(name: testPlayer1.name);
final player2 = Player(name: testPlayer1.name);
// Case 1: No existing players with the name
var nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 0);
// Case 2: One existing player with the name. Should return 2 for
// the new player
await database.playerDao.addPlayer(player: testPlayer1);
nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 2);
// Case 3: Multiple existing players with the name. Should return count + 1
await database.playerDao.addPlayersAsList(players: [player1, player2]);
nameCount = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(nameCount, 4);
});
test('updateNameCount works correctly', () async {
await database.playerDao.addPlayer(player: testPlayer1);
@@ -441,14 +576,24 @@ void main() {
final player2 = Player(name: testPlayer1.name, description: '');
final player3 = Player(name: testPlayer1.name, description: '');
await database.playerDao.addPlayersAsList(
players: [testPlayer1, player2, player3],
);
final player = await database.playerDao.getPlayerWithHighestNameCount(
await database.playerDao.addPlayer(player: testPlayer1);
var player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name,
);
expect(player, isNotNull);
expect(player!.nameCount, 0);
await database.playerDao.addPlayer(player: player2);
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name,
);
expect(player, isNotNull);
expect(player!.nameCount, 2);
await database.playerDao.addPlayer(player: player3);
player = await database.playerDao.getPlayerWithHighestNameCount(
name: testPlayer1.name,
);
expect(player, isNotNull);
expect(player!.nameCount, 3);
});
@@ -460,32 +605,6 @@ void main() {
expect(player, isNull);
});
test('calculateNameCount works correctly', () async {
// Case 1: No existing players with the name
var count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 0);
// Case 2: One existing player with the name. Should update that
// player's nameCount to 1 and return 2 for the new player
await database.playerDao.addPlayer(player: testPlayer1);
count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 2);
// Case 3: Multiple existing players with the name.
final player2 = Player(name: testPlayer1.name, nameCount: count);
await database.playerDao.addPlayer(player: player2);
count = await database.playerDao.calculateNameCount(
name: testPlayer1.name,
);
expect(count, 3);
});
test('getPlayerWithHighestNameCount with non existing player', () async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.initializeNameCount(name: testPlayer1.name);

View File

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

View File

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