57 Commits

Author SHA1 Message Date
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
60 changed files with 1160 additions and 3260 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

@@ -166,9 +166,6 @@
"notes": {
"type": "string"
},
"isTeamMatch": {
"type": "boolean"
},
"teams": {
"type": ["array", "null"]
}
@@ -180,8 +177,7 @@
"createdAt",
"gameId",
"playerIds",
"notes",
"isTeamMatch"
"notes"
]
}
}

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,62 +24,47 @@ String translateRulesetToString(Ruleset ruleset, BuildContext context) {
}
}
// Returns a AppColor enum value based on the provided team [index].
AppColor getTeamColor(int index) {
final colors = [
AppColor.red,
AppColor.blue,
AppColor.green,
AppColor.yellow,
AppColor.purple,
AppColor.orange,
AppColor.pink,
AppColor.teal,
];
return colors[index % colors.length];
}
/// Translates a [AppColor] enum value to its corresponding localized string.
String translateGameColorToString(AppColor color, BuildContext context) {
/// Translates a [GameColor] enum value to its corresponding localized string.
String translateGameColorToString(GameColor color, BuildContext context) {
final loc = AppLocalizations.of(context);
switch (color) {
case AppColor.red:
case GameColor.red:
return loc.color_red;
case AppColor.blue:
case GameColor.blue:
return loc.color_blue;
case AppColor.green:
case GameColor.green:
return loc.color_green;
case AppColor.yellow:
case GameColor.yellow:
return loc.color_yellow;
case AppColor.purple:
case GameColor.purple:
return loc.color_purple;
case AppColor.orange:
case GameColor.orange:
return loc.color_orange;
case AppColor.pink:
case GameColor.pink:
return loc.color_pink;
case AppColor.teal:
case GameColor.teal:
return loc.color_teal;
}
}
/// Returns the [Color] object corresponding to a [AppColor] enum value.
Color getColorFromGameColor(AppColor color) {
/// Returns the [Color] object corresponding to a [GameColor] enum value.
Color getColorFromGameColor(GameColor color) {
switch (color) {
case AppColor.red:
case GameColor.red:
return Colors.red;
case AppColor.blue:
case GameColor.blue:
return Colors.blue;
case AppColor.green:
case GameColor.green:
return Colors.green;
case AppColor.yellow:
case GameColor.yellow:
return const Color(0xFFF7CA28);
case AppColor.purple:
case GameColor.purple:
return Colors.purple;
case AppColor.orange:
case GameColor.orange:
return const Color(0xFFef681f);
case AppColor.pink:
case GameColor.pink:
return Colors.pink;
case AppColor.teal:
case GameColor.teal:
return Colors.teal;
}
}
@@ -92,10 +77,11 @@ IconData getRulesetIcon(Ruleset ruleset) {
case Ruleset.lowestScore:
return Icons.arrow_downward;
case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icons.emoji_events;
case Ruleset.singleLoser:
return Icons.sentiment_dissatisfied;
case Ruleset.multipleWinners:
return Icons.group;
case Ruleset.placement:
return RpgAwesome.podium;
}
@@ -127,7 +113,6 @@ String getExtraPlayerCount(Match match) {
return ' + ${count.toString()}';
}
/// Returns the player name count if greater 0 in the format " #2", otherwise an empty string
String getNameCountText(Player player) {
if (player.nameCount >= 1) {
return ' #${player.nameCount}';
@@ -135,7 +120,6 @@ String getNameCountText(Player player) {
return '';
}
/// Returns the correct singular or plural form of "point(s)" based on the [points] value.
String getPointLabel(AppLocalizations loc, int points) {
if (points == 1) {
return '$points ${loc.point}';

View File

@@ -65,11 +65,7 @@ class CustomTheme {
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
color: boxColor,
border: Border.all(
color: textColor,
width: 2,
strokeAlign: BorderSide.strokeAlignCenter,
),
border: Border.all(color: textColor, width: 2),
borderRadius: standardBorderRadiusAll,
);

View File

@@ -42,5 +42,5 @@ enum Ruleset {
singleLoser,
}
/// Different colors for highlighting content
enum AppColor { red, orange, yellow, green, teal, blue, purple, pink }
/// Different colors for highlighting games
enum GameColor { red, orange, yellow, green, teal, blue, purple, pink }

View File

@@ -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: AppColor.values.firstWhere((e) => e.name == row.color),
color: GameColor.values.firstWhere((e) => e.name == row.color),
icon: row.icon,
createdAt: row.createdAt,
),
@@ -109,7 +109,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
name: result.name,
ruleset: Ruleset.values.firstWhere((e) => e.name == result.ruleset),
description: result.description,
color: AppColor.values.firstWhere((e) => e.name == result.color),
color: GameColor.values.firstWhere((e) => e.name == result.color),
icon: result.icon,
createdAt: result.createdAt,
);
@@ -156,7 +156,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
/// Updates the color of the game with the given [gameId].
Future<bool> updateGameColor({
required String gameId,
required AppColor color,
required GameColor color,
}) async {
final rowsAffected =
await (update(gameTable)..where((g) => g.id.equals(gameId))).write(

View File

@@ -30,7 +30,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
gameId: match.game.id,
groupId: Value(match.group?.id),
name: match.name,
isTeamMatch: Value(match.isTeamMatch),
notes: match.notes,
createdAt: match.createdAt,
endedAt: Value(match.endedAt),
@@ -143,7 +142,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
gameId: match.game.id,
groupId: Value(match.group?.id),
name: match.name,
isTeamMatch: Value(match.isTeamMatch),
notes: match.notes,
createdAt: match.createdAt,
endedAt: Value(match.endedAt),
@@ -302,7 +300,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group,
players: players,
teams: teams.isEmpty ? null : teams,
isTeamMatch: row.isTeamMatch,
notes: row.notes,
createdAt: row.createdAt,
endedAt: row.endedAt,
@@ -337,7 +334,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group,
players: players,
teams: teams.isEmpty ? null : teams,
isTeamMatch: result.isTeamMatch,
notes: result.notes,
createdAt: result.createdAt,
endedAt: result.endedAt,
@@ -377,7 +373,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
group: group,
players: players,
teams: teams.isEmpty ? null : teams,
isTeamMatch: row.isTeamMatch,
notes: row.notes,
createdAt: row.createdAt,
endedAt: row.endedAt,
@@ -406,8 +401,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
teamIds.map((teamId) => db.teamDao.getTeamById(teamId: teamId)),
);
return teams
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return teams;
}
/* Update */

View File

@@ -74,8 +74,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
(row) => db.playerDao.getPlayerById(playerId: row.playerId),
);
final players = await Future.wait(futures);
return players
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return players;
}
/// Retrieves a list of [Player]s associated with a specific team in a match.

View File

@@ -16,12 +16,12 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
/* Create */
/// Adds a score entry to the database.
Future<bool> addScore({
Future<void> addScore({
required String playerId,
required String matchId,
required ScoreEntry entry,
}) async {
final rowsAffected = await into(scoreEntryTable).insert(
await into(scoreEntryTable).insert(
ScoreEntryTableCompanion.insert(
playerId: playerId,
matchId: matchId,
@@ -31,8 +31,6 @@ class ScoreEntryDao extends DatabaseAccessor<AppDatabase>
),
mode: InsertMode.insertOrReplace,
);
return rowsAffected > 0;
}
Future<void> addScoresAsList({

View File

@@ -1,10 +1,8 @@
import 'package:drift/drift.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/db/tables/player_match_table.dart';
import 'package:tallee/data/db/tables/team_table.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/data/models/team.dart';
part 'team_dao.g.dart';
@@ -24,8 +22,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: team.id,
name: team.name,
createdAt: team.createdAt,
color: Value(team.color.name),
score: Value(team.score),
),
mode: InsertMode.insertOrReplace,
);
@@ -60,8 +56,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: team.id,
name: team.name,
createdAt: team.createdAt,
color: Value(team.color.name),
score: Value(team.score),
),
)
.toList(),
@@ -116,32 +110,12 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: row.id,
name: row.name,
createdAt: row.createdAt,
color: AppColor.values.byName(row.color),
score: row.score,
members: members,
);
}),
);
}
Future<List<Team>> getTeamsByMatchId({required String matchId}) async {
final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.matchId.equals(matchId));
final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return [];
final teamIds = playerMatches
.map((pm) => pm.teamId)
.whereType<String>()
.toSet();
final teams = await Future.wait(
teamIds.map((id) => getTeamById(teamId: id)),
);
return teams;
}
/// Retrieves a [Team] by its [teamId], including its members.
Future<Team> getTeamById({required String teamId}) async {
final query = select(teamTable)..where((t) => t.id.equals(teamId));
@@ -151,8 +125,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
id: result.id,
name: result.name,
createdAt: result.createdAt,
color: AppColor.values.byName(result.color),
score: result.score,
members: members,
);
}
@@ -173,8 +145,7 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
final players = await Future.wait(
playerIds.map((id) => db.playerDao.getPlayerById(playerId: id)),
);
return players
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
return players;
}
/* Update */
@@ -191,81 +162,6 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
return rowsAffected > 0;
}
/// Updates the color of the team with the given [teamId].
Future<bool> updateTeamColor({
required String teamId,
required AppColor color,
}) async {
final rowsAffected =
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
TeamTableCompanion(color: Value(color.name)),
);
return rowsAffected > 0;
}
/// Updates the score of the team with the given [teamId].
/// Updates the member scores correspondingly
Future<bool> updateTeamScore({
required String teamId,
required String matchId,
required int score,
}) async {
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
const TeamTableCompanion(score: Value(null)),
);
await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId);
final rowsAffected =
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
TeamTableCompanion(score: Value(score)),
);
final members = await _getTeamMembers(teamId: teamId);
for (final member in members) {
await db.scoreEntryDao.addScore(
playerId: member.id,
matchId: matchId,
entry: ScoreEntry(score: score),
);
}
return rowsAffected > 0;
}
Future<bool> removeScoreForTeam({
required String teamId,
required String matchId,
}) async {
await (update(teamTable)..where((t) => t.id.equals(teamId))).write(
const TeamTableCompanion(score: Value(null)),
);
await _deleteAllScoresForMembersOfTeam(teamId: teamId, matchId: matchId);
return true;
}
/// Removes the scores for all teams in the match with the given [matchId] by setting their scores to null.
Future<bool> removeAllTeamScores({required String matchId}) async {
// collect all teamIds for the given matchId from playerMatchTable
final teamIds =
await (selectOnly(playerMatchTable)
..addColumns([playerMatchTable.teamId])
..where(playerMatchTable.matchId.equals(matchId)))
.map((row) => row.read(playerMatchTable.teamId))
.get();
// filter null or duplicates
final filteredTeamIds = teamIds.whereType<String>().toSet().toList();
var rowsAffected = 0;
if (filteredTeamIds.isNotEmpty) {
rowsAffected =
await (update(teamTable)..where((t) => t.id.isIn(filteredTeamIds)))
.write(const TeamTableCompanion(score: Value(null)));
}
await db.scoreEntryDao.deleteAllScoresForMatch(matchId: matchId);
return rowsAffected > 0;
}
/* Delete */
/// Deletes all teams from the database.
@@ -283,92 +179,4 @@ class TeamDao extends DatabaseAccessor<AppDatabase> with _$TeamDaoMixin {
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/* Score handling */
/// Sets the team with the given [teamId] as the winner of the match with the given [matchId] by assigning a score of 1.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> setWinnerTeam({
required String teamId,
required String matchId,
}) async {
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 1);
}
/// Sets multiple teams as winners of the match with the given [matchId] by assigning a score of 1 to each team.
/// Returns `true` if all scores were updated successfully, `false` otherwise.
Future<bool> setWinnerTeams({
required List<Team> winners,
required String matchId,
}) async {
// Reset all team scores .
await removeAllTeamScores(matchId: matchId);
// Reset all score entries
for (final team in winners) {
await _deleteAllScoresForMembersOfTeam(teamId: team.id, matchId: matchId);
}
for (final team in winners) {
await updateTeamScore(teamId: team.id, matchId: matchId, score: 1);
}
return true;
}
/// Removes the winner status from all Teams with the given [matchId] by setting its score to null.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> removeWinnerTeam({required String matchId}) async {
return await removeAllTeamScores(matchId: matchId);
}
/// Sets the team with the given [teamId] as the loser of the match with the given [matchId] by assigning a score of 0.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> setLoserTeam({
required String teamId,
required String matchId,
}) async {
return await updateTeamScore(teamId: teamId, matchId: matchId, score: 0);
}
/// Removes the loser from the match with the given [matchId] by setting its score to null.
/// Returns `true` if the score was updated successfully, `false` otherwise.
Future<bool> removeLoserTeam({required String matchId}) async {
return await removeAllTeamScores(matchId: matchId);
}
/// Sets the placements for the teams in the match with the given [matchId] by assigning scores based on their order in the [teams] list.
/// Returns `true` if all scores were updated successfully, `false` otherwise.
Future<bool> setTeamPlacements({
required String matchId,
required List<Team> teams,
}) async {
List<bool?> success = List.generate(teams.length, (index) => null);
for (int i = 0; i < teams.length; i++) {
success[i] = await updateTeamScore(
matchId: matchId,
teamId: teams[i].id,
score: teams.length - i,
);
}
return success.every((result) => result == true);
}
/// Helper method to delete all scores for members of a team in a specific match.
Future<bool> _deleteAllScoresForMembersOfTeam({
required String teamId,
required String matchId,
}) async {
final playerMatchQuery = select(db.playerMatchTable)
..where((pm) => pm.teamId.equals(teamId) & pm.matchId.equals(matchId));
final playerMatches = await playerMatchQuery.get();
if (playerMatches.isEmpty) return false;
for (final pm in playerMatches) {
await db.scoreEntryDao.deleteAllScoresForPlayerInMatch(
playerId: pm.playerId,
matchId: matchId,
);
}
return true;
}
}

View File

@@ -1185,21 +1185,6 @@ class $MatchTableTable extends MatchTable
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _isTeamMatchMeta = const VerificationMeta(
'isTeamMatch',
);
@override
late final GeneratedColumn<bool> isTeamMatch = GeneratedColumn<bool>(
'is_team_match',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("is_team_match" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _notesMeta = const VerificationMeta('notes');
@override
late final GeneratedColumn<String> notes = GeneratedColumn<String>(
@@ -1237,7 +1222,6 @@ class $MatchTableTable extends MatchTable
gameId,
groupId,
name,
isTeamMatch,
notes,
createdAt,
endedAt,
@@ -1281,15 +1265,6 @@ class $MatchTableTable extends MatchTable
} else if (isInserting) {
context.missing(_nameMeta);
}
if (data.containsKey('is_team_match')) {
context.handle(
_isTeamMatchMeta,
isTeamMatch.isAcceptableOrUnknown(
data['is_team_match']!,
_isTeamMatchMeta,
),
);
}
if (data.containsKey('notes')) {
context.handle(
_notesMeta,
@@ -1337,10 +1312,6 @@ class $MatchTableTable extends MatchTable
DriftSqlType.string,
data['${effectivePrefix}name'],
)!,
isTeamMatch: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}is_team_match'],
)!,
notes: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}notes'],
@@ -1367,7 +1338,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
final String gameId;
final String? groupId;
final String name;
final bool isTeamMatch;
final String notes;
final DateTime createdAt;
final DateTime? endedAt;
@@ -1376,7 +1346,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
required this.gameId,
this.groupId,
required this.name,
required this.isTeamMatch,
required this.notes,
required this.createdAt,
this.endedAt,
@@ -1390,7 +1359,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
map['group_id'] = Variable<String>(groupId);
}
map['name'] = Variable<String>(name);
map['is_team_match'] = Variable<bool>(isTeamMatch);
map['notes'] = Variable<String>(notes);
map['created_at'] = Variable<DateTime>(createdAt);
if (!nullToAbsent || endedAt != null) {
@@ -1407,7 +1375,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
? const Value.absent()
: Value(groupId),
name: Value(name),
isTeamMatch: Value(isTeamMatch),
notes: Value(notes),
createdAt: Value(createdAt),
endedAt: endedAt == null && nullToAbsent
@@ -1426,7 +1393,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: serializer.fromJson<String>(json['gameId']),
groupId: serializer.fromJson<String?>(json['groupId']),
name: serializer.fromJson<String>(json['name']),
isTeamMatch: serializer.fromJson<bool>(json['isTeamMatch']),
notes: serializer.fromJson<String>(json['notes']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
endedAt: serializer.fromJson<DateTime?>(json['endedAt']),
@@ -1440,7 +1406,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
'gameId': serializer.toJson<String>(gameId),
'groupId': serializer.toJson<String?>(groupId),
'name': serializer.toJson<String>(name),
'isTeamMatch': serializer.toJson<bool>(isTeamMatch),
'notes': serializer.toJson<String>(notes),
'createdAt': serializer.toJson<DateTime>(createdAt),
'endedAt': serializer.toJson<DateTime?>(endedAt),
@@ -1452,7 +1417,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
String? gameId,
Value<String?> groupId = const Value.absent(),
String? name,
bool? isTeamMatch,
String? notes,
DateTime? createdAt,
Value<DateTime?> endedAt = const Value.absent(),
@@ -1461,7 +1425,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: gameId ?? this.gameId,
groupId: groupId.present ? groupId.value : this.groupId,
name: name ?? this.name,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
endedAt: endedAt.present ? endedAt.value : this.endedAt,
@@ -1472,9 +1435,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
gameId: data.gameId.present ? data.gameId.value : this.gameId,
groupId: data.groupId.present ? data.groupId.value : this.groupId,
name: data.name.present ? data.name.value : this.name,
isTeamMatch: data.isTeamMatch.present
? data.isTeamMatch.value
: this.isTeamMatch,
notes: data.notes.present ? data.notes.value : this.notes,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
endedAt: data.endedAt.present ? data.endedAt.value : this.endedAt,
@@ -1488,7 +1448,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
..write('gameId: $gameId, ')
..write('groupId: $groupId, ')
..write('name: $name, ')
..write('isTeamMatch: $isTeamMatch, ')
..write('notes: $notes, ')
..write('createdAt: $createdAt, ')
..write('endedAt: $endedAt')
@@ -1497,16 +1456,8 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
}
@override
int get hashCode => Object.hash(
id,
gameId,
groupId,
name,
isTeamMatch,
notes,
createdAt,
endedAt,
);
int get hashCode =>
Object.hash(id, gameId, groupId, name, notes, createdAt, endedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -1515,7 +1466,6 @@ class MatchTableData extends DataClass implements Insertable<MatchTableData> {
other.gameId == this.gameId &&
other.groupId == this.groupId &&
other.name == this.name &&
other.isTeamMatch == this.isTeamMatch &&
other.notes == this.notes &&
other.createdAt == this.createdAt &&
other.endedAt == this.endedAt);
@@ -1526,7 +1476,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
final Value<String> gameId;
final Value<String?> groupId;
final Value<String> name;
final Value<bool> isTeamMatch;
final Value<String> notes;
final Value<DateTime> createdAt;
final Value<DateTime?> endedAt;
@@ -1536,7 +1485,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
this.gameId = const Value.absent(),
this.groupId = const Value.absent(),
this.name = const Value.absent(),
this.isTeamMatch = const Value.absent(),
this.notes = const Value.absent(),
this.createdAt = const Value.absent(),
this.endedAt = const Value.absent(),
@@ -1547,7 +1495,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
required String gameId,
this.groupId = const Value.absent(),
required String name,
this.isTeamMatch = const Value.absent(),
required String notes,
required DateTime createdAt,
this.endedAt = const Value.absent(),
@@ -1562,7 +1509,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
Expression<String>? gameId,
Expression<String>? groupId,
Expression<String>? name,
Expression<bool>? isTeamMatch,
Expression<String>? notes,
Expression<DateTime>? createdAt,
Expression<DateTime>? endedAt,
@@ -1573,7 +1519,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
if (gameId != null) 'game_id': gameId,
if (groupId != null) 'group_id': groupId,
if (name != null) 'name': name,
if (isTeamMatch != null) 'is_team_match': isTeamMatch,
if (notes != null) 'notes': notes,
if (createdAt != null) 'created_at': createdAt,
if (endedAt != null) 'ended_at': endedAt,
@@ -1586,7 +1531,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
Value<String>? gameId,
Value<String?>? groupId,
Value<String>? name,
Value<bool>? isTeamMatch,
Value<String>? notes,
Value<DateTime>? createdAt,
Value<DateTime?>? endedAt,
@@ -1597,7 +1541,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
gameId: gameId ?? this.gameId,
groupId: groupId ?? this.groupId,
name: name ?? this.name,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
notes: notes ?? this.notes,
createdAt: createdAt ?? this.createdAt,
endedAt: endedAt ?? this.endedAt,
@@ -1620,9 +1563,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
if (name.present) {
map['name'] = Variable<String>(name.value);
}
if (isTeamMatch.present) {
map['is_team_match'] = Variable<bool>(isTeamMatch.value);
}
if (notes.present) {
map['notes'] = Variable<String>(notes.value);
}
@@ -1645,7 +1585,6 @@ class MatchTableCompanion extends UpdateCompanion<MatchTableData> {
..write('gameId: $gameId, ')
..write('groupId: $groupId, ')
..write('name: $name, ')
..write('isTeamMatch: $isTeamMatch, ')
..write('notes: $notes, ')
..write('createdAt: $createdAt, ')
..write('endedAt: $endedAt, ')
@@ -1915,27 +1854,8 @@ class $TeamTableTable extends TeamTable
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
);
static const VerificationMeta _colorMeta = const VerificationMeta('color');
@override
late final GeneratedColumn<String> color = GeneratedColumn<String>(
'color',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant('blue'),
);
static const VerificationMeta _scoreMeta = const VerificationMeta('score');
@override
late final GeneratedColumn<int> score = GeneratedColumn<int>(
'score',
aliasedName,
true,
type: DriftSqlType.int,
requiredDuringInsert: false,
);
@override
List<GeneratedColumn> get $columns => [id, name, createdAt, color, score];
List<GeneratedColumn> get $columns => [id, name, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -1969,18 +1889,6 @@ class $TeamTableTable extends TeamTable
} else if (isInserting) {
context.missing(_createdAtMeta);
}
if (data.containsKey('color')) {
context.handle(
_colorMeta,
color.isAcceptableOrUnknown(data['color']!, _colorMeta),
);
}
if (data.containsKey('score')) {
context.handle(
_scoreMeta,
score.isAcceptableOrUnknown(data['score']!, _scoreMeta),
);
}
return context;
}
@@ -2002,14 +1910,6 @@ class $TeamTableTable extends TeamTable
DriftSqlType.dateTime,
data['${effectivePrefix}created_at'],
)!,
color: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}color'],
)!,
score: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}score'],
),
);
}
@@ -2023,14 +1923,10 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
final String id;
final String name;
final DateTime createdAt;
final String color;
final int? score;
const TeamTableData({
required this.id,
required this.name,
required this.createdAt,
required this.color,
this.score,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
@@ -2038,10 +1934,6 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
map['id'] = Variable<String>(id);
map['name'] = Variable<String>(name);
map['created_at'] = Variable<DateTime>(createdAt);
map['color'] = Variable<String>(color);
if (!nullToAbsent || score != null) {
map['score'] = Variable<int>(score);
}
return map;
}
@@ -2050,10 +1942,6 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
id: Value(id),
name: Value(name),
createdAt: Value(createdAt),
color: Value(color),
score: score == null && nullToAbsent
? const Value.absent()
: Value(score),
);
}
@@ -2066,8 +1954,6 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String>(json['name']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
color: serializer.fromJson<String>(json['color']),
score: serializer.fromJson<int?>(json['score']),
);
}
@override
@@ -2077,31 +1963,20 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String>(name),
'createdAt': serializer.toJson<DateTime>(createdAt),
'color': serializer.toJson<String>(color),
'score': serializer.toJson<int?>(score),
};
}
TeamTableData copyWith({
String? id,
String? name,
DateTime? createdAt,
String? color,
Value<int?> score = const Value.absent(),
}) => TeamTableData(
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score.present ? score.value : this.score,
);
TeamTableData copyWith({String? id, String? name, DateTime? createdAt}) =>
TeamTableData(
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
);
TeamTableData copyWithCompanion(TeamTableCompanion data) {
return TeamTableData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
color: data.color.present ? data.color.value : this.color,
score: data.score.present ? data.score.value : this.score,
);
}
@@ -2110,47 +1985,37 @@ class TeamTableData extends DataClass implements Insertable<TeamTableData> {
return (StringBuffer('TeamTableData(')
..write('id: $id, ')
..write('name: $name, ')
..write('createdAt: $createdAt, ')
..write('color: $color, ')
..write('score: $score')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, name, createdAt, color, score);
int get hashCode => Object.hash(id, name, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TeamTableData &&
other.id == this.id &&
other.name == this.name &&
other.createdAt == this.createdAt &&
other.color == this.color &&
other.score == this.score);
other.createdAt == this.createdAt);
}
class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
final Value<String> id;
final Value<String> name;
final Value<DateTime> createdAt;
final Value<String> color;
final Value<int?> score;
final Value<int> rowid;
const TeamTableCompanion({
this.id = const Value.absent(),
this.name = const Value.absent(),
this.createdAt = const Value.absent(),
this.color = const Value.absent(),
this.score = const Value.absent(),
this.rowid = const Value.absent(),
});
TeamTableCompanion.insert({
required String id,
required String name,
required DateTime createdAt,
this.color = const Value.absent(),
this.score = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
name = Value(name),
@@ -2159,16 +2024,12 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
Expression<String>? id,
Expression<String>? name,
Expression<DateTime>? createdAt,
Expression<String>? color,
Expression<int>? score,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (name != null) 'name': name,
if (createdAt != null) 'created_at': createdAt,
if (color != null) 'color': color,
if (score != null) 'score': score,
if (rowid != null) 'rowid': rowid,
});
}
@@ -2177,16 +2038,12 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
Value<String>? id,
Value<String>? name,
Value<DateTime>? createdAt,
Value<String>? color,
Value<int?>? score,
Value<int>? rowid,
}) {
return TeamTableCompanion(
id: id ?? this.id,
name: name ?? this.name,
createdAt: createdAt ?? this.createdAt,
color: color ?? this.color,
score: score ?? this.score,
rowid: rowid ?? this.rowid,
);
}
@@ -2203,12 +2060,6 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
if (color.present) {
map['color'] = Variable<String>(color.value);
}
if (score.present) {
map['score'] = Variable<int>(score.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
@@ -2221,8 +2072,6 @@ class TeamTableCompanion extends UpdateCompanion<TeamTableData> {
..write('id: $id, ')
..write('name: $name, ')
..write('createdAt: $createdAt, ')
..write('color: $color, ')
..write('score: $score, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@@ -4243,7 +4092,6 @@ typedef $$MatchTableTableCreateCompanionBuilder =
required String gameId,
Value<String?> groupId,
required String name,
Value<bool> isTeamMatch,
required String notes,
required DateTime createdAt,
Value<DateTime?> endedAt,
@@ -4255,7 +4103,6 @@ typedef $$MatchTableTableUpdateCompanionBuilder =
Value<String> gameId,
Value<String?> groupId,
Value<String> name,
Value<bool> isTeamMatch,
Value<String> notes,
Value<DateTime> createdAt,
Value<DateTime?> endedAt,
@@ -4368,11 +4215,6 @@ class $$MatchTableTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get notes => $composableBuilder(
column: $table.notes,
builder: (column) => ColumnFilters(column),
@@ -4504,11 +4346,6 @@ class $$MatchTableTableOrderingComposer
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get notes => $composableBuilder(
column: $table.notes,
builder: (column) => ColumnOrderings(column),
@@ -4586,11 +4423,6 @@ class $$MatchTableTableAnnotationComposer
GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumn<bool> get isTeamMatch => $composableBuilder(
column: $table.isTeamMatch,
builder: (column) => column,
);
GeneratedColumn<String> get notes =>
$composableBuilder(column: $table.notes, builder: (column) => column);
@@ -4734,7 +4566,6 @@ class $$MatchTableTableTableManager
Value<String> gameId = const Value.absent(),
Value<String?> groupId = const Value.absent(),
Value<String> name = const Value.absent(),
Value<bool> isTeamMatch = const Value.absent(),
Value<String> notes = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<DateTime?> endedAt = const Value.absent(),
@@ -4744,7 +4575,6 @@ class $$MatchTableTableTableManager
gameId: gameId,
groupId: groupId,
name: name,
isTeamMatch: isTeamMatch,
notes: notes,
createdAt: createdAt,
endedAt: endedAt,
@@ -4756,7 +4586,6 @@ class $$MatchTableTableTableManager
required String gameId,
Value<String?> groupId = const Value.absent(),
required String name,
Value<bool> isTeamMatch = const Value.absent(),
required String notes,
required DateTime createdAt,
Value<DateTime?> endedAt = const Value.absent(),
@@ -4766,7 +4595,6 @@ class $$MatchTableTableTableManager
gameId: gameId,
groupId: groupId,
name: name,
isTeamMatch: isTeamMatch,
notes: notes,
createdAt: createdAt,
endedAt: endedAt,
@@ -5281,8 +5109,6 @@ typedef $$TeamTableTableCreateCompanionBuilder =
required String id,
required String name,
required DateTime createdAt,
Value<String> color,
Value<int?> score,
Value<int> rowid,
});
typedef $$TeamTableTableUpdateCompanionBuilder =
@@ -5290,8 +5116,6 @@ typedef $$TeamTableTableUpdateCompanionBuilder =
Value<String> id,
Value<String> name,
Value<DateTime> createdAt,
Value<String> color,
Value<int?> score,
Value<int> rowid,
});
@@ -5347,16 +5171,6 @@ class $$TeamTableTableFilterComposer
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get color => $composableBuilder(
column: $table.color,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get score => $composableBuilder(
column: $table.score,
builder: (column) => ColumnFilters(column),
);
Expression<bool> playerMatchTableRefs(
Expression<bool> Function($$PlayerMatchTableTableFilterComposer f) f,
) {
@@ -5406,16 +5220,6 @@ class $$TeamTableTableOrderingComposer
column: $table.createdAt,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get color => $composableBuilder(
column: $table.color,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get score => $composableBuilder(
column: $table.score,
builder: (column) => ColumnOrderings(column),
);
}
class $$TeamTableTableAnnotationComposer
@@ -5436,12 +5240,6 @@ class $$TeamTableTableAnnotationComposer
GeneratedColumn<DateTime> get createdAt =>
$composableBuilder(column: $table.createdAt, builder: (column) => column);
GeneratedColumn<String> get color =>
$composableBuilder(column: $table.color, builder: (column) => column);
GeneratedColumn<int> get score =>
$composableBuilder(column: $table.score, builder: (column) => column);
Expression<T> playerMatchTableRefs<T extends Object>(
Expression<T> Function($$PlayerMatchTableTableAnnotationComposer a) f,
) {
@@ -5499,15 +5297,11 @@ class $$TeamTableTableTableManager
Value<String> id = const Value.absent(),
Value<String> name = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
Value<String> color = const Value.absent(),
Value<int?> score = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => TeamTableCompanion(
id: id,
name: name,
createdAt: createdAt,
color: color,
score: score,
rowid: rowid,
),
createCompanionCallback:
@@ -5515,15 +5309,11 @@ class $$TeamTableTableTableManager
required String id,
required String name,
required DateTime createdAt,
Value<String> color = const Value.absent(),
Value<int?> score = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => TeamTableCompanion.insert(
id: id,
name: name,
createdAt: createdAt,
color: color,
score: score,
rowid: rowid,
),
withReferenceMapper: (p0) => p0

View File

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

View File

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

View File

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

View File

@@ -16,10 +16,9 @@ class Match {
final Game game;
final Group? group;
final List<Player> players;
final bool isTeamMatch;
final List<Team>? teams;
final String notes;
final Map<String, ScoreEntry?> scores;
Map<String, ScoreEntry?> scores;
Match({
required this.name,
@@ -27,7 +26,6 @@ class Match {
required this.players,
this.endedAt,
this.group,
this.isTeamMatch = false,
this.teams,
this.notes = '',
String? id,
@@ -39,7 +37,7 @@ class Match {
@override
String toString() {
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, isTeamMatch: $isTeamMatch, teams: $teams, notes: $notes, scores: $scores, mvp: $mvp}';
return 'Match{id: $id, createdAt: $createdAt, endedAt: $endedAt, name: $name, game: $game, group: $group, players: $players, notes: $notes, scores: $scores, mvp: $mvp}';
}
Match copyWith({
@@ -50,7 +48,6 @@ class Match {
Game? game,
Group? group,
List<Player>? players,
bool? isTeamMatch,
List<Team>? teams,
String? notes,
Map<String, ScoreEntry?>? scores,
@@ -63,7 +60,6 @@ class Match {
game: game ?? this.game,
group: group ?? this.group,
players: players ?? this.players,
isTeamMatch: isTeamMatch ?? this.isTeamMatch,
teams: teams ?? this.teams,
notes: notes ?? this.notes,
scores: scores ?? this.scores,
@@ -82,7 +78,6 @@ class Match {
game == other.game &&
group == other.group &&
const DeepCollectionEquality().equals(players, other.players) &&
isTeamMatch == other.isTeamMatch &&
const DeepCollectionEquality().equals(teams, other.teams) &&
notes == other.notes &&
const DeepCollectionEquality().equals(scores, other.scores);
@@ -96,7 +91,6 @@ class Match {
game,
group,
const DeepCollectionEquality().hash(players),
isTeamMatch,
const DeepCollectionEquality().hash(teams),
notes,
const DeepCollectionEquality().hash(scores),
@@ -113,12 +107,11 @@ class Match {
name: '',
ruleset: Ruleset.singleWinner,
description: '',
color: AppColor.blue,
color: GameColor.blue,
icon: '',
),
group = null,
players = [],
isTeamMatch = json['isTeamMatch'],
teams = [],
scores = json['scores'] != null
? (json['scores'] as Map<String, dynamic>).map(
@@ -140,13 +133,11 @@ class Match {
'gameId': game.id,
'groupId': group?.id,
'playerIds': players.map((player) => player.id).toList(),
'isTeamMatch': isTeamMatch,
'teams': teams?.map((team) => team.toJson()).toList(),
'scores': scores.map((key, value) => MapEntry(key, value?.toJson())),
'notes': notes,
};
// Most Valuable Player(s) based on the match's ruleset
List<Player> get mvp {
if (players.isEmpty || scores.isEmpty) return [];
@@ -204,59 +195,4 @@ class Match {
return playerScore.score == lowestScore;
}).toList();
}
// MVP for team-based matches (Most Valuable Team)
List<Team> get mvt {
if (teams == null || teams!.isEmpty) return [];
switch (game.ruleset) {
case Ruleset.highestScore:
return _getHighestScoreTeam();
case Ruleset.lowestScore:
return _getLowestScoreTeam();
case Ruleset.singleWinner:
return _getHighestScoreTeam().take(1).toList();
case Ruleset.singleLoser:
return _getLowestScoreTeam().take(1).toList();
case Ruleset.multipleWinners:
return _getHighestScoreTeam();
case Ruleset.placement:
return _getHighestScoreTeam().take(1).toList();
}
}
List<Team> _getHighestScoreTeam() {
if (teams!.every((team) => team.score == null)) {
return [];
}
final int highestScore = teams!
.map((team) => team.score)
.whereType<int>()
.reduce((max, score) => score > max ? score : max);
return teams!.where((team) {
return team.score == highestScore;
}).toList();
}
List<Team> _getLowestScoreTeam() {
if (teams!.every((team) => team.score == null)) {
return [];
}
final int lowestScore = teams!
.map((team) => team.score)
.whereType<int>()
.reduce((min, score) => score < min ? score : min);
return teams!.where((team) {
return team.score == lowestScore;
}).toList();
}
}

View File

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

View File

@@ -1,6 +1,5 @@
{
"@@locale": "de",
"add_team": "Team hinzufügen",
"all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Spiele",
@@ -26,7 +25,6 @@
"create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel erstellen",
"create_teams": "Teams erstellen",
"created_on": "Erstellt am",
"data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht",
@@ -81,12 +79,10 @@
"live_edit_mode": "Live-Bearbeitungsmodus",
"loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte",
"manage_members": "Mitglieder bearbeiten",
"match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel",
"match_profile": "Spielprofil",
"matches": "Spiele",
"member": "Mitglied",
"members": "Mitglieder",
"most_points": "Höchste Punkte",
"multiple_winners": "Mehrere Gewinner:innen",
@@ -96,7 +92,6 @@
"no_license_text_available": "Kein Lizenztext verfügbar",
"no_licenses_found": "Keine Lizenzen gefunden",
"no_matches_created_yet": "Noch keine Spiele erstellt",
"no_players_available": "Keine Spieler:innen verfügbar",
"no_players_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
"no_players_selected": "Keine Spieler:innen ausgewählt",
@@ -104,7 +99,6 @@
"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_teams_available": "Keine Teams verfügbar",
"none": "Kein",
"none_group": "Keine",
"not_available": "Nicht verfügbar",
@@ -118,7 +112,6 @@
"privacy_policy": "Datenschutzerklärung",
"quick_create": "Schnellzugriff",
"recent_matches": "Letzte Spiele",
"redistribute": "Neu verteilen",
"result": "Ergebnis",
"results": "Ergebnisse",
"ruleset": "Regelwerk",
@@ -140,9 +133,6 @@
"statistics": "Statistiken",
"stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
"team": "Team",
"team_match": "Teamspiel",
"teams": "Teams",
"there_are_no_games_matching_your_search": "Es gibt keine Spielvorlagen, die deiner Suche entspricht",
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.",

View File

@@ -1,6 +1,5 @@
{
"@@locale": "en",
"add_team": "Add Team",
"all_players": "All players",
"all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches",
@@ -26,7 +25,6 @@
"create_match": "Create match",
"create_new_group": "Create new group",
"create_new_match": "Create new match",
"create_teams": "Create teams",
"created_on": "Created on",
"data": "Data",
"data_successfully_deleted": "Data successfully deleted",
@@ -81,12 +79,10 @@
"live_edit_mode": "Live Edit Mode",
"loser": "Loser",
"lowest_score": "Lowest Score",
"manage_members": "Manage Members",
"match_in_progress": "Match in progress...",
"match_name": "Match name",
"match_profile": "Match Profile",
"matches": "Matches",
"member": "Member",
"members": "Members",
"most_points": "Most Points",
"multiple_winners": "Multiple Winners",
@@ -96,7 +92,6 @@
"no_license_text_available": "No license text available",
"no_licenses_found": "No licenses found",
"no_matches_created_yet": "No matches created yet",
"no_players_available": "No players available",
"no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name",
"no_players_selected": "No players selected",
@@ -104,7 +99,6 @@
"no_results_entered_yet": "No results entered yet",
"no_second_match_available": "No second match available",
"no_statistics_available": "No statistics available",
"no_teams_available": "No teams available",
"none": "None",
"none_group": "None",
"not_available": "Not available",
@@ -118,7 +112,6 @@
"privacy_policy": "Privacy Policy",
"quick_create": "Quick Create",
"recent_matches": "Recent Matches",
"redistribute": "Redistribute",
"results": "Results",
"ruleset": "Ruleset",
"ruleset_least_points": "Inverse scoring: the player with the fewest points wins.",
@@ -148,9 +141,6 @@
}
}
},
"team": "Team",
"team_match": "Team Match",
"teams": "Teams",
"there_are_no_games_matching_your_search": "There are no games matching your search",
"there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone.",

View File

@@ -98,12 +98,6 @@ abstract class AppLocalizations {
Locale('en'),
];
/// No description provided for @add_team.
///
/// In en, this message translates to:
/// **'Add Team'**
String get add_team;
/// No description provided for @all_players.
///
/// In en, this message translates to:
@@ -254,12 +248,6 @@ abstract class AppLocalizations {
/// **'Create new match'**
String get create_new_match;
/// No description provided for @create_teams.
///
/// In en, this message translates to:
/// **'Create teams'**
String get create_teams;
/// No description provided for @created_on.
///
/// In en, this message translates to:
@@ -542,12 +530,6 @@ abstract class AppLocalizations {
/// **'Lowest Score'**
String get lowest_score;
/// No description provided for @manage_members.
///
/// In en, this message translates to:
/// **'Manage Members'**
String get manage_members;
/// No description provided for @match_in_progress.
///
/// In en, this message translates to:
@@ -572,12 +554,6 @@ abstract class AppLocalizations {
/// **'Matches'**
String get matches;
/// No description provided for @member.
///
/// In en, this message translates to:
/// **'Member'**
String get member;
/// No description provided for @members.
///
/// In en, this message translates to:
@@ -632,12 +608,6 @@ abstract class AppLocalizations {
/// **'No matches created yet'**
String get no_matches_created_yet;
/// No description provided for @no_players_available.
///
/// In en, this message translates to:
/// **'No players available'**
String get no_players_available;
/// No description provided for @no_players_created_yet.
///
/// In en, this message translates to:
@@ -680,12 +650,6 @@ abstract class AppLocalizations {
/// **'No statistics available'**
String get no_statistics_available;
/// No description provided for @no_teams_available.
///
/// In en, this message translates to:
/// **'No teams available'**
String get no_teams_available;
/// No description provided for @none.
///
/// In en, this message translates to:
@@ -764,12 +728,6 @@ abstract class AppLocalizations {
/// **'Recent Matches'**
String get recent_matches;
/// No description provided for @redistribute.
///
/// In en, this message translates to:
/// **'Redistribute'**
String get redistribute;
/// No description provided for @results.
///
/// In en, this message translates to:
@@ -890,24 +848,6 @@ abstract class AppLocalizations {
/// **'Successfully added player {playerName}'**
String successfully_added_player(String playerName);
/// No description provided for @team.
///
/// In en, this message translates to:
/// **'Team'**
String get team;
/// No description provided for @team_match.
///
/// In en, this message translates to:
/// **'Team Match'**
String get team_match;
/// No description provided for @teams.
///
/// In en, this message translates to:
/// **'Teams'**
String get teams;
/// No description provided for @there_are_no_games_matching_your_search.
///
/// In en, this message translates to:

View File

@@ -8,9 +8,6 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get add_team => 'Team hinzufügen';
@override
String get all_players => 'Alle Spieler:innen';
@@ -88,9 +85,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get create_new_match => 'Neues Spiel erstellen';
@override
String get create_teams => 'Teams erstellen';
@override
String get created_on => 'Erstellt am';
@@ -246,9 +240,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get lowest_score => 'Niedrigste Punkte';
@override
String get manage_members => 'Mitglieder bearbeiten';
@override
String get match_in_progress => 'Spiel läuft...';
@@ -261,9 +252,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get matches => 'Spiele';
@override
String get member => 'Mitglied';
@override
String get members => 'Mitglieder';
@@ -291,9 +279,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@override
String get no_players_available => 'Keine Spieler:innen verfügbar';
@override
String get no_players_created_yet => 'Noch keine Spieler:in erstellt';
@@ -316,9 +301,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get no_statistics_available => 'Keine Statistiken verfügbar';
@override
String get no_teams_available => 'Keine Teams verfügbar';
@override
String get none => 'Kein';
@@ -358,9 +340,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get recent_matches => 'Letzte Spiele';
@override
String get redistribute => 'Neu verteilen';
@override
String get results => 'Ergebnisse';
@@ -428,15 +407,6 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Spieler:in $playerName erfolgreich hinzugefügt';
}
@override
String get team => 'Team';
@override
String get team_match => 'Teamspiel';
@override
String get teams => 'Teams';
@override
String get there_are_no_games_matching_your_search =>
'Es gibt keine Spielvorlagen, die deiner Suche entspricht';

View File

@@ -8,9 +8,6 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get add_team => 'Add Team';
@override
String get all_players => 'All players';
@@ -88,9 +85,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get create_new_match => 'Create new match';
@override
String get create_teams => 'Create teams';
@override
String get created_on => 'Created on';
@@ -246,9 +240,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get lowest_score => 'Lowest Score';
@override
String get manage_members => 'Manage Members';
@override
String get match_in_progress => 'Match in progress...';
@@ -261,9 +252,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get matches => 'Matches';
@override
String get member => 'Member';
@override
String get members => 'Members';
@@ -291,9 +279,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get no_matches_created_yet => 'No matches created yet';
@override
String get no_players_available => 'No players available';
@override
String get no_players_created_yet => 'No players created yet';
@@ -316,9 +301,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get no_statistics_available => 'No statistics available';
@override
String get no_teams_available => 'No teams available';
@override
String get none => 'None';
@@ -358,9 +340,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get recent_matches => 'Recent Matches';
@override
String get redistribute => 'Redistribute';
@override
String get results => 'Results';
@@ -428,15 +407,6 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Successfully added player $playerName';
}
@override
String get team => 'Team';
@override
String get team_match => 'Team Match';
@override
String get teams => 'Teams';
@override
String get there_are_no_games_matching_your_search =>
'There are no games matching your search';

View File

@@ -8,7 +8,7 @@ import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
@@ -96,24 +96,19 @@ class _CreateGroupViewState extends State<CreateGroupView> {
},
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: AnimatedDialogButton(
buttonConstraints: const BoxConstraints(
minWidth: double.infinity,
minHeight: 50,
),
buttonText: widget.groupToEdit == null
? loc.create_group
: loc.edit_group,
buttonType: ButtonType.primary,
onPressed:
(_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null
: _saveGroup,
),
CustomWidthButton(
text: widget.groupToEdit == null
? loc.create_group
: loc.edit_group,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed:
(_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null
: _saveGroup,
),
const SizedBox(height: 20),
],
),
),

View File

@@ -150,6 +150,7 @@ class _GroupDetailViewState extends State<GroupDetailView> {
return TextIconTile(
text: member.name,
suffixText: getNameCountText(member),
iconEnabled: false,
);
}).toList(),
),

View File

@@ -51,9 +51,6 @@ class _ChooseGameViewState extends State<ChooseGameView> {
/// Games filtered according to the current search query
late List<Game> filteredGames;
List<Game> get games =>
widget.games..sort((a, b) => a.name.compareTo(b.name));
@override
void initState() {
db = Provider.of<AppDatabase>(context, listen: false);
@@ -62,7 +59,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
selectedGameId = widget.initialGameId;
// Start with all games visible
filteredGames = List<Game>.from(games);
filteredGames = List<Game>.from(widget.games);
super.initState();
}
@@ -80,7 +77,9 @@ class _ChooseGameViewState extends State<ChooseGameView> {
Navigator.of(context).pop(
selectedGameId == ''
? null
: games.firstWhere((game) => game.id == selectedGameId),
: widget.games.firstWhere(
(game) => game.id == selectedGameId,
),
);
},
),
@@ -100,7 +99,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
);
if (result != null && result.game != null) {
setState(() {
games.insert(0, result.game);
widget.games.insert(0, result.game);
});
_refreshFromSource();
}
@@ -140,7 +139,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
child: Visibility(
visible: filteredGames.isNotEmpty,
replacement: Visibility(
visible: games.isNotEmpty,
visible: widget.games.isNotEmpty,
replacement: TopCenteredMessage(
icon: Icons.info,
title: loc.info,
@@ -161,7 +160,10 @@ class _ChooseGameViewState extends State<ChooseGameView> {
return GameTile(
title: game.name,
description: game.description,
subtitle: translateRulesetToString(game.ruleset, context),
badgeText: translateRulesetToString(
game.ruleset,
context,
),
badgeColor: getColorFromGameColor(game.color),
isHighlighted: selectedGameId == game.id,
onTap: () async {
@@ -188,7 +190,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
);
if (result != null && result.game != null) {
// Find the index in the original list to mutate
final originalIndex = games.indexWhere(
final originalIndex = widget.games.indexWhere(
(g) => g.id == game.id,
);
if (originalIndex == -1) {
@@ -200,12 +202,12 @@ class _ChooseGameViewState extends State<ChooseGameView> {
if (selectedGameId == game.id) {
selectedGameId = '';
}
games.removeAt(originalIndex);
widget.games.removeAt(originalIndex);
widget.onGamesUpdated?.call();
});
} else {
setState(() {
games[originalIndex] = result.game;
widget.games[originalIndex] = result.game;
});
}
_refreshFromSource();
@@ -227,13 +229,13 @@ class _ChooseGameViewState extends State<ChooseGameView> {
final q = query.toLowerCase().trim();
if (q.isEmpty) {
setState(() {
filteredGames = List<Game>.from(games);
filteredGames = List<Game>.from(widget.games);
});
return;
}
setState(() {
filteredGames = games.where((game) {
filteredGames = widget.games.where((game) {
final name = game.name.toLowerCase();
final description = game.description.toLowerCase();
return name.contains(q) || description.contains(q);

View File

@@ -49,10 +49,10 @@ class _CreateGameViewState extends State<CreateGameView> {
late final AppDatabase db;
late List<(Ruleset, String)> _rulesets;
late List<(AppColor, String)> _colors;
late List<(GameColor, String)> _colors;
Ruleset? selectedRuleset = Ruleset.singleWinner;
AppColor? selectedColor = AppColor.orange;
GameColor? selectedColor = GameColor.orange;
/// Controller for the game name input field.
final _gameNameController = TextEditingController();
@@ -87,10 +87,10 @@ class _CreateGameViewState extends State<CreateGameView> {
),
);
_colors = List.generate(
AppColor.values.length,
GameColor.values.length,
(index) => (
AppColor.values[index],
translateGameColorToString(AppColor.values[index], context),
GameColor.values[index],
translateGameColorToString(GameColor.values[index], context),
),
);

View File

@@ -12,9 +12,8 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/create_teams_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/player_selection.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
@@ -60,7 +59,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
Group? selectedGroup;
Game? selectedGame;
bool isTeamMatch = false;
List<Player> selectedPlayers = [];
/// GlobalKey for ScaffoldMessenger to show snackbars
@@ -137,7 +135,24 @@ class _CreateMatchViewState extends State<CreateMatchView> {
trailing: selectedGame == null
? Text(loc.none_group)
: Text(selectedGame!.name),
onPressed: () async => await onChoosingGame(),
onPressed: () async {
selectedGame = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGameView(
games: gamesList,
initialGameId: selectedGame?.id ?? '',
onGamesUpdated: widget.onMatchesUpdated,
),
),
);
setState(() {
if (selectedGame != null) {
hintText = selectedGame!.name;
} else {
hintText = loc.match_name;
}
});
},
),
// Group selection tile.
@@ -146,19 +161,35 @@ class _CreateMatchViewState extends State<CreateMatchView> {
trailing: selectedGroup == null
? Text(loc.none_group)
: Text(selectedGroup!.name),
onPressed: () async => onChoosingGroup(),
),
onPressed: () async {
// Remove all players from the previously selected group from
// the selected players list, in case the user deselects the
// group or selects a different group.
selectedPlayers.removeWhere(
(player) =>
selectedGroup?.members.any(
(member) => member.id == player.id,
) ??
false,
);
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroup?.id ?? '',
),
),
);
if (!isEditMode())
ChooseTile(
title: loc.team_match,
trailing: Switch.adaptive(
activeTrackColor: CustomTheme.primaryColor,
padding: const EdgeInsets.symmetric(vertical: -15),
value: isTeamMatch,
onChanged: (value) => setState(() => isTeamMatch = value),
),
),
setState(() {
if (selectedGroup != null) {
setState(() {
selectedPlayers += [...selectedGroup!.members];
});
}
});
},
),
// Player selection widget.
Expanded(
@@ -175,21 +206,15 @@ class _CreateMatchViewState extends State<CreateMatchView> {
),
// Create or save button.
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: AnimatedDialogButton(
buttonConstraints: const BoxConstraints(
minWidth: double.infinity,
minHeight: 50,
),
buttonType: ButtonType.primary,
onPressed: isSubmitButtonEnabled()
? () {
submitButtonNavigation(context);
}
: null,
buttonText: buttonText,
),
CustomWidthButton(
text: buttonText,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton()
? () {
buttonNavigation(context);
}
: null,
),
],
),
@@ -202,86 +227,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
return widget.matchToEdit != null;
}
// If a match was provided to the view, this method prefills the input fields
void prefillMatchDetails() {
final match = widget.matchToEdit!;
_matchNameController.text = match.name;
selectedPlayers = match.players;
selectedGame = match.game;
if (match.group != null) {
selectedGroup = match.group;
}
}
Future<void> onChoosingGame() async {
selectedGame = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGameView(
games: gamesList,
initialGameId: selectedGame?.id ?? '',
onGamesUpdated: widget.onMatchesUpdated,
),
),
);
setState(() {
if (selectedGame != null) {
hintText = selectedGame!.name;
} else {
hintText = AppLocalizations.of(context).match_name;
}
});
}
Future<void> onChoosingGroup() async {
// Remove all players from the previously selected group from
// the selected players list, in case the user deselects the
// group or selects a different group.
selectedPlayers.removeWhere(
(player) =>
selectedGroup?.members.any((member) => member.id == player.id) ??
false,
);
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroup?.id ?? '',
),
),
);
setState(() {
if (selectedGroup != null) {
setState(() {
selectedPlayers += [...selectedGroup!.members];
});
}
});
}
// If none of the selected players are from the currently selected group,
// the group is also deselected.
Future<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return;
if (!selectedPlayers.any(
(player) =>
selectedGroup!.members.any((member) => member.id == player.id),
)) {
setState(() {
selectedGroup = null;
});
}
}
/// Determines whether the "Create Match" button should be enabled.
///
/// Returns `true` if:
/// - A game is selected AND
/// - Either a group is selected OR at least 2 players are selected.
bool isSubmitButtonEnabled() {
bool _enableCreateGameButton() {
return ((selectedGroup != null || selectedPlayers.length > 1) &&
selectedGame != null);
}
@@ -290,35 +241,20 @@ class _CreateMatchViewState extends State<CreateMatchView> {
///
/// If a match is being edited, updates the match in the database.
/// Otherwise, creates a new match and navigates to the MatchResultView.
void submitButtonNavigation(BuildContext context) async {
void buttonNavigation(BuildContext context) async {
if (isEditMode()) {
await updateMatch();
if (context.mounted) {
Navigator.pop(context);
}
}
final match = await createMatch();
if (isTeamMatch) {
if (context.mounted) {
Navigator.push(
context,
adaptivePageRoute(
fullscreenDialog: !isTeamMatch,
builder: (context) => CreateTeamsView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
} else {
final match = await createMatch();
if (context.mounted) {
Navigator.pushReplacement(
context,
adaptivePageRoute(
fullscreenDialog: !isTeamMatch,
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
@@ -391,12 +327,36 @@ class _CreateMatchViewState extends State<CreateMatchView> {
createdAt: DateTime.now(),
group: selectedGroup,
players: selectedPlayers,
isTeamMatch: isTeamMatch,
game: selectedGame!,
);
// Team matches are saved in OrganizeTeamsView
if (!isTeamMatch) await db.matchDao.addMatch(match: match);
await db.matchDao.addMatch(match: match);
return match;
}
// If a match was provided to the view, this method prefills the input fields
void prefillMatchDetails() {
final match = widget.matchToEdit!;
_matchNameController.text = match.name;
selectedPlayers = match.players;
selectedGame = match.game;
if (match.group != null) {
selectedGroup = match.group;
}
}
// If none of the selected players are from the currently selected group,
// the group is also deselected.
Future<void> removeGroupWhenNoMemberLeft() async {
if (selectedGroup == null) return;
if (!selectedPlayers.any(
(player) =>
selectedGroup!.members.any((member) => member.id == player.id),
)) {
setState(() {
selectedGroup = null;
});
}
}
}

View File

@@ -1,190 +0,0 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_teams/manage_members_view.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/team_creation_tile.dart';
class CreateTeamsView extends StatefulWidget {
const CreateTeamsView({super.key, required this.match, this.onWinnerChanged});
final Match match;
final VoidCallback? onWinnerChanged;
@override
State<CreateTeamsView> createState() => _CreateTeamsViewState();
}
class _CreateTeamsViewState extends State<CreateTeamsView> {
final Random random = Random();
List<Player> get matchPlayers => widget.match.players;
late List<Team> teams;
late List<TextEditingController> nameController;
final int initialTeamCount = 2;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final loc = AppLocalizations.of(context);
// Init the teams
teams = List.generate(
initialTeamCount,
(index) => Team(
name: '${loc.team} ${index + 1}',
color: getTeamColor(index),
members: [],
),
);
// Init the controllers
nameController = teams.map(getNewController).toList();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_teams)),
body: Stack(
alignment: Alignment.center,
children: [
Positioned.fill(
child: ListView.builder(
padding: const EdgeInsets.only(top: 12, bottom: 96),
itemCount: teams.length,
itemBuilder: (context, index) {
return TeamCreationTile(
color: teams[index].color,
controller: nameController[index],
hintText: '${loc.team} ${index + 1}',
onDelete: teams.length <= 2 ? null : () => removeTeam(index),
onColorSelection: (color) {
setState(() {
teams[index] = teams[index].copyWith(color: color);
});
},
);
},
),
),
// Button row
Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Add new team
MainMenuButton(
icon: Icons.add,
text: loc.add_team,
onPressed: teams.length >= widget.match.players.length
? null
: addTeam,
),
const SizedBox(width: 15),
// Confirm teams
MainMenuButton(
icon: Icons.arrow_forward_sharp,
onPressed: teams.length >= 2
? () {
final match = widget.match.copyWith(teams: teams);
Navigator.push(
context,
adaptivePageRoute(
builder: (context) => ManageMembersView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
: null,
),
],
),
),
],
),
);
}
/// Creates a new team with a default name and color based on the current number
Team getNewTeam() {
final loc = AppLocalizations.of(context);
return Team(
name: '${loc.team} ${teams.length + 1}',
color: getTeamColor(teams.length),
members: [],
);
}
/// Builds a [TextEditingController] for the given team and sets up a listener
/// to update the team's name whenever the text changes.
TextEditingController getNewController(Team team) {
final textController = TextEditingController(text: team.name);
textController.addListener(() {
final index = teams.indexWhere((t) => t.id == team.id);
if (index == -1) return;
teams[index] = teams[index].copyWith(name: textController.text);
});
return textController;
}
/// Adds a new team to the list of teams, creates a corresponding controller,
/// and redistributes the players among all teams.
void addTeam() {
setState(() {
final newTeam = getNewTeam();
teams.add(newTeam);
nameController.add(getNewController(newTeam));
});
}
/// Removes the team with the given index. If there are less than 2 teams the
/// removed team gets replaced with a new one
void removeTeam(int index) {
final loc = AppLocalizations.of(context);
setState(() {
teams.removeAt(index);
final removedController = nameController.removeAt(index);
removedController.dispose();
// Update index-based team names and default colors
for (int i = 0; i < nameController.length; i++) {
if (nameController[i].text.contains(
RegExp('^${RegExp.escape(loc.team)} \\d+\$'),
)) {
nameController[i].text = '${loc.team} ${i + 1}';
// Reset color to default if it was based on the index
final previousIndex = i < index ? i : i + 1;
if (teams[i].color == getTeamColor(previousIndex)) {
teams[i] = teams[i].copyWith(color: getTeamColor(i));
}
}
}
});
}
@override
void dispose() {
for (final c in nameController) {
c.dispose();
}
super.dispose();
}
}

View File

@@ -1,306 +0,0 @@
import 'dart:core' hide Match;
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_numeric_text/flutter_numeric_text.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart';
/// Displays the given [teams] as a flat reorderable list where every team is
/// preceded by a header row and followed by its members. Members can be
/// dragged across team boundaries to be reassigned to another team.
class ManageMembersView extends StatefulWidget {
const ManageMembersView({
super.key,
required this.match,
required this.onWinnerChanged,
});
final Match match;
final VoidCallback? onWinnerChanged;
@override
State<ManageMembersView> createState() => _ManageMembersViewState();
}
class _ManageMembersViewState extends State<ManageMembersView> {
late AppDatabase db;
List<Team> get teams => widget.match.teams!;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
redistributePlayers();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.manage_members)),
body: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned.fill(
child: ReorderableListView.builder(
padding: const EdgeInsets.fromLTRB(0, 12, 0, 96),
buildDefaultDragHandles: false,
itemCount: allItemsCount,
onReorderItem: onReorderItem,
proxyDecorator: (child, index, animation) =>
Material(type: MaterialType.transparency, child: child),
itemBuilder: (context, index) {
final teamIndex = teamIndexForFlat(index);
final memberIndex = memberIndexForFlat(index, teamIndex);
final team = teams[teamIndex];
if (memberIndex == -1) {
return buildTeamTile(team: team);
}
final player = team.members[memberIndex];
return ReorderableDelayedDragStartListener(
key: ValueKey('player_${player.id}'),
index: index,
child: TextIconListTile(
text: player.name,
suffixText: getNameCountText(player),
icon: Icons.drag_handle,
),
);
},
),
),
Positioned(
bottom: MediaQuery.of(context).padding.bottom + 20,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MainMenuButton(
onPressed: () => setState(() {
redistributePlayers();
}),
icon: Icons.cached,
),
const SizedBox(width: 16),
MainMenuButton(
onPressed: allTeamsHaveMembers
? () async => submitMatch()
: null,
text: loc.create_match,
icon: RpgAwesome.clovers_card,
),
],
),
),
],
),
);
}
Widget buildTeamTile({required Team team}) {
final color = getColorFromGameColor(team.color);
final loc = AppLocalizations.of(context);
final length = team.members.length;
final memberText = length == 1 ? loc.member : loc.members;
return Padding(
key: ValueKey(team.id),
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
child: Row(
children: [
// Color circle
Container(
width: 14,
height: 14,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 10),
// Team name
Expanded(
child: Text(
team.name,
style: const TextStyle(
color: CustomTheme.textColor,
fontSize: 17,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
// Member length
SizedBox(
width: 150,
child: NumericText(
'$length $memberText',
duration: const Duration(milliseconds: 200),
maxLines: 1,
textAlign: TextAlign.end,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: CustomTheme.hintColor,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
],
),
);
}
// Iterates through all teams and redistributes players randomly and
// as evenly as possible.
void redistributePlayers() {
for (final team in teams) {
team.members.clear();
}
var matchPlayers = widget.match.players;
Random random = Random();
if (matchPlayers.isEmpty || teams.isEmpty) {
return;
}
final shuffledPlayers = [...matchPlayers]..shuffle(random);
for (int i = 0; i < shuffledPlayers.length; i++) {
final teamIndex = i % teams.length;
teams[teamIndex].members.add(shuffledPlayers[i]);
}
}
/// Handles moving a member from one team to another
void onReorderItem(int oldIndex, int newIndex) {
final sourceTeamIndex = teamIndexForFlat(oldIndex);
final sourceMemberIndex = memberIndexForFlat(oldIndex, sourceTeamIndex);
// Headers themselves can't be reordered.
if (sourceMemberIndex == -1) return;
// When moving down, the target index is shifted by 1
// because the item is removed first.
var targetIndex = newIndex;
if (newIndex > oldIndex) targetIndex -= 1;
targetIndex = targetIndex.clamp(0, allItemsCount - 1);
// Resolve target location based on the item currently
// at targetIndex before the move.
int destTeamIndex;
int insertPositionInTeam;
if (targetIndex >= allItemsCount - 1 && newIndex >= allItemsCount) {
// dropped at the very end, append to the last team.
destTeamIndex = teams.length - 1;
insertPositionInTeam = teams[destTeamIndex].members.length;
} else {
destTeamIndex = teamIndexForFlat(targetIndex);
final anchorMemberIndex = memberIndexForFlat(targetIndex, destTeamIndex);
if (anchorMemberIndex == -1) {
// dropped on a header, direction decides which team the player gets added
// if moving down, insert as first member of that team.
// if moving UP, append to the previous team.
final isMovingDown = newIndex > oldIndex;
if (isMovingDown) {
insertPositionInTeam = 0;
} else {
final previousTeamIndex = destTeamIndex - 1;
if (previousTeamIndex < 0) {
// above the very first header, stay at top of team 0.
insertPositionInTeam = 0;
} else {
destTeamIndex = previousTeamIndex;
insertPositionInTeam = teams[destTeamIndex].members.length;
}
}
} else {
insertPositionInTeam = anchorMemberIndex;
}
}
setState(() {
final sourceMembers = teams[sourceTeamIndex].members;
final player = sourceMembers.removeAt(sourceMemberIndex);
// Adjust insert index if removed from before the insert point in the
// same team.
if (sourceTeamIndex == destTeamIndex &&
insertPositionInTeam > sourceMembers.length) {
insertPositionInTeam = sourceMembers.length;
}
teams[destTeamIndex].members.insert(insertPositionInTeam, player);
});
}
/// Total players + teams length
int get allItemsCount {
var count = 0;
for (final team in teams) {
count += 1 + team.members.length;
}
return count;
}
/// Returns the index of the team that owns the flat-list item at [flatIndex].
int teamIndexForFlat(int flatIndex) {
var remaining = flatIndex;
for (var i = 0; i < teams.length; i++) {
final size = 1 + teams[i].members.length;
if (remaining < size) return i;
remaining -= size;
}
return teams.length - 1;
}
/// Returns the member index within its team, or `-1` if the item at
/// [flatIndex] is the team header.
int memberIndexForFlat(int flatIndex, int teamIndex) {
var offset = 0;
for (var i = 0; i < teamIndex; i++) {
offset += 1 + teams[i].members.length;
}
// offset now points to the header of [teamIndex]. Anything beyond is a
// member of that team.
final localIndex = flatIndex - offset;
return localIndex == 0 ? -1 : localIndex - 1;
}
bool get allTeamsHaveMembers =>
teams.every((team) => team.members.isNotEmpty);
void submitMatch() async {
final match = widget.match;
await db.matchDao.addMatch(match: match);
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => MatchResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
(route) => route.isFirst,
);
}
}
}

View File

@@ -1,89 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart';
class LiveEditView extends StatefulWidget {
const LiveEditView({super.key, required this.match});
final Match match;
@override
State<LiveEditView> createState() => _LiveEditViewState();
}
class _LiveEditViewState extends State<LiveEditView> {
List<Team> get allTeams =>
(widget.match.teams ?? [])..sort((a, b) => a.name.compareTo(b.name));
List<Player> get allPlayers =>
widget.match.players..sort((a, b) => a.name.compareTo(b.name));
List<int> scores = [];
@override
void initState() {
super.initState();
if (widget.match.isTeamMatch) {
scores = List.generate(
allTeams.length,
(index) => allTeams[index].score ?? 0,
);
} else {
scores = List.generate(
allPlayers.length,
(index) => widget.match.scores[allPlayers[index].id]?.score ?? 0,
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.match.name),
leading: HapticIconButton(
onPressed: () => Navigator.pop(context, scores),
icon: const Icon(Icons.close),
),
),
body: Column(
children: [
Expanded(child: buildLiveEditWidget(widget.match.isTeamMatch)),
],
),
);
}
Widget buildLiveEditWidget(bool isTeamMatch) {
if (isTeamMatch) {
return ListView.builder(
itemCount: allTeams.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allTeams[index].name,
onChanged: (value) {
scores[index] = value;
},
value: scores[index],
);
},
);
} else {
return ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allPlayers[index].name,
onChanged: (value) {
setState(() {
scores[index] = value;
});
},
value: scores[index],
);
},
);
}
}
}

View File

@@ -13,7 +13,6 @@ import 'package:tallee/presentation/views/main_menu/match_view/create_match/crea
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/cards/team_card.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
@@ -44,13 +43,13 @@ class MatchDetailView extends StatefulWidget {
class _MatchDetailViewState extends State<MatchDetailView> {
late final AppDatabase db;
late Match localMatch;
late Match match;
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
localMatch = widget.match;
match = widget.match;
}
@override
@@ -84,7 +83,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
await db.matchDao.deleteMatch(matchId: localMatch.id);
await db.matchDao.deleteMatch(matchId: match.id);
if (!context.mounted) return;
Navigator.pop(context);
widget.onMatchUpdate.call();
@@ -118,7 +117,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Match Name
Text(
localMatch.name,
match.name,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
@@ -130,7 +129,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Creation Date
Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(localMatch.createdAt)}',
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: CustomTheme.textColor,
@@ -140,14 +139,14 @@ class _MatchDetailViewState extends State<MatchDetailView> {
const SizedBox(height: 10),
// Group Name
if (localMatch.group != null) ...[
if (match.group != null) ...[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.group),
const SizedBox(width: 8),
Text(
'${localMatch.group!.name}${getExtraPlayerCount(localMatch)}',
'${match.group!.name}${getExtraPlayerCount(match)}',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
@@ -155,60 +154,25 @@ class _MatchDetailViewState extends State<MatchDetailView> {
const SizedBox(height: 20),
],
// Teams or Players
if (localMatch.isTeamMatch) ...[
// Teams
InfoTile(
title: loc.teams,
icon: Icons.scoreboard,
horizontalAlignment: CrossAxisAlignment.start,
content:
localMatch.teams != null && localMatch.teams!.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: (localMatch.teams ?? []).map((team) {
return TeamCard(team: team);
}).toList(),
)
: Text(
loc.no_teams_available,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
// Players
InfoTile(
title: loc.players,
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: match.players.map((player) {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
);
}).toList(),
),
] else ...[
// Players
InfoTile(
title: loc.players,
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: localMatch.players.isNotEmpty
? Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: localMatch.players.map((player) {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
);
}).toList(),
)
: Text(
loc.no_players_available,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
),
],
),
const SizedBox(height: 15),
// Game
@@ -222,12 +186,12 @@ class _MatchDetailViewState extends State<MatchDetailView> {
horizontal: 8,
),
child: GameLabel(
title: localMatch.game.name,
title: match.game.name,
description: translateRulesetToString(
localMatch.game.ruleset,
match.game.ruleset,
context,
),
color: localMatch.game.color,
color: match.game.color,
),
),
),
@@ -258,7 +222,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => CreateMatchView(
matchToEdit: localMatch,
matchToEdit: match,
onMatchUpdated: onMatchUpdated,
),
),
@@ -274,10 +238,12 @@ class _MatchDetailViewState extends State<MatchDetailView> {
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: localMatch,
onWinnerChanged: () async {
match: match,
onWinnerChanged: () {
widget.onMatchUpdate.call();
await updateScoresForCurrentMatch();
setState(() {
updateScoresForCurrentMatch();
});
},
),
),
@@ -297,7 +263,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
/// updates the match in this view
void onMatchUpdated(Match editedMatch) {
setState(() {
localMatch = editedMatch;
match = editedMatch;
});
widget.onMatchUpdate.call();
}
@@ -318,113 +284,95 @@ class _MatchDetailViewState extends State<MatchDetailView> {
/// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet
List<Widget> getSingleResultRow(AppLocalizations loc) {
final ruleset = localMatch.game.ruleset;
if (match.mvp.isNotEmpty) {
final ruleset = match.game.ruleset;
if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) {
// Single winner/loser, multiple winner
final names = localMatch.isTeamMatch
? localMatch.mvt.map((t) => t.name).toList()
: localMatch.mvp.map((p) => p.name).toList();
final mvpNames = names.length == 1 ? names.first : names.join(', ');
final label = ruleset == Ruleset.singleWinner
? loc.winner
: ruleset == Ruleset.singleLoser
? loc.loser
: loc.winners;
return [
Text(
label,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
SizedBox(
width: 200,
child: Text(
mvpNames,
textAlign: TextAlign.end,
if (ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser) {
return [
Text(
ruleset == Ruleset.singleWinner ? loc.winner : loc.loser,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.mvp.first.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
),
];
} else {
// No result yet
return [
Text(
loc.no_results_entered_yet,
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
),
];
];
} else if (match.game.ruleset == Ruleset.multipleWinners) {
return [
Text(
loc.winners,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Flexible(
child: Container(
padding: const EdgeInsets.only(left: 10),
child: Text(
match.mvp.map((player) => player.name).join(', '),
textAlign: TextAlign.end,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
),
),
];
}
}
// No results yet
return [
Text(
loc.no_results_entered_yet,
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
),
];
}
/// Returns the result widget for scores or placement
Widget getMultiResultRows(AppLocalizations loc) {
List<(String, int)> scores = getSortedScores();
List<(String, int)> playerScores = [];
for (var player in match.players) {
int score = match.scores[player.id]?.score ?? 0;
playerScores.add((player.name, score));
}
final ruleset = match.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
playerScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (ruleset == Ruleset.lowestScore) {
playerScores.sort((a, b) => a.$2.compareTo(b.$2));
}
return Column(
children: [
for (var i = 0; i < scores.length; i++)
for (var i = 0; i < playerScores.length; i++)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
scores[i].$1,
playerScores[i].$1,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
getResultValueText(loc, i, scores[i].$2),
getResultValueText(loc, i, playerScores[i].$2),
],
),
],
);
}
/// Returns a list of player/team names and their corresponding scores, sorted by score according to the ruleset
List<(String, int)> getSortedScores() {
List<(String, int)> namedScores = [];
if (localMatch.isTeamMatch) {
final teams = localMatch.teams ?? [];
for (var team in teams) {
int score = team.score ?? 0;
namedScores.add((team.name, score));
}
final ruleset = localMatch.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
namedScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (ruleset == Ruleset.lowestScore) {
namedScores.sort((a, b) => a.$2.compareTo(b.$2));
}
} else {
final scores = localMatch.scores;
for (var player in localMatch.players) {
int score = scores[player.id]?.score ?? 0;
namedScores.add((player.name, score));
}
final ruleset = localMatch.game.ruleset;
if (ruleset == Ruleset.highestScore || ruleset == Ruleset.placement) {
namedScores.sort((a, b) => b.$2.compareTo(a.$2));
} else if (ruleset == Ruleset.lowestScore) {
namedScores.sort((a, b) => a.$2.compareTo(b.$2));
}
}
return namedScores;
}
/// Returns the text widget for the score or placement value, styled according to the ruleset
Widget getResultValueText(AppLocalizations loc, int index, int score) {
final ruleset = localMatch.game.ruleset;
final ruleset = match.game.ruleset;
if (ruleset == Ruleset.placement) {
return Text(
@@ -462,9 +410,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Returns if the result can be displayed in a single row
bool isSingleRowResult() {
return localMatch.game.ruleset == Ruleset.singleWinner ||
localMatch.game.ruleset == Ruleset.singleLoser ||
localMatch.game.ruleset == Ruleset.multipleWinners;
return match.game.ruleset == Ruleset.singleWinner ||
match.game.ruleset == Ruleset.singleLoser ||
match.game.ruleset == Ruleset.multipleWinners;
}
String getPlacementText(BuildContext context, int rank) {
@@ -495,19 +443,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
}
}
Future<void> updateScoresForCurrentMatch() async {
if (localMatch.isTeamMatch) {
final teams = await db.teamDao.getTeamsByMatchId(matchId: localMatch.id);
setState(() {
localMatch = localMatch.copyWith(teams: teams);
});
} else {
final scores = await db.scoreEntryDao.getAllMatchScores(
matchId: localMatch.id,
);
setState(() {
localMatch = localMatch.copyWith(scores: scores);
});
}
void updateScoresForCurrentMatch() {
db.scoreEntryDao
.getAllMatchScores(matchId: match.id)
.then((scores) => match.scores = scores);
}
}

View File

@@ -39,7 +39,7 @@ class _MatchViewState extends State<MatchView> {
game: Game(
name: 'Game name',
ruleset: Ruleset.singleWinner,
color: AppColor.blue,
color: GameColor.blue,
icon: '',
),
group: Group(

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+283
const _tallee = Package(
name: 'tallee',
description: 'Tracking App for Card Games',
authors: [],
version: '0.0.33+273',
version: '0.0.35+283',
spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false,
isSdk: false,

View File

@@ -15,12 +15,11 @@ class AnimatedDialogButton extends StatefulWidget {
this.buttonConstraints,
this.buttonType = ButtonType.primary,
this.isDescructive = false,
this.content,
});
final String buttonText;
final VoidCallback? onPressed;
final VoidCallback onPressed;
final BoxConstraints? buttonConstraints;
@@ -28,8 +27,6 @@ class AnimatedDialogButton extends StatefulWidget {
final bool isDescructive;
final Widget? content;
@override
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
}
@@ -41,40 +38,28 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
Widget build(BuildContext context) {
final textStyling = _getTextStyling();
final buttonDecoration = _getButtonDecoration();
final isDisabled = widget.onPressed == null;
return IgnorePointer(
ignoring: isDisabled,
child: Opacity(
opacity: isDisabled ? 0.4 : 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: widget.buttonText == ''
? widget.content!
: Text(
widget.buttonText,
style: textStyling,
textAlign: TextAlign.center,
),
),
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,
),
),
),

View File

@@ -56,7 +56,6 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call();
},
style: ElevatedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
@@ -92,7 +91,6 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call();
},
style: OutlinedButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,
@@ -130,7 +128,6 @@ class CustomWidthButton extends StatelessWidget {
onPressed!.call();
},
style: TextButton.styleFrom(
splashFactory: NoSplash.splashFactory,
foregroundColor: textcolor,
disabledForegroundColor: disabledTextColor,
backgroundColor: buttonBackgroundColor,

View File

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

View File

@@ -1,103 +0,0 @@
import 'package:flutter/material.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
class TeamCard extends StatelessWidget {
const TeamCard({
super.key,
required this.team,
this.compact = false,
this.width = double.infinity,
});
final Team team;
final bool compact;
final double width;
@override
Widget build(BuildContext context) {
final teamColor = getColorFromGameColor(team.color);
if (compact) {
return Container(
width: width,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: teamColor.withAlpha(50),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: teamColor, width: 2),
),
child: Row(
children: [
Expanded(
child: Text(
team.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w700,
color: Colors.white,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 8),
Container(
width: 1,
height: 14,
color: Colors.white.withValues(alpha: 0.35),
),
const SizedBox(width: 8),
const Icon(Icons.people_alt_rounded, size: 14, color: Colors.white),
const SizedBox(width: 4),
Text(
'${team.members.length}',
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
);
} else {
return Container(
width: width,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
decoration: BoxDecoration(
color: teamColor.withAlpha(50),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: teamColor, width: 2),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 6,
children: [
Text(
team.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
),
Wrap(
spacing: 6,
runSpacing: 6,
children: team.members.map((player) {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
);
}).toList(),
),
],
),
);
}
}
}

View File

@@ -12,7 +12,7 @@ class GameLabel extends StatelessWidget {
final String title;
final String description;
final AppColor color;
final GameColor color;
@override
Widget build(BuildContext context) {

View File

@@ -143,9 +143,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
child: TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
onIconTap: () async {
await HapticFeedback.selectionClick();
setState(() {
onIconTap: () {
setState(() async {
await HapticFeedback.selectionClick();
// Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
@@ -252,9 +252,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
),
)
.toList();
suggestedPlayers = suggestedPlayers
.where((p) => !selectedPlayers.any((sp) => sp.id == p.id))
.toList();
}
} else {
// Otherwise, use the loaded players from the database.

View File

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

View File

@@ -7,7 +7,6 @@ import 'package:tallee/core/enums.dart';
class GameTile extends StatelessWidget {
/// A list tile widget that displays a title and description, with optional highlighting and badge.
/// - [title]: The title text displayed on the tile.
/// - [subtitle]: An optional subtitle displayed under the title.
/// - [description]: The description text displayed below the title.
/// - [onTap]: The callback invoked when the tile is tapped.
/// - [onLongPress]: The callback invoked when the tile is tapped.
@@ -18,7 +17,6 @@ class GameTile extends StatelessWidget {
super.key,
required this.title,
required this.description,
this.subtitle,
this.onTap,
this.onLongPress,
this.isHighlighted = false,
@@ -26,20 +24,25 @@ class GameTile extends StatelessWidget {
this.badgeColor,
});
/// The title text displayed on the tile.
final String title;
final String? subtitle;
/// The description text displayed below the title.
final String description;
/// The callback invoked when the tile is tapped.
final VoidCallback? onTap;
/// The callback invoked when the tile is long-pressed.
final VoidCallback? onLongPress;
/// A boolean to determine if the tile should be highlighted.
final bool isHighlighted;
/// Optional text to display in a badge on the right side of the title.
final String? badgeText;
/// Optional color for the badge background.
final Color? badgeColor;
@override
@@ -48,7 +51,7 @@ class GameTile extends StatelessWidget {
? (badgeColor!.computeLuminance() > 0.5 ? Colors.black : Colors.white)
: Colors.white;
final gameColor = badgeColor ?? getColorFromGameColor(AppColor.orange);
final gameColor = badgeColor ?? getColorFromGameColor(GameColor.orange);
return GestureDetector(
onTap: () async {
@@ -64,14 +67,13 @@ class GameTile extends StatelessWidget {
}
},
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 10),
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
decoration: !isHighlighted
? CustomTheme.standardBoxDecoration
: CustomTheme.highlightedBoxDecoration.copyWith(
border: Border.all(
color: gameColor.withValues(alpha: 0.9),
width: 2,
strokeAlign: BorderSide.strokeAlignCenter,
),
),
duration: const Duration(milliseconds: 200),
@@ -116,21 +118,6 @@ class GameTile extends StatelessWidget {
),
),
// Title
if (subtitle != null && subtitle!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
subtitle!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.hintColor,
),
),
],
// Badge
if (badgeText != null) ...[
const SizedBox(height: 5),

View File

@@ -91,6 +91,7 @@ class _GroupTileState extends State<GroupTile> {
TextIconTile(
text: member.name,
suffixText: getNameCountText(member),
iconEnabled: false,
),
],
),

View File

@@ -5,12 +5,12 @@ import 'package:tallee/core/custom_theme.dart';
class CustomCheckboxListTile extends StatelessWidget {
const CustomCheckboxListTile({
super.key,
required this.content,
required this.text,
required this.value,
required this.onChanged,
});
final Widget content;
final String text;
final bool value;
final ValueChanged<bool> onChanged;
@@ -39,7 +39,16 @@ class CustomCheckboxListTile extends StatelessWidget {
onChanged(v);
},
),
Expanded(child: content),
Expanded(
child: Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
),
],
),
),

View File

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

View File

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

View File

@@ -8,7 +8,6 @@ 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/widgets/cards/team_card.dart';
import 'package:tallee/presentation/widgets/game_label.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -18,11 +17,13 @@ class MatchTile extends StatefulWidget {
/// - [match]: The match data to be displayed.
/// - [onTap]: The callback invoked when the tile is tapped.
/// - [width]: Optional width for the tile.
/// - [compact]: Whether to display the tile in a compact mode
const MatchTile({
super.key,
required this.match,
required this.onTap,
this.width,
this.compact = false,
});
/// The match data to be displayed.
@@ -34,6 +35,9 @@ class MatchTile extends StatefulWidget {
/// Optional width for the tile.
final double? width;
/// Whether to display the tile in a compact mode
final bool compact;
@override
State<MatchTile> createState() => _MatchTileState();
}
@@ -96,59 +100,40 @@ class _MatchTileState extends State<MatchTile> {
],
),
const SizedBox(height: 4),
] else if (widget.compact) ...[
Row(
children: [
const Icon(Icons.person, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
'${match.players.length} ${loc.players}',
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 6),
] else ...[
const SizedBox(height: 8),
],
// Game + Ruleset Badge
GameLabel(
title: match.game.name,
description: translateRulesetToString(
match.game.ruleset,
context,
if (!widget.compact)
GameLabel(
title: match.game.name,
description: translateRulesetToString(
match.game.ruleset,
context,
),
color: match.game.color,
),
color: match.game.color,
),
const SizedBox(height: 12),
// Winner / In Progress Info
if (match.isTeamMatch && match.mvt.isNotEmpty) ...[
// MVT Display for team matches
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.green.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.green.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
getMvpIcon(),
const SizedBox(width: 8),
Expanded(
child: Text(
getMvtText(loc),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
] else if (match.mvp.isNotEmpty) ...[
// MVP Display for player matches
if (match.mvp.isNotEmpty) ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
@@ -182,7 +167,6 @@ class _MatchTileState extends State<MatchTile> {
),
const SizedBox(height: 12),
] else ...[
// Match in progress display
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
@@ -221,46 +205,8 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(height: 12),
],
if (match.teams != null &&
match.teams!.isNotEmpty &&
match.isTeamMatch) ...[
// Team display
Text(
loc.teams,
style: const TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
LayoutBuilder(
builder: (context, constraints) {
final useSingleColumn = match.teams!.any(
(team) => team.name.length > 10,
);
const spacing = 8.0;
final itemWidth = useSingleColumn
? constraints.maxWidth
: (constraints.maxWidth - spacing) / 2;
return Wrap(
spacing: spacing,
runSpacing: spacing,
children: match.teams!.map((team) {
return TeamCard(
team: team,
compact: true,
width: itemWidth,
);
}).toList(),
);
},
),
const SizedBox(height: 12),
] else if (players.isNotEmpty) ...[
// Player display
// Players List
if (players.isNotEmpty && widget.compact == false) ...[
Text(
loc.players,
style: const TextStyle(
@@ -277,17 +223,10 @@ class _MatchTileState extends State<MatchTile> {
return TextIconTile(
text: player.name,
suffixText: getNameCountText(player),
iconEnabled: false,
);
}).toList(),
),
] else ...[
Text(
loc.no_players_available,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.hintColor,
),
),
],
],
),
@@ -313,7 +252,6 @@ class _MatchTileState extends State<MatchTile> {
}
}
// Returns the appropriate text based on the match's ruleset and MVP.
String getMvpText(AppLocalizations loc) {
if (widget.match.mvp.isEmpty) return '';
final ruleset = widget.match.game.ruleset;
@@ -337,41 +275,11 @@ class _MatchTileState extends State<MatchTile> {
return '${loc.winner}: n.A.';
}
// Returns the appropriate text based on the match's ruleset and MVT.
String getMvtText(AppLocalizations loc) {
if (widget.match.mvt.isEmpty) return '';
final ruleset = widget.match.game.ruleset;
switch (ruleset) {
case Ruleset.singleWinner:
return '${loc.winner}: ${widget.match.mvt.first.name}';
case Ruleset.singleLoser:
return '${loc.loser}: ${widget.match.mvt.first.name}';
case Ruleset.highestScore:
case Ruleset.lowestScore:
final mvt = widget.match.mvt;
final mvtScore =
widget.match.teams!
.firstWhere((team) => team.id == mvt.first.id)
.score ??
0;
final mvtNames = mvt.map((team) => team.name).join(', ');
return '${loc.winner}: $mvtNames (${getPointLabel(loc, mvtScore)})';
case Ruleset.placement:
return '${loc.winner}: ${widget.match.mvt.first.name}';
case Ruleset.multipleWinners:
final mvtNames = widget.match.mvt.map((team) => team.name).join(', ');
return '${loc.winners}: $mvtNames';
}
}
// Returns the appropriate icon based on the match's ruleset.
Icon getMvpIcon() {
final icon = getRulesetIcon(widget.match.game.ruleset);
switch (widget.match.game.ruleset) {
case Ruleset.singleWinner:
case Ruleset.multipleWinners:
return Icon(icon, size: 20, color: Colors.amber);
case Ruleset.singleLoser:
return Icon(icon, size: 20, color: Colors.blue);
@@ -379,6 +287,8 @@ class _MatchTileState extends State<MatchTile> {
return Icon(icon, size: 20, color: Colors.orange);
case Ruleset.highestScore:
return Icon(icon, size: 20, color: Colors.green);
case Ruleset.multipleWinners:
return Icon(icon, size: 20, color: Colors.amber);
case Ruleset.placement:
return Icon(icon, size: 20, color: Colors.deepOrangeAccent);
}

View File

@@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:fluttericon/font_awesome_icons.dart';
import 'package:tallee/core/common.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
class TeamCreationTile extends StatefulWidget {
const TeamCreationTile({
super.key,
required this.color,
required this.controller,
required this.hintText,
this.onDelete,
this.onColorSelection,
});
final AppColor color;
final TextEditingController controller;
final String hintText;
final VoidCallback? onDelete;
final ValueChanged<AppColor>? onColorSelection;
@override
State<TeamCreationTile> createState() => _TeamCreationTileState();
}
class _TeamCreationTileState extends State<TeamCreationTile> {
final teamColors = List.generate(
AppColor.values.length,
(index) => getTeamColor(index),
);
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: CustomTheme.standardMargin,
decoration: CustomTheme.standardBoxDecoration,
clipBehavior: Clip.antiAlias,
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 12,
horizontal: 6,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Name input + delete icon
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: TextInputField(
controller: widget.controller,
hintText: widget.hintText,
maxLength: Constants.MAX_TEAM_NAME_LENGTH,
),
),
HapticIconButton(
icon: const Icon(FontAwesome.trash),
color: CustomTheme.textColor,
iconSize: 25,
onPressed: widget.onDelete,
),
],
),
const SizedBox(height: 12),
// Color label
Padding(
padding: const EdgeInsets.only(left: 8),
child: Text(
loc.color,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
),
),
const SizedBox(height: 8),
// Color picker
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: teamColors.map((color) {
final isSelected = widget.color == color;
return GestureDetector(
onTap: () {
widget.onColorSelection?.call(color);
},
child: Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: getColorFromGameColor(color),
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? Colors.white
: Colors.transparent,
width: 3,
),
),
child: isSelected
? const Icon(
Icons.check,
size: 18,
color: Colors.white,
)
: null,
),
);
}).toList(),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@@ -11,7 +11,6 @@ class TextIconListTile extends StatelessWidget {
required this.text,
this.suffixText = '',
this.icon,
this.color,
this.onPressed,
});
@@ -24,8 +23,6 @@ class TextIconListTile extends StatelessWidget {
/// The icon to display in the tile.
final IconData? icon;
final Color? color;
/// The callback to be invoked when the icon is pressed.
final VoidCallback? onPressed;
@@ -34,17 +31,7 @@ class TextIconListTile extends StatelessWidget {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 15),
decoration: BoxDecoration(
color:
Color.lerp(CustomTheme.onBoxColor, color?.withAlpha(10), 0.1) ??
CustomTheme.boxColor,
border: Border.all(
color: color ?? CustomTheme.boxBorderColor,
width: color != null ? 2 : 1,
strokeAlign: BorderSide.strokeAlignCenter,
),
borderRadius: CustomTheme.standardBorderRadiusAll,
),
decoration: CustomTheme.standardBoxDecoration,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.max,

View File

@@ -4,14 +4,14 @@ import 'package:tallee/core/custom_theme.dart';
class TextIconTile extends StatelessWidget {
/// A tile widget that displays text with an optional icon that can be tapped.
/// - [text]: The text to display in the tile.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed.
/// - [onIconTap]: The callback to be invoked when the icon is tapped.
/// - [icon]: Optional custom icon. Defaults to [Icons.close].
const TextIconTile({
super.key,
required this.text,
this.suffixText = '',
this.iconEnabled = true,
this.onIconTap,
this.icon = Icons.close,
});
/// The text to display in the tile.
@@ -19,16 +19,14 @@ class TextIconTile extends StatelessWidget {
final String suffixText;
/// A boolean to determine if the icon should be displayed.
final bool iconEnabled;
/// The callback to be invoked when the icon is tapped.
final VoidCallback? onIconTap;
/// The icon to display. Defaults to [Icons.close].
final IconData icon;
@override
Widget build(BuildContext context) {
final iconEnabled = onIconTap != null;
return Container(
padding: const EdgeInsets.all(5),
decoration: BoxDecoration(
@@ -67,7 +65,10 @@ class TextIconTile extends StatelessWidget {
),
if (iconEnabled) ...<Widget>[
const SizedBox(width: 3),
GestureDetector(onTap: onIconTap, child: Icon(icon, size: 20)),
GestureDetector(
onTap: onIconTap,
child: const Icon(Icons.close, size: 20),
),
],
],
),

View File

@@ -200,9 +200,13 @@ class DataTransferService {
.map((id) => playerById[id])
.whereType<Player>()
.toList();
final team = Team.fromJson(map);
return team.copyWith(members: members);
return Team(
id: map['id'] as String,
name: map['name'] as String,
members: members,
createdAt: DateTime.parse(map['createdAt'] as String),
);
}).toList();
}
@@ -227,7 +231,6 @@ class DataTransferService {
final endedAt = map['endedAt'] != null
? DateTime.parse(map['endedAt'] as String)
: null;
final isTeamMatch = map['isTeamMatch'] as bool;
final notes = map['notes'] as String? ?? '';
final scoresJson = map['scores'] as Map<String, dynamic>? ?? {};
final scores = scoresJson.map(
@@ -259,7 +262,6 @@ class DataTransferService {
game: game,
group: group,
players: players,
isTeamMatch: isTeamMatch,
teams: teams.isEmpty ? null : teams,
createdAt: createdAt,
endedAt: endedAt,
@@ -276,7 +278,7 @@ class DataTransferService {
name: 'Unknown',
ruleset: Ruleset.singleWinner,
description: '',
color: AppColor.blue,
color: GameColor.blue,
icon: '',
);
}

View File

@@ -85,10 +85,10 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6"
url: "https://pub.dev"
source: hosted
version: "2.13.1"
version: "2.15.0"
built_collection:
dependency: transitive
description:
@@ -177,14 +177,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection:
dependency: "direct main"
description:
@@ -333,26 +325,26 @@ packages:
dependency: "direct main"
description:
name: drift
sha256: "970cd188fddb111b26ea6a9b07a62bf5c2432d74147b8122c67044ae3b97e99e"
sha256: "8033500116b24398fba0cca0369cc31678cd627c01e41753a61186911cea743e"
url: "https://pub.dev"
source: hosted
version: "2.31.0"
version: "2.33.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "917184b2fb867b70a548a83bf0d36268423b38d39968c06cce4905683da49587"
sha256: b3dd5b75e30522a91da8abda9f5bb17230cb038097f6d15fa75d42bb563428aa
url: "https://pub.dev"
source: hosted
version: "2.31.0"
version: "2.33.0"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: c07120854742a0cae2f7501a0da02493addde550db6641d284983c08762e60a7
sha256: "887fdec622174dc7eaefd0048403e34ee07cc18626ac8a7544cc3b8a4a172166"
url: "https://pub.dev"
source: hosted
version: "0.2.8"
version: "0.3.0"
equatable:
dependency: transitive
description:
@@ -397,10 +389,10 @@ packages:
dependency: "direct main"
description:
name: file_saver
sha256: "9d93db09bd4da9e43238f9dd485360fc51a5c138eea5ef5f407ec56e58079ac0"
sha256: "68c9a085d9bb4546e0a31d1e583a48d7c17a6987d538788ea064f0043b1fc02d"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
version: "0.4.0"
fixnum:
dependency: transitive
description:
@@ -1122,30 +1114,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.2"
sqlcipher_flutter_libs:
dependency: transitive
description:
name: sqlcipher_flutter_libs
sha256: "38d62d659d2fb8739bf25a42c9a350d1fdd6c29a5a61f13a946778ec75d27929"
url: "https://pub.dev"
source: hosted
version: "0.7.0+eol"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5"
url: "https://pub.dev"
source: hosted
version: "2.9.4"
version: "3.3.1"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: eeb9e3a45207649076b808f8a5a74d68770d0b7f26ccef6d5f43106eee5375ad
sha256: "3ed7553eee7bb368f8950f58ba29f634e06e813c029aff6a0d60862b96de8454"
url: "https://pub.dev"
source: hosted
version: "0.5.42"
version: "0.6.0+eol"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "337e9997f7141ffdd054259128553c348635fa318f7ca492f07a4ab76f850d19"
sha256: ecdc06d4a7d79dcbc928d99afd2f7f5b0f98a637c46f89be83d911617f759978
url: "https://pub.dev"
source: hosted
version: "0.43.1"
version: "0.44.4"
stack_trace:
dependency: transitive
description:
@@ -1427,5 +1427,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.3 <4.0.0"
flutter: ">=3.38.4"
dart: ">=3.12.0 <4.0.0"
flutter: ">=3.41.0"

View File

@@ -1,19 +1,19 @@
name: tallee
description: "Tracking App for Card Games"
publish_to: 'none'
version: 0.0.33+340
version: 0.0.35+283
environment:
sdk: ^3.8.1
sdk: ^3.12.0
dependencies:
clock: ^1.1.2
collection: ^1.19.1
cupertino_icons: ^1.0.6
drift: ^2.27.0
drift_flutter: ^0.2.4
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 +24,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

@@ -56,7 +56,7 @@ void main() {
name: 'Test Game',
ruleset: Ruleset.singleWinner,
description: 'A test game',
color: AppColor.blue,
color: GameColor.blue,
icon: '',
);
testMatch1 = Match(
@@ -507,36 +507,34 @@ void main() {
deleted = await database.matchDao.deleteAllMatches();
expect(deleted, isFalse);
});
});
test('deleteMatchesByGame() deletes all matches for a game', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatch2);
test('deleteMatchesByGame() deletes all matches for a game', () async {
await database.matchDao.addMatch(match: testMatch1);
await database.matchDao.addMatch(match: testMatch2);
var count = await database.matchDao.getMatchCountByGame(
gameId: testGame.id,
);
expect(count, 2);
var count = await database.matchDao.getMatchCountByGame(
gameId: testGame.id,
);
expect(count, 2);
final deletedCount = await database.matchDao.deleteMatchesByGame(
gameId: testGame.id,
);
expect(deletedCount, 2);
final deletedCount = await database.matchDao.deleteMatchesByGame(
gameId: testGame.id,
);
expect(deletedCount, 2);
count = await database.matchDao.getMatchCountByGame(
gameId: testGame.id,
);
expect(count, 0);
count = await database.matchDao.getMatchCountByGame(gameId: testGame.id);
expect(count, 0);
final allMatches = await database.matchDao.getAllMatches();
expect(allMatches, isEmpty);
});
final allMatches = await database.matchDao.getAllMatches();
expect(allMatches, isEmpty);
});
test('deleteMatchesByGame() returns 0 for non-existent game', () async {
final deletedCount = await database.matchDao.deleteMatchesByGame(
gameId: 'non-existent-game-id',
);
expect(deletedCount, 0);
});
test('deleteMatchesByGame() returns 0 for non-existent game', () async {
final deletedCount = await database.matchDao.deleteMatchesByGame(
gameId: 'non-existent-game-id',
);
expect(deletedCount, 0);
});
});
}

View File

@@ -1,7 +1,7 @@
import 'dart:core' hide Match;
import 'package:clock/clock.dart';
import 'package:drift/drift.dart' hide isNotNull, isNull;
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:tallee/core/enums.dart';
@@ -49,7 +49,7 @@ void main() {
testGame = Game(
name: 'Test Game',
ruleset: Ruleset.highestScore,
color: AppColor.blue,
color: GameColor.blue,
icon: '',
);
testMatch1 = Match(
@@ -327,200 +327,5 @@ void main() {
expect(deleted, isFalse);
});
});
group('SCORE', () {
test('updateTeamScore() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final updated = await database.teamDao.updateTeamScore(
teamId: testTeam1.id,
matchId: testMatch1.id,
score: 5,
);
expect(updated, isTrue);
final team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 5);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 5);
}
});
test('set-/removeWinnerTeam() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setWinnerTeam(
teamId: testTeam1.id,
matchId: testMatch1.id,
);
expect(set, isTrue);
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 1);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
final removed = await database.teamDao.removeWinnerTeam(
matchId: testMatch1.id,
);
expect(removed, isTrue);
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, isNull);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
});
test('set-/removeLoserTeam() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setLoserTeam(
teamId: testTeam1.id,
matchId: testMatch1.id,
);
expect(set, isTrue);
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 0);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 0);
}
final removed = await database.teamDao.removeLoserTeam(
matchId: testMatch1.id,
);
expect(removed, isTrue);
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, isNull);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
});
test('set-/removeWinnerTeams() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setWinnerTeams(
winners: [testTeam1, testTeam2],
matchId: testMatch1.id,
);
expect(set, isTrue);
// check both teams got the winner score
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 1);
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
expect(team.score, 1);
// check all members of both teams got the winner score
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
for (final member in testTeam2.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
final removed = await database.teamDao.removeWinnerTeam(
matchId: testMatch1.id,
);
expect(removed, isTrue);
team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, isNull);
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
expect(team.score, isNull);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
for (final member in testTeam2.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNull);
}
});
test('setTeamPlacements() works correctly', () async {
await database.matchDao.addMatch(match: testMatch1);
final set = await database.teamDao.setTeamPlacements(
teams: [testTeam1, testTeam2],
matchId: testMatch1.id,
);
expect(set, isTrue);
var team = await database.teamDao.getTeamById(teamId: testTeam1.id);
expect(team.score, 2);
team = await database.teamDao.getTeamById(teamId: testTeam2.id);
expect(team.score, 1);
for (final member in testTeam1.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 2);
}
for (final member in testTeam2.members) {
final entry = await database.scoreEntryDao.getScore(
playerId: member.id,
matchId: testMatch1.id,
);
expect(entry, isNotNull);
expect(entry!.score, 1);
}
});
});
});
}

View File

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

View File

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

View File

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

View File

@@ -45,7 +45,7 @@ void main() {
name: 'Chess',
ruleset: Ruleset.singleWinner,
description: 'Strategic board game',
color: AppColor.blue,
color: GameColor.blue,
icon: 'chess_icon',
);
@@ -55,12 +55,7 @@ void main() {
members: [testPlayer1, testPlayer2],
);
testTeam = Team(
name: 'Test Team',
color: AppColor.yellow,
score: 5,
members: [testPlayer1, testPlayer2],
);
testTeam = Team(name: 'Test Team', members: [testPlayer1, testPlayer2]);
testMatch = Match(
name: 'Test Match',
@@ -142,6 +137,9 @@ void main() {
await database.playerDao.addPlayer(player: testPlayer2);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
/*
await database.teamDao.addTeam(team: testTeam);
*/
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
@@ -450,19 +448,19 @@ void main() {
Game(
name: 'Red Game',
ruleset: Ruleset.singleWinner,
color: AppColor.red,
color: GameColor.red,
icon: 'icon',
),
Game(
name: 'Blue Game',
ruleset: Ruleset.singleWinner,
color: AppColor.blue,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Green Game',
ruleset: Ruleset.singleWinner,
color: AppColor.green,
color: GameColor.green,
icon: 'icon',
),
];
@@ -486,19 +484,19 @@ void main() {
Game(
name: 'Highest Score Game',
ruleset: Ruleset.highestScore,
color: AppColor.blue,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Lowest Score Game',
ruleset: Ruleset.lowestScore,
color: AppColor.blue,
color: GameColor.blue,
icon: 'icon',
),
Game(
name: 'Single Winner',
ruleset: Ruleset.singleWinner,
color: AppColor.blue,
color: GameColor.blue,
icon: 'icon',
),
];
@@ -671,8 +669,6 @@ void main() {
'name': testTeam.name,
'memberIds': [testPlayer1.id],
'createdAt': testTeam.createdAt.toIso8601String(),
'color': testTeam.color.name,
'score': testTeam.score,
},
];
@@ -686,8 +682,6 @@ void main() {
expect(teams[0].name, testTeam.name);
expect(teams[0].members.length, 1);
expect(teams[0].members[0].id, testPlayer1.id);
expect(teams[0].color, testTeam.color);
expect(teams[0].score, testTeam.score);
});
test('parseTeamsFromJson() empty list', () {
@@ -724,9 +718,6 @@ void main() {
'gameId': testGame.id,
'groupId': testGroup.id,
'playerIds': [testPlayer1.id, testPlayer2.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': testMatch.notes,
'createdAt': testMatch.createdAt.toIso8601String(),
},
@@ -782,9 +773,6 @@ void main() {
'name': testMatch.name,
'gameId': 'non-existent-game-id',
'playerIds': [testPlayer1.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
},
@@ -816,9 +804,6 @@ void main() {
'gameId': testGame.id,
'groupId': null,
'playerIds': [testPlayer1.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
},
@@ -849,9 +834,6 @@ void main() {
'name': testMatch.name,
'gameId': testGame.id,
'playerIds': [testPlayer1.id],
'isTeamMatch': false,
'teams': null,
'scores': null,
'notes': '',
'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': endedDate.toIso8601String(),
@@ -871,7 +853,7 @@ void main() {
});
});
test('validateJsonSchema() works correctly', () async {
test('validateJsonSchema()', () async {
final validJson = json.encode({
'players': [
{
@@ -915,15 +897,6 @@ void main() {
},
'createdAt': testMatch.createdAt.toIso8601String(),
'endedAt': null,
'isTeamMatch': true,
'teams': [
{
'id': testTeam.id,
'name': testTeam.name,
'memberIds': [testPlayer1.id, testPlayer2.id],
'createdAt': testTeam.createdAt.toIso8601String(),
},
],
},
],
});
@@ -931,28 +904,5 @@ void main() {
final isValid = await DataTransferService.validateJsonSchema(validJson);
expect(isValid, true);
});
testWidgets('validateJsonSchema() validates exported json file', (
tester,
) async {
await database.playerDao.addPlayer(player: testPlayer1);
await database.playerDao.addPlayer(player: testPlayer2);
await database.gameDao.addGame(game: testGame);
await database.groupDao.addGroup(group: testGroup);
await database.matchDao.addMatch(match: testMatch);
final ctx = await getContext(tester);
final jsonString = await DataTransferService.getAppDataAsJson(ctx);
expect(jsonString, isNotEmpty);
// Schema validation requires real async operations (rootBundle,
// HttpClient within json_schema). These must run via
// tester.runAsync, otherwise the test hangs due to a pending timer.
final isValid = await tester.runAsync(
() => DataTransferService.validateJsonSchema(jsonString),
);
expect(isValid, true);
});
});
}