diff --git a/.gitea/workflows/pull_request.yaml b/.gitea/workflows/pull_request.yaml new file mode 100644 index 0000000..43d36d2 --- /dev/null +++ b/.gitea/workflows/pull_request.yaml @@ -0,0 +1,57 @@ +name: Pull Request Pipeline + +on: + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install jq + run: | + apt-get update + apt-get install -y jq + + - name: Install Flutter (wget) + run: | + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + tar xf flutter_linux_3.38.2-stable.tar.xz + # Set Git safe directory for Flutter path + git config --global --add safe.directory "$(pwd)/flutter" + # Set Flutter path + echo "$(pwd)/flutter/bin" >> $GITHUB_PATH + + - name: Get dependencies + run: flutter pub get + + - name: Analyze Formatting + run: flutter analyze lib test + + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt-get update + apt-get install -y jq + + - name: Install Flutter (wget) + run: | + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + tar xf flutter_linux_3.38.2-stable.tar.xz + # Set Git safe directory for Flutter path + git config --global --add safe.directory "$(pwd)/flutter" + # Set Flutter path + echo "$(pwd)/flutter/bin" >> $GITHUB_PATH + + - name: Get dependencies + run: flutter pub get + + - name: Run tests + run: flutter test \ No newline at end of file diff --git a/.gitea/workflows/push.yaml b/.gitea/workflows/push.yaml new file mode 100644 index 0000000..700e96b --- /dev/null +++ b/.gitea/workflows/push.yaml @@ -0,0 +1,50 @@ +name: Push Pipeline + +on: + push: + branches: + - "development" + - "main" + +jobs: + format: + runs-on: ubuntu-latest + if: false # Needs bot user + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + apt-get update + apt-get install -y jq + + - name: Install Flutter (wget) + run: | + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.38.2-stable.tar.xz + tar xf flutter_linux_3.38.2-stable.tar.xz + # Set Git safe directory for Flutter path + git config --global --add safe.directory "$(pwd)/flutter" + # Set Flutter path + echo "$(pwd)/flutter/bin" >> $GITHUB_PATH + + - name: Get & upgrade dependencies + run: | + flutter pub get + flutter pub upgrade --major-versions + + - name: Auto-format + run: | + dart format lib + dart fix --apply lib + + # Needs credentials, push access and the right files need to be staged + - name: Commit Changes + run: | + git config --global user.name "Gitea Actions" + git config --global user.email "actions@gitea.com" + git status + git add lib/ + git status + git commit -m "Actions: Auto-formatting [skip ci]" + git push diff --git a/.gitignore b/.gitignore index e000548..72eb56e 100644 --- a/.gitignore +++ b/.gitignore @@ -195,3 +195,4 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/devtools_options.yaml diff --git a/.metadata b/.metadata index dc84ef0..85b54cb 100644 --- a/.metadata +++ b/.metadata @@ -15,19 +15,7 @@ migration: - platform: root create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - - platform: android - create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - - platform: linux - create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - - platform: macos - create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - - platform: web - create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 - - platform: windows + - platform: ios create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 4cded1b..7e89f4b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ - + + @color/app_icon_background + \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..da964ae 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -4,7 +4,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..3ff2769 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..d6b8994 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,47 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Game Tracker + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + game_tracker + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..f7805c8 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,5 @@ +arb-dir: lib/l10n/arb +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart +output-dir: lib/l10n/generated +nullable-getter: false \ No newline at end of file diff --git a/lib/core/constants.dart b/lib/core/constants.dart new file mode 100644 index 0000000..8d3c8cc --- /dev/null +++ b/lib/core/constants.dart @@ -0,0 +1,6 @@ +class Constants { + Constants._(); // Private constructor to prevent instantiation + + /// Minimum duration of all app skeletons + static Duration minimumSkeletonDuration = const Duration(milliseconds: 250); +} diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart new file mode 100644 index 0000000..a6c6376 --- /dev/null +++ b/lib/core/custom_theme.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +class CustomTheme { + CustomTheme._(); // Private constructor to prevent instantiation + + // ==================== Colors ==================== + static Color primaryColor = const Color(0xFF7505E4); + static Color secondaryColor = const Color(0xFFAFA2FF); + static Color backgroundColor = const Color(0xFF0B0B0B); + static Color boxColor = const Color(0xFF101010); + static Color onBoxColor = const Color(0xFF181818); + static Color boxBorder = const Color(0xFF272727); + static const Color textColor = Colors.white; + + // ==================== Border Radius ==================== + static const double standardBorderRadius = 12.0; + static BorderRadius get standardBorderRadiusAll => + BorderRadius.circular(standardBorderRadius); + + // ==================== Padding & Margins ==================== + static const EdgeInsets standardMargin = EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ); + static const EdgeInsets tileMargin = EdgeInsets.symmetric( + horizontal: 12, + vertical: 5, + ); + + // ==================== Decorations ==================== + static BoxDecoration standardBoxDecoration = BoxDecoration( + color: boxColor, + border: Border.all(color: boxBorder), + borderRadius: standardBorderRadiusAll, + ); + + static BoxDecoration highlightedBoxDecoration = BoxDecoration( + color: boxColor, + border: Border.all(color: primaryColor), + borderRadius: standardBorderRadiusAll, + boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], + ); + + // ==================== App Bar Theme ==================== + static AppBarTheme appBarTheme = AppBarTheme( + backgroundColor: backgroundColor, + foregroundColor: textColor, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + titleTextStyle: const TextStyle( + color: textColor, + fontSize: 20, + fontWeight: FontWeight.bold, + overflow: TextOverflow.ellipsis, + ), + iconTheme: const IconThemeData(color: textColor), + ); +} diff --git a/lib/core/enums.dart b/lib/core/enums.dart new file mode 100644 index 0000000..17a01f6 --- /dev/null +++ b/lib/core/enums.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; + +/// Button types used for styling the [CustomWidthButton] +/// - [ButtonType.primary]: Primary button style. +/// - [ButtonType.secondary]: Secondary button style. +/// - [ButtonType.tertiary]: Tertiary button style. +enum ButtonType { primary, secondary, tertiary } + +/// Result types for import operations in the [SettingsView] +/// - [ImportResult.success]: The import operation was successful. +/// - [ImportResult.canceled]: The import operation was canceled by the user. +/// - [ImportResult.fileReadError]: There was an error reading the selected file. +/// - [ImportResult.invalidSchema]: The JSON schema of the imported data is invalid. +/// - [ImportResult.formatException]: A format exception occurred during import. +/// - [ImportResult.unknownException]: An exception occurred during import. +enum ImportResult { + success, + canceled, + fileReadError, + invalidSchema, + formatException, + unknownException, +} + +/// Result types for export operations in the [SettingsView] +/// - [ExportResult.success]: The export operation was successful. +/// - [ExportResult.canceled]: The export operation was canceled by the user. +/// - [ExportResult.unknownException]: An exception occurred during export. +enum ExportResult { success, canceled, unknownException } + +/// Different rulesets available for matches +/// - [Ruleset.singleWinner]: The match is won by a single player +/// - [Ruleset.singleLoser]: The match is lost by a single player +/// - [Ruleset.mostPoints]: The player with the most points wins. +/// - [Ruleset.leastPoints]: The player with the fewest points wins. +enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints } + +/// Translates a [Ruleset] enum value to its corresponding localized string. +String translateRulesetToString(Ruleset ruleset, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (ruleset) { + case Ruleset.singleWinner: + return loc.single_winner; + case Ruleset.singleLoser: + return loc.single_loser; + case Ruleset.mostPoints: + return loc.most_points; + case Ruleset.leastPoints: + return loc.least_points; + } +} diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart new file mode 100644 index 0000000..98c602a --- /dev/null +++ b/lib/data/dao/group_dao.dart @@ -0,0 +1,214 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/db/tables/group_table.dart'; +import 'package:game_tracker/data/db/tables/player_group_table.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +part 'group_dao.g.dart'; + +@DriftAccessor(tables: [GroupTable, PlayerGroupTable]) +class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { + GroupDao(super.db); + + /// Retrieves all groups from the database. + Future> getAllGroups() async { + final query = select(groupTable); + final result = await query.get(); + return Future.wait( + result.map((groupData) async { + final members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupData.id, + ); + return Group( + id: groupData.id, + name: groupData.name, + members: members, + createdAt: groupData.createdAt, + ); + }), + ); + } + + /// Retrieves a [Group] by its [groupId], including its members. + Future getGroupById({required String groupId}) async { + final query = select(groupTable)..where((g) => g.id.equals(groupId)); + final result = await query.getSingle(); + + List members = await db.playerGroupDao.getPlayersOfGroup( + groupId: groupId, + ); + + return Group( + id: result.id, + name: result.name, + members: members, + createdAt: result.createdAt, + ); + } + + /// Adds a new group with the given [id] and [name] to the database. + /// This method also adds the group's members to the [PlayerGroupTable]. + Future addGroup({required Group group}) async { + if (!await groupExists(groupId: group.id)) { + await db.transaction(() async { + await into(groupTable).insert( + GroupTableCompanion.insert( + id: group.id, + name: group.name, + createdAt: group.createdAt, + ), + mode: InsertMode.insertOrReplace, + ); + await Future.wait( + group.members.map((player) => db.playerDao.addPlayer(player: player)), + ); + await db.batch( + (b) => b.insertAll( + db.playerGroupTable, + group.members + .map( + (member) => PlayerGroupTableCompanion.insert( + playerId: member.id, + groupId: group.id, + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + }); + return true; + } + return false; + } + + /// Adds multiple groups to the database. + /// Also adds the group's members to the [PlayerGroupTable]. + Future addGroupsAsList({required List groups}) async { + if (groups.isEmpty) return; + await db.transaction(() async { + // Deduplicate groups by id - keep first occurrence + final Map uniqueGroups = {}; + for (final g in groups) { + uniqueGroups.putIfAbsent(g.id, () => g); + } + + // Insert unique groups in batch + // Using insertOrIgnore to avoid triggering cascade deletes on + // player_group associations when groups already exist + await db.batch( + (b) => b.insertAll( + groupTable, + uniqueGroups.values + .map( + (group) => GroupTableCompanion.insert( + id: group.id, + name: group.name, + createdAt: group.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + + // Collect unique players from all groups + final uniquePlayers = {}; + for (final g in uniqueGroups.values) { + for (final m in g.members) { + uniquePlayers[m.id] = m; + } + } + + if (uniquePlayers.isNotEmpty) { + // Using insertOrIgnore to avoid triggering cascade deletes on + // player_group associations when players already exist + await db.batch( + (b) => b.insertAll( + db.playerTable, + uniquePlayers.values + .map( + (p) => PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + } + + // Prepare all player-group associations in one list (unique pairs) + final Set seenPairs = {}; + final List pgRows = []; + for (final g in uniqueGroups.values) { + for (final m in g.members) { + final key = '${m.id}|${g.id}'; + if (!seenPairs.contains(key)) { + seenPairs.add(key); + pgRows.add( + PlayerGroupTableCompanion.insert(playerId: m.id, groupId: g.id), + ); + } + } + } + + if (pgRows.isNotEmpty) { + await db.batch((b) { + for (final pg in pgRows) { + b.insert(db.playerGroupTable, pg, mode: InsertMode.insertOrReplace); + } + }); + } + }); + } + + /// Deletes the group with the given [id] from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteGroup({required String groupId}) async { + final query = (delete(groupTable)..where((g) => g.id.equals(groupId))); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Updates the name of the group with the given [id] to [newName]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateGroupname({ + required String groupId, + required String newName, + }) async { + final rowsAffected = + await (update(groupTable)..where((g) => g.id.equals(groupId))).write( + GroupTableCompanion(name: Value(newName)), + ); + return rowsAffected > 0; + } + + /// Retrieves the number of groups in the database. + Future getGroupCount() async { + final count = + await (selectOnly(groupTable)..addColumns([groupTable.id.count()])) + .map((row) => row.read(groupTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Checks if a group with the given [groupId] exists in the database. + /// Returns `true` if the group exists, `false` otherwise. + Future groupExists({required String groupId}) async { + final query = select(groupTable)..where((g) => g.id.equals(groupId)); + final result = await query.getSingleOrNull(); + return result != null; + } + + /// Deletes all groups from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllGroups() async { + final query = delete(groupTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } +} diff --git a/lib/data/dao/group_dao.g.dart b/lib/data/dao/group_dao.g.dart new file mode 100644 index 0000000..b9534b4 --- /dev/null +++ b/lib/data/dao/group_dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'group_dao.dart'; + +// ignore_for_file: type=lint +mixin _$GroupDaoMixin on DatabaseAccessor { + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $PlayerTableTable get playerTable => attachedDatabase.playerTable; + $PlayerGroupTableTable get playerGroupTable => + attachedDatabase.playerGroupTable; +} diff --git a/lib/data/dao/group_match_dao.dart b/lib/data/dao/group_match_dao.dart new file mode 100644 index 0000000..d428fb5 --- /dev/null +++ b/lib/data/dao/group_match_dao.dart @@ -0,0 +1,98 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/db/tables/group_match_table.dart'; +import 'package:game_tracker/data/dto/group.dart'; + +part 'group_match_dao.g.dart'; + +@DriftAccessor(tables: [GroupMatchTable]) +class GroupMatchDao extends DatabaseAccessor + with _$GroupMatchDaoMixin { + GroupMatchDao(super.db); + + /// Associates a group with a match by inserting a record into the + /// [GroupMatchTable]. + Future addGroupToMatch({ + required String matchId, + required String groupId, + }) async { + if (await matchHasGroup(matchId: matchId)) { + throw Exception('Match already has a group'); + } + await into(groupMatchTable).insert( + GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId), + mode: InsertMode.insertOrIgnore, + ); + } + + /// Retrieves the [Group] associated with the given [matchId]. + /// Returns `null` if no group is found. + Future getGroupOfMatch({required String matchId}) async { + final result = await (select( + groupMatchTable, + )..where((g) => g.matchId.equals(matchId))).getSingleOrNull(); + + if (result == null) { + return null; + } + + final group = await db.groupDao.getGroupById(groupId: result.groupId); + return group; + } + + /// Checks if there is a group associated with the given [matchId]. + /// Returns `true` if there is a group, otherwise `false`. + Future matchHasGroup({required String matchId}) async { + final count = + await (selectOnly(groupMatchTable) + ..where(groupMatchTable.matchId.equals(matchId)) + ..addColumns([groupMatchTable.groupId.count()])) + .map((row) => row.read(groupMatchTable.groupId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Checks if a specific group is associated with a specific match. + /// Returns `true` if the group is in the match, otherwise `false`. + Future isGroupInMatch({ + required String matchId, + required String groupId, + }) async { + final count = + await (selectOnly(groupMatchTable) + ..where( + groupMatchTable.matchId.equals(matchId) & + groupMatchTable.groupId.equals(groupId), + ) + ..addColumns([groupMatchTable.groupId.count()])) + .map((row) => row.read(groupMatchTable.groupId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Removes the association of a group from a match based on [groupId] and + /// [matchId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removeGroupFromMatch({ + required String matchId, + required String groupId, + }) async { + final query = delete(groupMatchTable) + ..where((g) => g.matchId.equals(matchId) & g.groupId.equals(groupId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Updates the group associated with a match to [newGroupId] based on + /// [matchId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateGroupOfMatch({ + required String matchId, + required String newGroupId, + }) async { + final updatedRows = + await (update(groupMatchTable)..where((g) => g.matchId.equals(matchId))) + .write(GroupMatchTableCompanion(groupId: Value(newGroupId))); + return updatedRows > 0; + } +} diff --git a/lib/data/dao/group_match_dao.g.dart b/lib/data/dao/group_match_dao.g.dart new file mode 100644 index 0000000..5cc0b82 --- /dev/null +++ b/lib/data/dao/group_match_dao.g.dart @@ -0,0 +1,10 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'group_match_dao.dart'; + +// ignore_for_file: type=lint +mixin _$GroupMatchDaoMixin on DatabaseAccessor { + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; + $GroupMatchTableTable get groupMatchTable => attachedDatabase.groupMatchTable; +} diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart new file mode 100644 index 0000000..160686a --- /dev/null +++ b/lib/data/dao/match_dao.dart @@ -0,0 +1,326 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/db/tables/match_table.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +part 'match_dao.g.dart'; + +@DriftAccessor(tables: [MatchTable]) +class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { + MatchDao(super.db); + + /// Retrieves all matches from the database. + Future> getAllMatches() async { + final query = select(matchTable); + final result = await query.get(); + + return Future.wait( + result.map((row) async { + final group = await db.groupMatchDao.getGroupOfMatch(matchId: row.id); + final players = await db.playerMatchDao.getPlayersOfMatch( + matchId: row.id, + ); + final winner = row.winnerId != null + ? await db.playerDao.getPlayerById(playerId: row.winnerId!) + : null; + return Match( + id: row.id, + name: row.name, + group: group, + players: players, + createdAt: row.createdAt, + winner: winner, + ); + }), + ); + } + + /// Retrieves a [Match] by its [matchId]. + Future getMatchById({required String matchId}) async { + final query = select(matchTable)..where((g) => g.id.equals(matchId)); + final result = await query.getSingle(); + + List? players; + if (await db.playerMatchDao.matchHasPlayers(matchId: matchId)) { + players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId); + } + Group? group; + if (await db.groupMatchDao.matchHasGroup(matchId: matchId)) { + group = await db.groupMatchDao.getGroupOfMatch(matchId: matchId); + } + Player? winner; + if (result.winnerId != null) { + winner = await db.playerDao.getPlayerById(playerId: result.winnerId!); + } + + return Match( + id: result.id, + name: result.name, + players: players, + group: group, + winner: winner, + createdAt: result.createdAt, + ); + } + + /// Adds a new [Match] to the database. Also adds players and group + /// associations. This method assumes that the players and groups added to + /// this match are already present in the database. + Future addMatch({required Match match}) async { + await db.transaction(() async { + await into(matchTable).insert( + MatchTableCompanion.insert( + id: match.id, + name: match.name, + winnerId: Value(match.winner?.id), + createdAt: match.createdAt, + ), + mode: InsertMode.insertOrReplace, + ); + + if (match.players != null) { + for (final p in match.players ?? []) { + await db.playerMatchDao.addPlayerToMatch( + matchId: match.id, + playerId: p.id, + ); + } + } + + if (match.group != null) { + await db.groupMatchDao.addGroupToMatch( + matchId: match.id, + groupId: match.group!.id, + ); + } + }); + } + + /// Adds multiple [Match]s to the database in a batch operation. + /// Also adds associated players and groups if they exist. + /// If the [matches] list is empty, the method returns immediately. + /// This Method should only be used to import matches from a different device. + Future addMatchAsList({required List matches}) async { + if (matches.isEmpty) return; + await db.transaction(() async { + // Add all matches in batch + await db.batch( + (b) => b.insertAll( + matchTable, + matches + .map( + (match) => MatchTableCompanion.insert( + id: match.id, + name: match.name, + createdAt: match.createdAt, + winnerId: Value(match.winner?.id), + ), + ) + .toList(), + mode: InsertMode.insertOrReplace, + ), + ); + + // Add all groups of the matches in batch + // Using insertOrIgnore to avoid overwriting existing groups (which would + // trigger cascade deletes on player_group associations) + await db.batch( + (b) => b.insertAll( + db.groupTable, + matches + .where((match) => match.group != null) + .map( + (matches) => GroupTableCompanion.insert( + id: matches.group!.id, + name: matches.group!.name, + createdAt: matches.group!.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + + // Add all players of the matches in batch (unique) + final uniquePlayers = {}; + for (final match in matches) { + if (match.players != null) { + for (final p in match.players!) { + uniquePlayers[p.id] = p; + } + } + // Also include members of groups + if (match.group != null) { + for (final m in match.group!.members) { + uniquePlayers[m.id] = m; + } + } + } + + if (uniquePlayers.isNotEmpty) { + // Using insertOrIgnore to avoid triggering cascade deletes on + // player_group/player_match associations when players already exist + await db.batch( + (b) => b.insertAll( + db.playerTable, + uniquePlayers.values + .map( + (p) => PlayerTableCompanion.insert( + id: p.id, + name: p.name, + createdAt: p.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + } + + // Add all player-match associations in batch + await db.batch((b) { + for (final match in matches) { + if (match.players != null) { + for (final p in match.players ?? []) { + b.insert( + db.playerMatchTable, + PlayerMatchTableCompanion.insert( + matchId: match.id, + playerId: p.id, + ), + mode: InsertMode.insertOrIgnore, + ); + } + } + } + }); + + // Add all player-group associations in batch + await db.batch((b) { + for (final match in matches) { + if (match.group != null) { + for (final m in match.group!.members) { + b.insert( + db.playerGroupTable, + PlayerGroupTableCompanion.insert( + playerId: m.id, + groupId: match.group!.id, + ), + mode: InsertMode.insertOrIgnore, + ); + } + } + } + }); + + // Add all group-match associations in batch + await db.batch((b) { + for (final match in matches) { + if (match.group != null) { + b.insert( + db.groupMatchTable, + GroupMatchTableCompanion.insert( + matchId: match.id, + groupId: match.group!.id, + ), + mode: InsertMode.insertOrIgnore, + ); + } + } + }); + }); + } + + /// Deletes the match with the given [matchId] from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteMatch({required String matchId}) async { + final query = delete(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Retrieves the number of matches in the database. + Future getMatchCount() async { + final count = + await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) + .map((row) => row.read(matchTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Checks if a match with the given [matchId] exists in the database. + /// Returns `true` if the match exists, otherwise `false`. + Future matchExists({required String matchId}) async { + final query = select(matchTable)..where((g) => g.id.equals(matchId)); + final result = await query.getSingleOrNull(); + return result != null; + } + + /// Deletes all matches from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllMatches() async { + final query = delete(matchTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Sets the winner of the match with the given [matchId] to the player with + /// the given [winnerId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future setWinner({ + required String matchId, + required String winnerId, + }) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + MatchTableCompanion(winnerId: Value(winnerId)), + ); + return rowsAffected > 0; + } + + /// Retrieves the winner of the match with the given [matchId]. + /// Returns the [Player] who won the match, or `null` if no winner is set. + Future getWinner({required String matchId}) async { + final query = select(matchTable)..where((g) => g.id.equals(matchId)); + final result = await query.getSingleOrNull(); + if (result == null || result.winnerId == null) { + return null; + } + final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!); + return winner; + } + + /// Removes the winner of the match with the given [matchId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removeWinner({required String matchId}) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + const MatchTableCompanion(winnerId: Value(null)), + ); + return rowsAffected > 0; + } + + /// Checks if the match with the given [matchId] has a winner set. + /// Returns `true` if a winner is set, otherwise `false`. + Future hasWinner({required String matchId}) async { + final query = select(matchTable) + ..where((g) => g.id.equals(matchId) & g.winnerId.isNotNull()); + final result = await query.getSingleOrNull(); + return result != null; + } + + /// Changes the title of the match with the given [matchId] to [newName]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future updateMatchName({ + required String matchId, + required String newName, + }) async { + final query = update(matchTable)..where((g) => g.id.equals(matchId)); + final rowsAffected = await query.write( + MatchTableCompanion(name: Value(newName)), + ); + return rowsAffected > 0; + } +} diff --git a/lib/data/dao/match_dao.g.dart b/lib/data/dao/match_dao.g.dart new file mode 100644 index 0000000..a9f6f4c --- /dev/null +++ b/lib/data/dao/match_dao.g.dart @@ -0,0 +1,8 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'match_dao.dart'; + +// ignore_for_file: type=lint +mixin _$MatchDaoMixin on DatabaseAccessor { + $MatchTableTable get matchTable => attachedDatabase.matchTable; +} diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart new file mode 100644 index 0000000..c8db800 --- /dev/null +++ b/lib/data/dao/player_dao.dart @@ -0,0 +1,119 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/db/tables/player_table.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +part 'player_dao.g.dart'; + +@DriftAccessor(tables: [PlayerTable]) +class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { + PlayerDao(super.db); + + /// Retrieves all players from the database. + Future> getAllPlayers() async { + final query = select(playerTable); + final result = await query.get(); + return result + .map( + (row) => Player(id: row.id, name: row.name, createdAt: row.createdAt), + ) + .toList(); + } + + /// Retrieves a [Player] by their [id]. + Future getPlayerById({required String playerId}) async { + final query = select(playerTable)..where((p) => p.id.equals(playerId)); + final result = await query.getSingle(); + return Player( + id: result.id, + name: result.name, + createdAt: result.createdAt, + ); + } + + /// Adds a new [player] to the database. + /// If a player with the same ID already exists, updates their name to + /// the new one. + Future addPlayer({required Player player}) async { + if (!await playerExists(playerId: player.id)) { + await into(playerTable).insert( + PlayerTableCompanion.insert( + id: player.id, + name: player.name, + createdAt: player.createdAt, + ), + mode: InsertMode.insertOrReplace, + ); + return true; + } + return false; + } + + /// Adds multiple [players] to the database in a batch operation. + /// Uses insertOrIgnore to avoid triggering cascade deletes on + /// player_group associations when players already exist. + Future addPlayersAsList({required List players}) async { + if (players.isEmpty) return false; + + await db.batch( + (b) => b.insertAll( + playerTable, + players + .map( + (player) => PlayerTableCompanion.insert( + id: player.id, + name: player.name, + createdAt: player.createdAt, + ), + ) + .toList(), + mode: InsertMode.insertOrIgnore, + ), + ); + + return true; + } + + /// Deletes the player with the given [id] from the database. + /// Returns `true` if the player was deleted, `false` if the player did not exist. + Future deletePlayer({required String playerId}) async { + final query = delete(playerTable)..where((p) => p.id.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Checks if a player with the given [id] exists in the database. + /// Returns `true` if the player exists, `false` otherwise. + Future playerExists({required String playerId}) async { + final query = select(playerTable)..where((p) => p.id.equals(playerId)); + final result = await query.getSingleOrNull(); + return result != null; + } + + /// Updates the name of the player with the given [playerId] to [newName]. + Future updatePlayername({ + required String playerId, + required String newName, + }) async { + await (update(playerTable)..where((p) => p.id.equals(playerId))).write( + PlayerTableCompanion(name: Value(newName)), + ); + } + + /// Retrieves the total count of players in the database. + Future getPlayerCount() async { + final count = + await (selectOnly(playerTable)..addColumns([playerTable.id.count()])) + .map((row) => row.read(playerTable.id.count())) + .getSingle(); + return count ?? 0; + } + + /// Deletes all players from the database. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future deleteAllPlayers() async { + final query = delete(playerTable); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } +} diff --git a/lib/data/dao/player_dao.g.dart b/lib/data/dao/player_dao.g.dart new file mode 100644 index 0000000..c517581 --- /dev/null +++ b/lib/data/dao/player_dao.g.dart @@ -0,0 +1,8 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'player_dao.dart'; + +// ignore_for_file: type=lint +mixin _$PlayerDaoMixin on DatabaseAccessor { + $PlayerTableTable get playerTable => attachedDatabase.playerTable; +} diff --git a/lib/data/dao/player_group_dao.dart b/lib/data/dao/player_group_dao.dart new file mode 100644 index 0000000..db45735 --- /dev/null +++ b/lib/data/dao/player_group_dao.dart @@ -0,0 +1,79 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/db/tables/player_group_table.dart'; +import 'package:game_tracker/data/db/tables/player_table.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +part 'player_group_dao.g.dart'; + +@DriftAccessor(tables: [PlayerGroupTable, PlayerTable]) +class PlayerGroupDao extends DatabaseAccessor + with _$PlayerGroupDaoMixin { + PlayerGroupDao(super.db); + + /// No need for a groupHasPlayers method since the members attribute is + /// not nullable + + /// Adds a [player] to a group with the given [groupId]. + /// If the player is already in the group, no action is taken. + /// If the player does not exist in the player table, they are added. + /// Returns `true` if the player was added, otherwise `false`. + Future addPlayerToGroup({ + required Player player, + required String groupId, + }) async { + if (await isPlayerInGroup(playerId: player.id, groupId: groupId)) { + return false; + } + + if (!await db.playerDao.playerExists(playerId: player.id)) { + db.playerDao.addPlayer(player: player); + } + + await into(playerGroupTable).insert( + PlayerGroupTableCompanion.insert(playerId: player.id, groupId: groupId), + ); + + return true; + } + + /// Retrieves all players belonging to a specific group by [groupId]. + Future> getPlayersOfGroup({required String groupId}) async { + final query = select(playerGroupTable) + ..where((pG) => pG.groupId.equals(groupId)); + final result = await query.get(); + + List groupMembers = List.empty(growable: true); + + for (var entry in result) { + final player = await db.playerDao.getPlayerById(playerId: entry.playerId); + groupMembers.add(player); + } + + return groupMembers; + } + + /// Removes a player from a group based on [playerId] and [groupId]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removePlayerFromGroup({ + required String playerId, + required String groupId, + }) async { + final query = delete(playerGroupTable) + ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Checks if a player with [playerId] is in the group with [groupId]. + /// Returns `true` if the player is in the group, otherwise `false`. + Future isPlayerInGroup({ + required String playerId, + required String groupId, + }) async { + final query = select(playerGroupTable) + ..where((p) => p.playerId.equals(playerId) & p.groupId.equals(groupId)); + final result = await query.getSingleOrNull(); + return result != null; + } +} diff --git a/lib/data/dao/player_group_dao.g.dart b/lib/data/dao/player_group_dao.g.dart new file mode 100644 index 0000000..d54f979 --- /dev/null +++ b/lib/data/dao/player_group_dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'player_group_dao.dart'; + +// ignore_for_file: type=lint +mixin _$PlayerGroupDaoMixin on DatabaseAccessor { + $PlayerTableTable get playerTable => attachedDatabase.playerTable; + $GroupTableTable get groupTable => attachedDatabase.groupTable; + $PlayerGroupTableTable get playerGroupTable => + attachedDatabase.playerGroupTable; +} diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart new file mode 100644 index 0000000..7ebaee6 --- /dev/null +++ b/lib/data/dao/player_match_dao.dart @@ -0,0 +1,130 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/db/tables/player_match_table.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +part 'player_match_dao.g.dart'; + +@DriftAccessor(tables: [PlayerMatchTable]) +class PlayerMatchDao extends DatabaseAccessor + with _$PlayerMatchDaoMixin { + PlayerMatchDao(super.db); + + /// Associates a player with a match by inserting a record into the + /// [PlayerMatchTable]. + Future addPlayerToMatch({ + required String matchId, + required String playerId, + }) async { + await into(playerMatchTable).insert( + PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId), + mode: InsertMode.insertOrIgnore, + ); + } + + /// Retrieves a list of [Player]s associated with the given [matchId]. + /// Returns null if no players are found. + Future?> getPlayersOfMatch({required String matchId}) async { + final result = await (select( + playerMatchTable, + )..where((p) => p.matchId.equals(matchId))).get(); + + if (result.isEmpty) return null; + + final futures = result.map( + (row) => db.playerDao.getPlayerById(playerId: row.playerId), + ); + final players = await Future.wait(futures); + return players; + } + + /// Checks if there are any players associated with the given [matchId]. + /// Returns `true` if there are players, otherwise `false`. + Future matchHasPlayers({required String matchId}) async { + final count = + await (selectOnly(playerMatchTable) + ..where(playerMatchTable.matchId.equals(matchId)) + ..addColumns([playerMatchTable.playerId.count()])) + .map((row) => row.read(playerMatchTable.playerId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Checks if a specific player is associated with a specific match. + /// Returns `true` if the player is in the match, otherwise `false`. + Future isPlayerInMatch({ + required String matchId, + required String playerId, + }) async { + final count = + await (selectOnly(playerMatchTable) + ..where(playerMatchTable.matchId.equals(matchId)) + ..where(playerMatchTable.playerId.equals(playerId)) + ..addColumns([playerMatchTable.playerId.count()])) + .map((row) => row.read(playerMatchTable.playerId.count())) + .getSingle(); + return (count ?? 0) > 0; + } + + /// Removes the association of a player with a match by deleting the record + /// from the [PlayerMatchTable]. + /// Returns `true` if more than 0 rows were affected, otherwise `false`. + Future removePlayerFromMatch({ + required String matchId, + required String playerId, + }) async { + final query = delete(playerMatchTable) + ..where((pg) => pg.matchId.equals(matchId)) + ..where((pg) => pg.playerId.equals(playerId)); + final rowsAffected = await query.go(); + return rowsAffected > 0; + } + + /// Updates the players associated with a match based on the provided + /// [newPlayer] list. It adds new players and removes players that are no + /// longer associated with the match. + Future updatePlayersFromMatch({ + required String matchId, + required List newPlayer, + }) async { + final currentPlayers = await getPlayersOfMatch(matchId: matchId); + // Create sets of player IDs for easy comparison + final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {}; + final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet(); + + // Determine players to add and remove + final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds); + final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet); + + db.transaction(() async { + // Remove old players + if (playersToRemove.isNotEmpty) { + await (delete(playerMatchTable)..where( + (pg) => + pg.matchId.equals(matchId) & + pg.playerId.isIn(playersToRemove.toList()), + )) + .go(); + } + + // Add new players + if (playersToAdd.isNotEmpty) { + final inserts = playersToAdd + .map( + (id) => PlayerMatchTableCompanion.insert( + playerId: id, + matchId: matchId, + ), + ) + .toList(); + await Future.wait( + inserts.map( + (c) => into( + playerMatchTable, + ).insert(c, mode: InsertMode.insertOrIgnore), + ), + ); + } + }); + } +} diff --git a/lib/data/dao/player_match_dao.g.dart b/lib/data/dao/player_match_dao.g.dart new file mode 100644 index 0000000..bcc8ef7 --- /dev/null +++ b/lib/data/dao/player_match_dao.g.dart @@ -0,0 +1,11 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'player_match_dao.dart'; + +// ignore_for_file: type=lint +mixin _$PlayerMatchDaoMixin on DatabaseAccessor { + $PlayerTableTable get playerTable => attachedDatabase.playerTable; + $MatchTableTable get matchTable => attachedDatabase.matchTable; + $PlayerMatchTableTable get playerMatchTable => + attachedDatabase.playerMatchTable; +} diff --git a/lib/data/db/database.dart b/lib/data/db/database.dart new file mode 100644 index 0000000..e6c322f --- /dev/null +++ b/lib/data/db/database.dart @@ -0,0 +1,60 @@ +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:game_tracker/data/dao/group_dao.dart'; +import 'package:game_tracker/data/dao/group_match_dao.dart'; +import 'package:game_tracker/data/dao/match_dao.dart'; +import 'package:game_tracker/data/dao/player_dao.dart'; +import 'package:game_tracker/data/dao/player_group_dao.dart'; +import 'package:game_tracker/data/dao/player_match_dao.dart'; +import 'package:game_tracker/data/db/tables/group_match_table.dart'; +import 'package:game_tracker/data/db/tables/group_table.dart'; +import 'package:game_tracker/data/db/tables/match_table.dart'; +import 'package:game_tracker/data/db/tables/player_group_table.dart'; +import 'package:game_tracker/data/db/tables/player_match_table.dart'; +import 'package:game_tracker/data/db/tables/player_table.dart'; +import 'package:path_provider/path_provider.dart'; + +part 'database.g.dart'; + +@DriftDatabase( + tables: [ + PlayerTable, + GroupTable, + MatchTable, + PlayerGroupTable, + PlayerMatchTable, + GroupMatchTable, + ], + daos: [ + PlayerDao, + GroupDao, + MatchDao, + PlayerGroupDao, + PlayerMatchDao, + GroupMatchDao, + ], +) +class AppDatabase extends _$AppDatabase { + AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); + + @override + int get schemaVersion => 1; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); + } + + static QueryExecutor _openConnection() { + return driftDatabase( + name: 'gametracker_db', + native: const DriftNativeOptions( + databaseDirectory: getApplicationSupportDirectory, + ), + ); + } +} diff --git a/lib/data/db/database.g.dart b/lib/data/db/database.g.dart new file mode 100644 index 0000000..6bc493c --- /dev/null +++ b/lib/data/db/database.g.dart @@ -0,0 +1,3844 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database.dart'; + +// ignore_for_file: type=lint +class $PlayerTableTable extends PlayerTable + with TableInfo<$PlayerTableTable, PlayerTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlayerTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, name, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'player_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + PlayerTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlayerTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + $PlayerTableTable createAlias(String alias) { + return $PlayerTableTable(attachedDatabase, alias); + } +} + +class PlayerTableData extends DataClass implements Insertable { + final String id; + final String name; + final DateTime createdAt; + const PlayerTableData({ + required this.id, + required this.name, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['created_at'] = Variable(createdAt); + return map; + } + + PlayerTableCompanion toCompanion(bool nullToAbsent) { + return PlayerTableCompanion( + id: Value(id), + name: Value(name), + createdAt: Value(createdAt), + ); + } + + factory PlayerTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlayerTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'createdAt': serializer.toJson(createdAt), + }; + } + + PlayerTableData copyWith({String? id, String? name, DateTime? createdAt}) => + PlayerTableData( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + ); + PlayerTableData copyWithCompanion(PlayerTableCompanion data) { + return PlayerTableData( + 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, + ); + } + + @override + String toString() { + return (StringBuffer('PlayerTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlayerTableData && + other.id == this.id && + other.name == this.name && + other.createdAt == this.createdAt); +} + +class PlayerTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value createdAt; + final Value rowid; + const PlayerTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + PlayerTableCompanion.insert({ + required String id, + required String name, + required DateTime createdAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + createdAt = Value(createdAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + PlayerTableCompanion copyWith({ + Value? id, + Value? name, + Value? createdAt, + Value? rowid, + }) { + return PlayerTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlayerTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $GroupTableTable extends GroupTable + with TableInfo<$GroupTableTable, GroupTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, name, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + GroupTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + $GroupTableTable createAlias(String alias) { + return $GroupTableTable(attachedDatabase, alias); + } +} + +class GroupTableData extends DataClass implements Insertable { + final String id; + final String name; + final DateTime createdAt; + const GroupTableData({ + required this.id, + required this.name, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + map['created_at'] = Variable(createdAt); + return map; + } + + GroupTableCompanion toCompanion(bool nullToAbsent) { + return GroupTableCompanion( + id: Value(id), + name: Value(name), + createdAt: Value(createdAt), + ); + } + + factory GroupTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'createdAt': serializer.toJson(createdAt), + }; + } + + GroupTableData copyWith({String? id, String? name, DateTime? createdAt}) => + GroupTableData( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + ); + GroupTableData copyWithCompanion(GroupTableCompanion data) { + return GroupTableData( + 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, + ); + } + + @override + String toString() { + return (StringBuffer('GroupTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupTableData && + other.id == this.id && + other.name == this.name && + other.createdAt == this.createdAt); +} + +class GroupTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value createdAt; + final Value rowid; + const GroupTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupTableCompanion.insert({ + required String id, + required String name, + required DateTime createdAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + createdAt = Value(createdAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupTableCompanion copyWith({ + Value? id, + Value? name, + Value? createdAt, + Value? rowid, + }) { + return GroupTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $MatchTableTable extends MatchTable + with TableInfo<$MatchTableTable, MatchTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $MatchTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = GeneratedColumn( + 'id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _nameMeta = const VerificationMeta('name'); + @override + late final GeneratedColumn name = GeneratedColumn( + 'name', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + ); + static const VerificationMeta _winnerIdMeta = const VerificationMeta( + 'winnerId', + ); + @override + late final GeneratedColumn winnerId = GeneratedColumn( + 'winner_id', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ); + static const VerificationMeta _createdAtMeta = const VerificationMeta( + 'createdAt', + ); + @override + late final GeneratedColumn createdAt = GeneratedColumn( + 'created_at', + aliasedName, + false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + ); + @override + List get $columns => [id, name, winnerId, createdAt]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'match_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, + name.isAcceptableOrUnknown(data['name']!, _nameMeta), + ); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('winner_id')) { + context.handle( + _winnerIdMeta, + winnerId.isAcceptableOrUnknown(data['winner_id']!, _winnerIdMeta), + ); + } + if (data.containsKey('created_at')) { + context.handle( + _createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta), + ); + } else if (isInserting) { + context.missing(_createdAtMeta); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + MatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return MatchTableData( + id: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}id'], + )!, + name: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}name'], + )!, + winnerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}winner_id'], + ), + createdAt: attachedDatabase.typeMapping.read( + DriftSqlType.dateTime, + data['${effectivePrefix}created_at'], + )!, + ); + } + + @override + $MatchTableTable createAlias(String alias) { + return $MatchTableTable(attachedDatabase, alias); + } +} + +class MatchTableData extends DataClass implements Insertable { + final String id; + final String name; + final String? winnerId; + final DateTime createdAt; + const MatchTableData({ + required this.id, + required this.name, + this.winnerId, + required this.createdAt, + }); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = Variable(id); + map['name'] = Variable(name); + if (!nullToAbsent || winnerId != null) { + map['winner_id'] = Variable(winnerId); + } + map['created_at'] = Variable(createdAt); + return map; + } + + MatchTableCompanion toCompanion(bool nullToAbsent) { + return MatchTableCompanion( + id: Value(id), + name: Value(name), + winnerId: winnerId == null && nullToAbsent + ? const Value.absent() + : Value(winnerId), + createdAt: Value(createdAt), + ); + } + + factory MatchTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return MatchTableData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + winnerId: serializer.fromJson(json['winnerId']), + createdAt: serializer.fromJson(json['createdAt']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'winnerId': serializer.toJson(winnerId), + 'createdAt': serializer.toJson(createdAt), + }; + } + + MatchTableData copyWith({ + String? id, + String? name, + Value winnerId = const Value.absent(), + DateTime? createdAt, + }) => MatchTableData( + id: id ?? this.id, + name: name ?? this.name, + winnerId: winnerId.present ? winnerId.value : this.winnerId, + createdAt: createdAt ?? this.createdAt, + ); + MatchTableData copyWithCompanion(MatchTableCompanion data) { + return MatchTableData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + winnerId: data.winnerId.present ? data.winnerId.value : this.winnerId, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + ); + } + + @override + String toString() { + return (StringBuffer('MatchTableData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('winnerId: $winnerId, ') + ..write('createdAt: $createdAt') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, winnerId, createdAt); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is MatchTableData && + other.id == this.id && + other.name == this.name && + other.winnerId == this.winnerId && + other.createdAt == this.createdAt); +} + +class MatchTableCompanion extends UpdateCompanion { + final Value id; + final Value name; + final Value winnerId; + final Value createdAt; + final Value rowid; + const MatchTableCompanion({ + this.id = const Value.absent(), + this.name = const Value.absent(), + this.winnerId = const Value.absent(), + this.createdAt = const Value.absent(), + this.rowid = const Value.absent(), + }); + MatchTableCompanion.insert({ + required String id, + required String name, + this.winnerId = const Value.absent(), + required DateTime createdAt, + this.rowid = const Value.absent(), + }) : id = Value(id), + name = Value(name), + createdAt = Value(createdAt); + static Insertable custom({ + Expression? id, + Expression? name, + Expression? winnerId, + Expression? createdAt, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (winnerId != null) 'winner_id': winnerId, + if (createdAt != null) 'created_at': createdAt, + if (rowid != null) 'rowid': rowid, + }); + } + + MatchTableCompanion copyWith({ + Value? id, + Value? name, + Value? winnerId, + Value? createdAt, + Value? rowid, + }) { + return MatchTableCompanion( + id: id ?? this.id, + name: name ?? this.name, + winnerId: winnerId ?? this.winnerId, + createdAt: createdAt ?? this.createdAt, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = Variable(id.value); + } + if (name.present) { + map['name'] = Variable(name.value); + } + if (winnerId.present) { + map['winner_id'] = Variable(winnerId.value); + } + if (createdAt.present) { + map['created_at'] = Variable(createdAt.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('MatchTableCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('winnerId: $winnerId, ') + ..write('createdAt: $createdAt, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PlayerGroupTableTable extends PlayerGroupTable + with TableInfo<$PlayerGroupTableTable, PlayerGroupTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlayerGroupTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _playerIdMeta = const VerificationMeta( + 'playerId', + ); + @override + late final GeneratedColumn playerId = GeneratedColumn( + 'player_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES player_table (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _groupIdMeta = const VerificationMeta( + 'groupId', + ); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES group_table (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [playerId, groupId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'player_group_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('player_id')) { + context.handle( + _playerIdMeta, + playerId.isAcceptableOrUnknown(data['player_id']!, _playerIdMeta), + ); + } else if (isInserting) { + context.missing(_playerIdMeta); + } + if (data.containsKey('group_id')) { + context.handle( + _groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta), + ); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {playerId, groupId}; + @override + PlayerGroupTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlayerGroupTableData( + playerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}player_id'], + )!, + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + ); + } + + @override + $PlayerGroupTableTable createAlias(String alias) { + return $PlayerGroupTableTable(attachedDatabase, alias); + } +} + +class PlayerGroupTableData extends DataClass + implements Insertable { + final String playerId; + final String groupId; + const PlayerGroupTableData({required this.playerId, required this.groupId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['player_id'] = Variable(playerId); + map['group_id'] = Variable(groupId); + return map; + } + + PlayerGroupTableCompanion toCompanion(bool nullToAbsent) { + return PlayerGroupTableCompanion( + playerId: Value(playerId), + groupId: Value(groupId), + ); + } + + factory PlayerGroupTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlayerGroupTableData( + playerId: serializer.fromJson(json['playerId']), + groupId: serializer.fromJson(json['groupId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'playerId': serializer.toJson(playerId), + 'groupId': serializer.toJson(groupId), + }; + } + + PlayerGroupTableData copyWith({String? playerId, String? groupId}) => + PlayerGroupTableData( + playerId: playerId ?? this.playerId, + groupId: groupId ?? this.groupId, + ); + PlayerGroupTableData copyWithCompanion(PlayerGroupTableCompanion data) { + return PlayerGroupTableData( + playerId: data.playerId.present ? data.playerId.value : this.playerId, + groupId: data.groupId.present ? data.groupId.value : this.groupId, + ); + } + + @override + String toString() { + return (StringBuffer('PlayerGroupTableData(') + ..write('playerId: $playerId, ') + ..write('groupId: $groupId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(playerId, groupId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlayerGroupTableData && + other.playerId == this.playerId && + other.groupId == this.groupId); +} + +class PlayerGroupTableCompanion extends UpdateCompanion { + final Value playerId; + final Value groupId; + final Value rowid; + const PlayerGroupTableCompanion({ + this.playerId = const Value.absent(), + this.groupId = const Value.absent(), + this.rowid = const Value.absent(), + }); + PlayerGroupTableCompanion.insert({ + required String playerId, + required String groupId, + this.rowid = const Value.absent(), + }) : playerId = Value(playerId), + groupId = Value(groupId); + static Insertable custom({ + Expression? playerId, + Expression? groupId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (playerId != null) 'player_id': playerId, + if (groupId != null) 'group_id': groupId, + if (rowid != null) 'rowid': rowid, + }); + } + + PlayerGroupTableCompanion copyWith({ + Value? playerId, + Value? groupId, + Value? rowid, + }) { + return PlayerGroupTableCompanion( + playerId: playerId ?? this.playerId, + groupId: groupId ?? this.groupId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (playerId.present) { + map['player_id'] = Variable(playerId.value); + } + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlayerGroupTableCompanion(') + ..write('playerId: $playerId, ') + ..write('groupId: $groupId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $PlayerMatchTableTable extends PlayerMatchTable + with TableInfo<$PlayerMatchTableTable, PlayerMatchTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $PlayerMatchTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _playerIdMeta = const VerificationMeta( + 'playerId', + ); + @override + late final GeneratedColumn playerId = GeneratedColumn( + 'player_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES player_table (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _matchIdMeta = const VerificationMeta( + 'matchId', + ); + @override + late final GeneratedColumn matchId = GeneratedColumn( + 'match_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES match_table (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [playerId, matchId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'player_match_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('player_id')) { + context.handle( + _playerIdMeta, + playerId.isAcceptableOrUnknown(data['player_id']!, _playerIdMeta), + ); + } else if (isInserting) { + context.missing(_playerIdMeta); + } + if (data.containsKey('match_id')) { + context.handle( + _matchIdMeta, + matchId.isAcceptableOrUnknown(data['match_id']!, _matchIdMeta), + ); + } else if (isInserting) { + context.missing(_matchIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {playerId, matchId}; + @override + PlayerMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return PlayerMatchTableData( + playerId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}player_id'], + )!, + matchId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}match_id'], + )!, + ); + } + + @override + $PlayerMatchTableTable createAlias(String alias) { + return $PlayerMatchTableTable(attachedDatabase, alias); + } +} + +class PlayerMatchTableData extends DataClass + implements Insertable { + final String playerId; + final String matchId; + const PlayerMatchTableData({required this.playerId, required this.matchId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['player_id'] = Variable(playerId); + map['match_id'] = Variable(matchId); + return map; + } + + PlayerMatchTableCompanion toCompanion(bool nullToAbsent) { + return PlayerMatchTableCompanion( + playerId: Value(playerId), + matchId: Value(matchId), + ); + } + + factory PlayerMatchTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return PlayerMatchTableData( + playerId: serializer.fromJson(json['playerId']), + matchId: serializer.fromJson(json['matchId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'playerId': serializer.toJson(playerId), + 'matchId': serializer.toJson(matchId), + }; + } + + PlayerMatchTableData copyWith({String? playerId, String? matchId}) => + PlayerMatchTableData( + playerId: playerId ?? this.playerId, + matchId: matchId ?? this.matchId, + ); + PlayerMatchTableData copyWithCompanion(PlayerMatchTableCompanion data) { + return PlayerMatchTableData( + playerId: data.playerId.present ? data.playerId.value : this.playerId, + matchId: data.matchId.present ? data.matchId.value : this.matchId, + ); + } + + @override + String toString() { + return (StringBuffer('PlayerMatchTableData(') + ..write('playerId: $playerId, ') + ..write('matchId: $matchId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(playerId, matchId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is PlayerMatchTableData && + other.playerId == this.playerId && + other.matchId == this.matchId); +} + +class PlayerMatchTableCompanion extends UpdateCompanion { + final Value playerId; + final Value matchId; + final Value rowid; + const PlayerMatchTableCompanion({ + this.playerId = const Value.absent(), + this.matchId = const Value.absent(), + this.rowid = const Value.absent(), + }); + PlayerMatchTableCompanion.insert({ + required String playerId, + required String matchId, + this.rowid = const Value.absent(), + }) : playerId = Value(playerId), + matchId = Value(matchId); + static Insertable custom({ + Expression? playerId, + Expression? matchId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (playerId != null) 'player_id': playerId, + if (matchId != null) 'match_id': matchId, + if (rowid != null) 'rowid': rowid, + }); + } + + PlayerMatchTableCompanion copyWith({ + Value? playerId, + Value? matchId, + Value? rowid, + }) { + return PlayerMatchTableCompanion( + playerId: playerId ?? this.playerId, + matchId: matchId ?? this.matchId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (playerId.present) { + map['player_id'] = Variable(playerId.value); + } + if (matchId.present) { + map['match_id'] = Variable(matchId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('PlayerMatchTableCompanion(') + ..write('playerId: $playerId, ') + ..write('matchId: $matchId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +class $GroupMatchTableTable extends GroupMatchTable + with TableInfo<$GroupMatchTableTable, GroupMatchTableData> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $GroupMatchTableTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _groupIdMeta = const VerificationMeta( + 'groupId', + ); + @override + late final GeneratedColumn groupId = GeneratedColumn( + 'group_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES group_table (id) ON DELETE CASCADE', + ), + ); + static const VerificationMeta _matchIdMeta = const VerificationMeta( + 'matchId', + ); + @override + late final GeneratedColumn matchId = GeneratedColumn( + 'match_id', + aliasedName, + false, + type: DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'REFERENCES match_table (id) ON DELETE CASCADE', + ), + ); + @override + List get $columns => [groupId, matchId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'group_match_table'; + @override + VerificationContext validateIntegrity( + Insertable instance, { + bool isInserting = false, + }) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('group_id')) { + context.handle( + _groupIdMeta, + groupId.isAcceptableOrUnknown(data['group_id']!, _groupIdMeta), + ); + } else if (isInserting) { + context.missing(_groupIdMeta); + } + if (data.containsKey('match_id')) { + context.handle( + _matchIdMeta, + matchId.isAcceptableOrUnknown(data['match_id']!, _matchIdMeta), + ); + } else if (isInserting) { + context.missing(_matchIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {groupId, matchId}; + @override + GroupMatchTableData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return GroupMatchTableData( + groupId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}group_id'], + )!, + matchId: attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}match_id'], + )!, + ); + } + + @override + $GroupMatchTableTable createAlias(String alias) { + return $GroupMatchTableTable(attachedDatabase, alias); + } +} + +class GroupMatchTableData extends DataClass + implements Insertable { + final String groupId; + final String matchId; + const GroupMatchTableData({required this.groupId, required this.matchId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['group_id'] = Variable(groupId); + map['match_id'] = Variable(matchId); + return map; + } + + GroupMatchTableCompanion toCompanion(bool nullToAbsent) { + return GroupMatchTableCompanion( + groupId: Value(groupId), + matchId: Value(matchId), + ); + } + + factory GroupMatchTableData.fromJson( + Map json, { + ValueSerializer? serializer, + }) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return GroupMatchTableData( + groupId: serializer.fromJson(json['groupId']), + matchId: serializer.fromJson(json['matchId']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'groupId': serializer.toJson(groupId), + 'matchId': serializer.toJson(matchId), + }; + } + + GroupMatchTableData copyWith({String? groupId, String? matchId}) => + GroupMatchTableData( + groupId: groupId ?? this.groupId, + matchId: matchId ?? this.matchId, + ); + GroupMatchTableData copyWithCompanion(GroupMatchTableCompanion data) { + return GroupMatchTableData( + groupId: data.groupId.present ? data.groupId.value : this.groupId, + matchId: data.matchId.present ? data.matchId.value : this.matchId, + ); + } + + @override + String toString() { + return (StringBuffer('GroupMatchTableData(') + ..write('groupId: $groupId, ') + ..write('matchId: $matchId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(groupId, matchId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is GroupMatchTableData && + other.groupId == this.groupId && + other.matchId == this.matchId); +} + +class GroupMatchTableCompanion extends UpdateCompanion { + final Value groupId; + final Value matchId; + final Value rowid; + const GroupMatchTableCompanion({ + this.groupId = const Value.absent(), + this.matchId = const Value.absent(), + this.rowid = const Value.absent(), + }); + GroupMatchTableCompanion.insert({ + required String groupId, + required String matchId, + this.rowid = const Value.absent(), + }) : groupId = Value(groupId), + matchId = Value(matchId); + static Insertable custom({ + Expression? groupId, + Expression? matchId, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (groupId != null) 'group_id': groupId, + if (matchId != null) 'match_id': matchId, + if (rowid != null) 'rowid': rowid, + }); + } + + GroupMatchTableCompanion copyWith({ + Value? groupId, + Value? matchId, + Value? rowid, + }) { + return GroupMatchTableCompanion( + groupId: groupId ?? this.groupId, + matchId: matchId ?? this.matchId, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (groupId.present) { + map['group_id'] = Variable(groupId.value); + } + if (matchId.present) { + map['match_id'] = Variable(matchId.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('GroupMatchTableCompanion(') + ..write('groupId: $groupId, ') + ..write('matchId: $matchId, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $PlayerTableTable playerTable = $PlayerTableTable(this); + late final $GroupTableTable groupTable = $GroupTableTable(this); + late final $MatchTableTable matchTable = $MatchTableTable(this); + late final $PlayerGroupTableTable playerGroupTable = $PlayerGroupTableTable( + this, + ); + late final $PlayerMatchTableTable playerMatchTable = $PlayerMatchTableTable( + this, + ); + late final $GroupMatchTableTable groupMatchTable = $GroupMatchTableTable( + this, + ); + late final PlayerDao playerDao = PlayerDao(this as AppDatabase); + late final GroupDao groupDao = GroupDao(this as AppDatabase); + late final MatchDao matchDao = MatchDao(this as AppDatabase); + late final PlayerGroupDao playerGroupDao = PlayerGroupDao( + this as AppDatabase, + ); + late final PlayerMatchDao playerMatchDao = PlayerMatchDao( + this as AppDatabase, + ); + late final GroupMatchDao groupMatchDao = GroupMatchDao(this as AppDatabase); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => [ + playerTable, + groupTable, + matchTable, + playerGroupTable, + playerMatchTable, + groupMatchTable, + ]; + @override + StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules([ + WritePropagation( + on: TableUpdateQuery.onTableName( + 'player_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('player_group_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'group_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('player_group_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'player_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('player_match_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'match_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('player_match_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'group_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('group_match_table', kind: UpdateKind.delete)], + ), + WritePropagation( + on: TableUpdateQuery.onTableName( + 'match_table', + limitUpdateKind: UpdateKind.delete, + ), + result: [TableUpdate('group_match_table', kind: UpdateKind.delete)], + ), + ]); +} + +typedef $$PlayerTableTableCreateCompanionBuilder = + PlayerTableCompanion Function({ + required String id, + required String name, + required DateTime createdAt, + Value rowid, + }); +typedef $$PlayerTableTableUpdateCompanionBuilder = + PlayerTableCompanion Function({ + Value id, + Value name, + Value createdAt, + Value rowid, + }); + +final class $$PlayerTableTableReferences + extends BaseReferences<_$AppDatabase, $PlayerTableTable, PlayerTableData> { + $$PlayerTableTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$PlayerGroupTableTable, List> + _playerGroupTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.playerGroupTable, + aliasName: $_aliasNameGenerator( + db.playerTable.id, + db.playerGroupTable.playerId, + ), + ); + + $$PlayerGroupTableTableProcessedTableManager get playerGroupTableRefs { + final manager = $$PlayerGroupTableTableTableManager( + $_db, + $_db.playerGroupTable, + ).filter((f) => f.playerId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _playerGroupTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$PlayerMatchTableTable, List> + _playerMatchTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.playerMatchTable, + aliasName: $_aliasNameGenerator( + db.playerTable.id, + db.playerMatchTable.playerId, + ), + ); + + $$PlayerMatchTableTableProcessedTableManager get playerMatchTableRefs { + final manager = $$PlayerMatchTableTableTableManager( + $_db, + $_db.playerMatchTable, + ).filter((f) => f.playerId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _playerMatchTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$PlayerTableTableFilterComposer + extends Composer<_$AppDatabase, $PlayerTableTable> { + $$PlayerTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + Expression playerGroupTableRefs( + Expression Function($$PlayerGroupTableTableFilterComposer f) f, + ) { + final $$PlayerGroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerGroupTable, + getReferencedColumn: (t) => t.playerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerGroupTableTableFilterComposer( + $db: $db, + $table: $db.playerGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression playerMatchTableRefs( + Expression Function($$PlayerMatchTableTableFilterComposer f) f, + ) { + final $$PlayerMatchTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerMatchTable, + getReferencedColumn: (t) => t.playerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerMatchTableTableFilterComposer( + $db: $db, + $table: $db.playerMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$PlayerTableTableOrderingComposer + extends Composer<_$AppDatabase, $PlayerTableTable> { + $$PlayerTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$PlayerTableTableAnnotationComposer + extends Composer<_$AppDatabase, $PlayerTableTable> { + $$PlayerTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression playerGroupTableRefs( + Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, + ) { + final $$PlayerGroupTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerGroupTable, + getReferencedColumn: (t) => t.playerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerGroupTableTableAnnotationComposer( + $db: $db, + $table: $db.playerGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression playerMatchTableRefs( + Expression Function($$PlayerMatchTableTableAnnotationComposer a) f, + ) { + final $$PlayerMatchTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerMatchTable, + getReferencedColumn: (t) => t.playerId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerMatchTableTableAnnotationComposer( + $db: $db, + $table: $db.playerMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$PlayerTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $PlayerTableTable, + PlayerTableData, + $$PlayerTableTableFilterComposer, + $$PlayerTableTableOrderingComposer, + $$PlayerTableTableAnnotationComposer, + $$PlayerTableTableCreateCompanionBuilder, + $$PlayerTableTableUpdateCompanionBuilder, + (PlayerTableData, $$PlayerTableTableReferences), + PlayerTableData, + PrefetchHooks Function({ + bool playerGroupTableRefs, + bool playerMatchTableRefs, + }) + > { + $$PlayerTableTableTableManager(_$AppDatabase db, $PlayerTableTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PlayerTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PlayerTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PlayerTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => PlayerTableCompanion( + id: id, + name: name, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required DateTime createdAt, + Value rowid = const Value.absent(), + }) => PlayerTableCompanion.insert( + id: id, + name: name, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PlayerTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({playerGroupTableRefs = false, playerMatchTableRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (playerGroupTableRefs) db.playerGroupTable, + if (playerMatchTableRefs) db.playerMatchTable, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (playerGroupTableRefs) + await $_getPrefetchedData< + PlayerTableData, + $PlayerTableTable, + PlayerGroupTableData + >( + currentTable: table, + referencedTable: $$PlayerTableTableReferences + ._playerGroupTableRefsTable(db), + managerFromTypedResult: (p0) => + $$PlayerTableTableReferences( + db, + table, + p0, + ).playerGroupTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.playerId == item.id, + ), + typedResults: items, + ), + if (playerMatchTableRefs) + await $_getPrefetchedData< + PlayerTableData, + $PlayerTableTable, + PlayerMatchTableData + >( + currentTable: table, + referencedTable: $$PlayerTableTableReferences + ._playerMatchTableRefsTable(db), + managerFromTypedResult: (p0) => + $$PlayerTableTableReferences( + db, + table, + p0, + ).playerMatchTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.playerId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$PlayerTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $PlayerTableTable, + PlayerTableData, + $$PlayerTableTableFilterComposer, + $$PlayerTableTableOrderingComposer, + $$PlayerTableTableAnnotationComposer, + $$PlayerTableTableCreateCompanionBuilder, + $$PlayerTableTableUpdateCompanionBuilder, + (PlayerTableData, $$PlayerTableTableReferences), + PlayerTableData, + PrefetchHooks Function({ + bool playerGroupTableRefs, + bool playerMatchTableRefs, + }) + >; +typedef $$GroupTableTableCreateCompanionBuilder = + GroupTableCompanion Function({ + required String id, + required String name, + required DateTime createdAt, + Value rowid, + }); +typedef $$GroupTableTableUpdateCompanionBuilder = + GroupTableCompanion Function({ + Value id, + Value name, + Value createdAt, + Value rowid, + }); + +final class $$GroupTableTableReferences + extends BaseReferences<_$AppDatabase, $GroupTableTable, GroupTableData> { + $$GroupTableTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$PlayerGroupTableTable, List> + _playerGroupTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.playerGroupTable, + aliasName: $_aliasNameGenerator( + db.groupTable.id, + db.playerGroupTable.groupId, + ), + ); + + $$PlayerGroupTableTableProcessedTableManager get playerGroupTableRefs { + final manager = $$PlayerGroupTableTableTableManager( + $_db, + $_db.playerGroupTable, + ).filter((f) => f.groupId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _playerGroupTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$GroupMatchTableTable, List> + _groupMatchTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.groupMatchTable, + aliasName: $_aliasNameGenerator( + db.groupTable.id, + db.groupMatchTable.groupId, + ), + ); + + $$GroupMatchTableTableProcessedTableManager get groupMatchTableRefs { + final manager = $$GroupMatchTableTableTableManager( + $_db, + $_db.groupMatchTable, + ).filter((f) => f.groupId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _groupMatchTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$GroupTableTableFilterComposer + extends Composer<_$AppDatabase, $GroupTableTable> { + $$GroupTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + Expression playerGroupTableRefs( + Expression Function($$PlayerGroupTableTableFilterComposer f) f, + ) { + final $$PlayerGroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerGroupTable, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerGroupTableTableFilterComposer( + $db: $db, + $table: $db.playerGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression groupMatchTableRefs( + Expression Function($$GroupMatchTableTableFilterComposer f) f, + ) { + final $$GroupMatchTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.groupMatchTable, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupMatchTableTableFilterComposer( + $db: $db, + $table: $db.groupMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$GroupTableTableOrderingComposer + extends Composer<_$AppDatabase, $GroupTableTable> { + $$GroupTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$GroupTableTableAnnotationComposer + extends Composer<_$AppDatabase, $GroupTableTable> { + $$GroupTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression playerGroupTableRefs( + Expression Function($$PlayerGroupTableTableAnnotationComposer a) f, + ) { + final $$PlayerGroupTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerGroupTable, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerGroupTableTableAnnotationComposer( + $db: $db, + $table: $db.playerGroupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression groupMatchTableRefs( + Expression Function($$GroupMatchTableTableAnnotationComposer a) f, + ) { + final $$GroupMatchTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.groupMatchTable, + getReferencedColumn: (t) => t.groupId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupMatchTableTableAnnotationComposer( + $db: $db, + $table: $db.groupMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$GroupTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $GroupTableTable, + GroupTableData, + $$GroupTableTableFilterComposer, + $$GroupTableTableOrderingComposer, + $$GroupTableTableAnnotationComposer, + $$GroupTableTableCreateCompanionBuilder, + $$GroupTableTableUpdateCompanionBuilder, + (GroupTableData, $$GroupTableTableReferences), + GroupTableData, + PrefetchHooks Function({ + bool playerGroupTableRefs, + bool groupMatchTableRefs, + }) + > { + $$GroupTableTableTableManager(_$AppDatabase db, $GroupTableTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => GroupTableCompanion( + id: id, + name: name, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + required DateTime createdAt, + Value rowid = const Value.absent(), + }) => GroupTableCompanion.insert( + id: id, + name: name, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$GroupTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({playerGroupTableRefs = false, groupMatchTableRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (playerGroupTableRefs) db.playerGroupTable, + if (groupMatchTableRefs) db.groupMatchTable, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (playerGroupTableRefs) + await $_getPrefetchedData< + GroupTableData, + $GroupTableTable, + PlayerGroupTableData + >( + currentTable: table, + referencedTable: $$GroupTableTableReferences + ._playerGroupTableRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupTableTableReferences( + db, + table, + p0, + ).playerGroupTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.groupId == item.id, + ), + typedResults: items, + ), + if (groupMatchTableRefs) + await $_getPrefetchedData< + GroupTableData, + $GroupTableTable, + GroupMatchTableData + >( + currentTable: table, + referencedTable: $$GroupTableTableReferences + ._groupMatchTableRefsTable(db), + managerFromTypedResult: (p0) => + $$GroupTableTableReferences( + db, + table, + p0, + ).groupMatchTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.groupId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$GroupTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $GroupTableTable, + GroupTableData, + $$GroupTableTableFilterComposer, + $$GroupTableTableOrderingComposer, + $$GroupTableTableAnnotationComposer, + $$GroupTableTableCreateCompanionBuilder, + $$GroupTableTableUpdateCompanionBuilder, + (GroupTableData, $$GroupTableTableReferences), + GroupTableData, + PrefetchHooks Function({ + bool playerGroupTableRefs, + bool groupMatchTableRefs, + }) + >; +typedef $$MatchTableTableCreateCompanionBuilder = + MatchTableCompanion Function({ + required String id, + required String name, + Value winnerId, + required DateTime createdAt, + Value rowid, + }); +typedef $$MatchTableTableUpdateCompanionBuilder = + MatchTableCompanion Function({ + Value id, + Value name, + Value winnerId, + Value createdAt, + Value rowid, + }); + +final class $$MatchTableTableReferences + extends BaseReferences<_$AppDatabase, $MatchTableTable, MatchTableData> { + $$MatchTableTableReferences(super.$_db, super.$_table, super.$_typedResult); + + static MultiTypedResultKey<$PlayerMatchTableTable, List> + _playerMatchTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.playerMatchTable, + aliasName: $_aliasNameGenerator( + db.matchTable.id, + db.playerMatchTable.matchId, + ), + ); + + $$PlayerMatchTableTableProcessedTableManager get playerMatchTableRefs { + final manager = $$PlayerMatchTableTableTableManager( + $_db, + $_db.playerMatchTable, + ).filter((f) => f.matchId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _playerMatchTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } + + static MultiTypedResultKey<$GroupMatchTableTable, List> + _groupMatchTableRefsTable(_$AppDatabase db) => MultiTypedResultKey.fromTable( + db.groupMatchTable, + aliasName: $_aliasNameGenerator( + db.matchTable.id, + db.groupMatchTable.matchId, + ), + ); + + $$GroupMatchTableTableProcessedTableManager get groupMatchTableRefs { + final manager = $$GroupMatchTableTableTableManager( + $_db, + $_db.groupMatchTable, + ).filter((f) => f.matchId.id.sqlEquals($_itemColumn('id')!)); + + final cache = $_typedResult.readTableOrNull( + _groupMatchTableRefsTable($_db), + ); + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: cache), + ); + } +} + +class $$MatchTableTableFilterComposer + extends Composer<_$AppDatabase, $MatchTableTable> { + $$MatchTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnFilters(column), + ); + + ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnFilters(column), + ); + + Expression playerMatchTableRefs( + Expression Function($$PlayerMatchTableTableFilterComposer f) f, + ) { + final $$PlayerMatchTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerMatchTable, + getReferencedColumn: (t) => t.matchId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerMatchTableTableFilterComposer( + $db: $db, + $table: $db.playerMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression groupMatchTableRefs( + Expression Function($$GroupMatchTableTableFilterComposer f) f, + ) { + final $$GroupMatchTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.groupMatchTable, + getReferencedColumn: (t) => t.matchId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupMatchTableTableFilterComposer( + $db: $db, + $table: $db.groupMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$MatchTableTableOrderingComposer + extends Composer<_$AppDatabase, $MatchTableTable> { + $$MatchTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get id => $composableBuilder( + column: $table.id, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get name => $composableBuilder( + column: $table.name, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get winnerId => $composableBuilder( + column: $table.winnerId, + builder: (column) => ColumnOrderings(column), + ); + + ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => ColumnOrderings(column), + ); +} + +class $$MatchTableTableAnnotationComposer + extends Composer<_$AppDatabase, $MatchTableTable> { + $$MatchTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + GeneratedColumn get winnerId => + $composableBuilder(column: $table.winnerId, builder: (column) => column); + + GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + Expression playerMatchTableRefs( + Expression Function($$PlayerMatchTableTableAnnotationComposer a) f, + ) { + final $$PlayerMatchTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.playerMatchTable, + getReferencedColumn: (t) => t.matchId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerMatchTableTableAnnotationComposer( + $db: $db, + $table: $db.playerMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } + + Expression groupMatchTableRefs( + Expression Function($$GroupMatchTableTableAnnotationComposer a) f, + ) { + final $$GroupMatchTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.id, + referencedTable: $db.groupMatchTable, + getReferencedColumn: (t) => t.matchId, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupMatchTableTableAnnotationComposer( + $db: $db, + $table: $db.groupMatchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return f(composer); + } +} + +class $$MatchTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $MatchTableTable, + MatchTableData, + $$MatchTableTableFilterComposer, + $$MatchTableTableOrderingComposer, + $$MatchTableTableAnnotationComposer, + $$MatchTableTableCreateCompanionBuilder, + $$MatchTableTableUpdateCompanionBuilder, + (MatchTableData, $$MatchTableTableReferences), + MatchTableData, + PrefetchHooks Function({ + bool playerMatchTableRefs, + bool groupMatchTableRefs, + }) + > { + $$MatchTableTableTableManager(_$AppDatabase db, $MatchTableTable table) + : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$MatchTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$MatchTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$MatchTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value id = const Value.absent(), + Value name = const Value.absent(), + Value winnerId = const Value.absent(), + Value createdAt = const Value.absent(), + Value rowid = const Value.absent(), + }) => MatchTableCompanion( + id: id, + name: name, + winnerId: winnerId, + createdAt: createdAt, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String id, + required String name, + Value winnerId = const Value.absent(), + required DateTime createdAt, + Value rowid = const Value.absent(), + }) => MatchTableCompanion.insert( + id: id, + name: name, + winnerId: winnerId, + createdAt: createdAt, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$MatchTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: + ({playerMatchTableRefs = false, groupMatchTableRefs = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [ + if (playerMatchTableRefs) db.playerMatchTable, + if (groupMatchTableRefs) db.groupMatchTable, + ], + addJoins: null, + getPrefetchedDataCallback: (items) async { + return [ + if (playerMatchTableRefs) + await $_getPrefetchedData< + MatchTableData, + $MatchTableTable, + PlayerMatchTableData + >( + currentTable: table, + referencedTable: $$MatchTableTableReferences + ._playerMatchTableRefsTable(db), + managerFromTypedResult: (p0) => + $$MatchTableTableReferences( + db, + table, + p0, + ).playerMatchTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.matchId == item.id, + ), + typedResults: items, + ), + if (groupMatchTableRefs) + await $_getPrefetchedData< + MatchTableData, + $MatchTableTable, + GroupMatchTableData + >( + currentTable: table, + referencedTable: $$MatchTableTableReferences + ._groupMatchTableRefsTable(db), + managerFromTypedResult: (p0) => + $$MatchTableTableReferences( + db, + table, + p0, + ).groupMatchTableRefs, + referencedItemsForCurrentItem: + (item, referencedItems) => referencedItems.where( + (e) => e.matchId == item.id, + ), + typedResults: items, + ), + ]; + }, + ); + }, + ), + ); +} + +typedef $$MatchTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $MatchTableTable, + MatchTableData, + $$MatchTableTableFilterComposer, + $$MatchTableTableOrderingComposer, + $$MatchTableTableAnnotationComposer, + $$MatchTableTableCreateCompanionBuilder, + $$MatchTableTableUpdateCompanionBuilder, + (MatchTableData, $$MatchTableTableReferences), + MatchTableData, + PrefetchHooks Function({ + bool playerMatchTableRefs, + bool groupMatchTableRefs, + }) + >; +typedef $$PlayerGroupTableTableCreateCompanionBuilder = + PlayerGroupTableCompanion Function({ + required String playerId, + required String groupId, + Value rowid, + }); +typedef $$PlayerGroupTableTableUpdateCompanionBuilder = + PlayerGroupTableCompanion Function({ + Value playerId, + Value groupId, + Value rowid, + }); + +final class $$PlayerGroupTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $PlayerGroupTableTable, + PlayerGroupTableData + > { + $$PlayerGroupTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $PlayerTableTable _playerIdTable(_$AppDatabase db) => + db.playerTable.createAlias( + $_aliasNameGenerator(db.playerGroupTable.playerId, db.playerTable.id), + ); + + $$PlayerTableTableProcessedTableManager get playerId { + final $_column = $_itemColumn('player_id')!; + + final manager = $$PlayerTableTableTableManager( + $_db, + $_db.playerTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_playerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $GroupTableTable _groupIdTable(_$AppDatabase db) => + db.groupTable.createAlias( + $_aliasNameGenerator(db.playerGroupTable.groupId, db.groupTable.id), + ); + + $$GroupTableTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupTableTableTableManager( + $_db, + $_db.groupTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$PlayerGroupTableTableFilterComposer + extends Composer<_$AppDatabase, $PlayerGroupTableTable> { + $$PlayerGroupTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$PlayerTableTableFilterComposer get playerId { + final $$PlayerTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playerId, + referencedTable: $db.playerTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerTableTableFilterComposer( + $db: $db, + $table: $db.playerTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupTableTableFilterComposer get groupId { + final $$GroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableFilterComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PlayerGroupTableTableOrderingComposer + extends Composer<_$AppDatabase, $PlayerGroupTableTable> { + $$PlayerGroupTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$PlayerTableTableOrderingComposer get playerId { + final $$PlayerTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playerId, + referencedTable: $db.playerTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerTableTableOrderingComposer( + $db: $db, + $table: $db.playerTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupTableTableOrderingComposer get groupId { + final $$GroupTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableOrderingComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PlayerGroupTableTableAnnotationComposer + extends Composer<_$AppDatabase, $PlayerGroupTableTable> { + $$PlayerGroupTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$PlayerTableTableAnnotationComposer get playerId { + final $$PlayerTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playerId, + referencedTable: $db.playerTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerTableTableAnnotationComposer( + $db: $db, + $table: $db.playerTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$GroupTableTableAnnotationComposer get groupId { + final $$GroupTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableAnnotationComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PlayerGroupTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $PlayerGroupTableTable, + PlayerGroupTableData, + $$PlayerGroupTableTableFilterComposer, + $$PlayerGroupTableTableOrderingComposer, + $$PlayerGroupTableTableAnnotationComposer, + $$PlayerGroupTableTableCreateCompanionBuilder, + $$PlayerGroupTableTableUpdateCompanionBuilder, + (PlayerGroupTableData, $$PlayerGroupTableTableReferences), + PlayerGroupTableData, + PrefetchHooks Function({bool playerId, bool groupId}) + > { + $$PlayerGroupTableTableTableManager( + _$AppDatabase db, + $PlayerGroupTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PlayerGroupTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PlayerGroupTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PlayerGroupTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value playerId = const Value.absent(), + Value groupId = const Value.absent(), + Value rowid = const Value.absent(), + }) => PlayerGroupTableCompanion( + playerId: playerId, + groupId: groupId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String playerId, + required String groupId, + Value rowid = const Value.absent(), + }) => PlayerGroupTableCompanion.insert( + playerId: playerId, + groupId: groupId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PlayerGroupTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({playerId = false, groupId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (playerId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.playerId, + referencedTable: + $$PlayerGroupTableTableReferences + ._playerIdTable(db), + referencedColumn: + $$PlayerGroupTableTableReferences + ._playerIdTable(db) + .id, + ) + as T; + } + if (groupId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$PlayerGroupTableTableReferences + ._groupIdTable(db), + referencedColumn: + $$PlayerGroupTableTableReferences + ._groupIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$PlayerGroupTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $PlayerGroupTableTable, + PlayerGroupTableData, + $$PlayerGroupTableTableFilterComposer, + $$PlayerGroupTableTableOrderingComposer, + $$PlayerGroupTableTableAnnotationComposer, + $$PlayerGroupTableTableCreateCompanionBuilder, + $$PlayerGroupTableTableUpdateCompanionBuilder, + (PlayerGroupTableData, $$PlayerGroupTableTableReferences), + PlayerGroupTableData, + PrefetchHooks Function({bool playerId, bool groupId}) + >; +typedef $$PlayerMatchTableTableCreateCompanionBuilder = + PlayerMatchTableCompanion Function({ + required String playerId, + required String matchId, + Value rowid, + }); +typedef $$PlayerMatchTableTableUpdateCompanionBuilder = + PlayerMatchTableCompanion Function({ + Value playerId, + Value matchId, + Value rowid, + }); + +final class $$PlayerMatchTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $PlayerMatchTableTable, + PlayerMatchTableData + > { + $$PlayerMatchTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $PlayerTableTable _playerIdTable(_$AppDatabase db) => + db.playerTable.createAlias( + $_aliasNameGenerator(db.playerMatchTable.playerId, db.playerTable.id), + ); + + $$PlayerTableTableProcessedTableManager get playerId { + final $_column = $_itemColumn('player_id')!; + + final manager = $$PlayerTableTableTableManager( + $_db, + $_db.playerTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_playerIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $MatchTableTable _matchIdTable(_$AppDatabase db) => + db.matchTable.createAlias( + $_aliasNameGenerator(db.playerMatchTable.matchId, db.matchTable.id), + ); + + $$MatchTableTableProcessedTableManager get matchId { + final $_column = $_itemColumn('match_id')!; + + final manager = $$MatchTableTableTableManager( + $_db, + $_db.matchTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_matchIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$PlayerMatchTableTableFilterComposer + extends Composer<_$AppDatabase, $PlayerMatchTableTable> { + $$PlayerMatchTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$PlayerTableTableFilterComposer get playerId { + final $$PlayerTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playerId, + referencedTable: $db.playerTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerTableTableFilterComposer( + $db: $db, + $table: $db.playerTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MatchTableTableFilterComposer get matchId { + final $$MatchTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.matchId, + referencedTable: $db.matchTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MatchTableTableFilterComposer( + $db: $db, + $table: $db.matchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PlayerMatchTableTableOrderingComposer + extends Composer<_$AppDatabase, $PlayerMatchTableTable> { + $$PlayerMatchTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$PlayerTableTableOrderingComposer get playerId { + final $$PlayerTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playerId, + referencedTable: $db.playerTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerTableTableOrderingComposer( + $db: $db, + $table: $db.playerTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MatchTableTableOrderingComposer get matchId { + final $$MatchTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.matchId, + referencedTable: $db.matchTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MatchTableTableOrderingComposer( + $db: $db, + $table: $db.matchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PlayerMatchTableTableAnnotationComposer + extends Composer<_$AppDatabase, $PlayerMatchTableTable> { + $$PlayerMatchTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$PlayerTableTableAnnotationComposer get playerId { + final $$PlayerTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.playerId, + referencedTable: $db.playerTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$PlayerTableTableAnnotationComposer( + $db: $db, + $table: $db.playerTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MatchTableTableAnnotationComposer get matchId { + final $$MatchTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.matchId, + referencedTable: $db.matchTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MatchTableTableAnnotationComposer( + $db: $db, + $table: $db.matchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$PlayerMatchTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $PlayerMatchTableTable, + PlayerMatchTableData, + $$PlayerMatchTableTableFilterComposer, + $$PlayerMatchTableTableOrderingComposer, + $$PlayerMatchTableTableAnnotationComposer, + $$PlayerMatchTableTableCreateCompanionBuilder, + $$PlayerMatchTableTableUpdateCompanionBuilder, + (PlayerMatchTableData, $$PlayerMatchTableTableReferences), + PlayerMatchTableData, + PrefetchHooks Function({bool playerId, bool matchId}) + > { + $$PlayerMatchTableTableTableManager( + _$AppDatabase db, + $PlayerMatchTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$PlayerMatchTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$PlayerMatchTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$PlayerMatchTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value playerId = const Value.absent(), + Value matchId = const Value.absent(), + Value rowid = const Value.absent(), + }) => PlayerMatchTableCompanion( + playerId: playerId, + matchId: matchId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String playerId, + required String matchId, + Value rowid = const Value.absent(), + }) => PlayerMatchTableCompanion.insert( + playerId: playerId, + matchId: matchId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$PlayerMatchTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({playerId = false, matchId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (playerId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.playerId, + referencedTable: + $$PlayerMatchTableTableReferences + ._playerIdTable(db), + referencedColumn: + $$PlayerMatchTableTableReferences + ._playerIdTable(db) + .id, + ) + as T; + } + if (matchId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.matchId, + referencedTable: + $$PlayerMatchTableTableReferences + ._matchIdTable(db), + referencedColumn: + $$PlayerMatchTableTableReferences + ._matchIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$PlayerMatchTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $PlayerMatchTableTable, + PlayerMatchTableData, + $$PlayerMatchTableTableFilterComposer, + $$PlayerMatchTableTableOrderingComposer, + $$PlayerMatchTableTableAnnotationComposer, + $$PlayerMatchTableTableCreateCompanionBuilder, + $$PlayerMatchTableTableUpdateCompanionBuilder, + (PlayerMatchTableData, $$PlayerMatchTableTableReferences), + PlayerMatchTableData, + PrefetchHooks Function({bool playerId, bool matchId}) + >; +typedef $$GroupMatchTableTableCreateCompanionBuilder = + GroupMatchTableCompanion Function({ + required String groupId, + required String matchId, + Value rowid, + }); +typedef $$GroupMatchTableTableUpdateCompanionBuilder = + GroupMatchTableCompanion Function({ + Value groupId, + Value matchId, + Value rowid, + }); + +final class $$GroupMatchTableTableReferences + extends + BaseReferences< + _$AppDatabase, + $GroupMatchTableTable, + GroupMatchTableData + > { + $$GroupMatchTableTableReferences( + super.$_db, + super.$_table, + super.$_typedResult, + ); + + static $GroupTableTable _groupIdTable(_$AppDatabase db) => + db.groupTable.createAlias( + $_aliasNameGenerator(db.groupMatchTable.groupId, db.groupTable.id), + ); + + $$GroupTableTableProcessedTableManager get groupId { + final $_column = $_itemColumn('group_id')!; + + final manager = $$GroupTableTableTableManager( + $_db, + $_db.groupTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_groupIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } + + static $MatchTableTable _matchIdTable(_$AppDatabase db) => + db.matchTable.createAlias( + $_aliasNameGenerator(db.groupMatchTable.matchId, db.matchTable.id), + ); + + $$MatchTableTableProcessedTableManager get matchId { + final $_column = $_itemColumn('match_id')!; + + final manager = $$MatchTableTableTableManager( + $_db, + $_db.matchTable, + ).filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_matchIdTable($_db)); + if (item == null) return manager; + return ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item]), + ); + } +} + +class $$GroupMatchTableTableFilterComposer + extends Composer<_$AppDatabase, $GroupMatchTableTable> { + $$GroupMatchTableTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$GroupTableTableFilterComposer get groupId { + final $$GroupTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableFilterComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MatchTableTableFilterComposer get matchId { + final $$MatchTableTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.matchId, + referencedTable: $db.matchTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MatchTableTableFilterComposer( + $db: $db, + $table: $db.matchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$GroupMatchTableTableOrderingComposer + extends Composer<_$AppDatabase, $GroupMatchTableTable> { + $$GroupMatchTableTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$GroupTableTableOrderingComposer get groupId { + final $$GroupTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableOrderingComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MatchTableTableOrderingComposer get matchId { + final $$MatchTableTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.matchId, + referencedTable: $db.matchTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MatchTableTableOrderingComposer( + $db: $db, + $table: $db.matchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$GroupMatchTableTableAnnotationComposer + extends Composer<_$AppDatabase, $GroupMatchTableTable> { + $$GroupMatchTableTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + $$GroupTableTableAnnotationComposer get groupId { + final $$GroupTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.groupId, + referencedTable: $db.groupTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$GroupTableTableAnnotationComposer( + $db: $db, + $table: $db.groupTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } + + $$MatchTableTableAnnotationComposer get matchId { + final $$MatchTableTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.matchId, + referencedTable: $db.matchTable, + getReferencedColumn: (t) => t.id, + builder: + ( + joinBuilder, { + $addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer, + }) => $$MatchTableTableAnnotationComposer( + $db: $db, + $table: $db.matchTable, + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + ), + ); + return composer; + } +} + +class $$GroupMatchTableTableTableManager + extends + RootTableManager< + _$AppDatabase, + $GroupMatchTableTable, + GroupMatchTableData, + $$GroupMatchTableTableFilterComposer, + $$GroupMatchTableTableOrderingComposer, + $$GroupMatchTableTableAnnotationComposer, + $$GroupMatchTableTableCreateCompanionBuilder, + $$GroupMatchTableTableUpdateCompanionBuilder, + (GroupMatchTableData, $$GroupMatchTableTableReferences), + GroupMatchTableData, + PrefetchHooks Function({bool groupId, bool matchId}) + > { + $$GroupMatchTableTableTableManager( + _$AppDatabase db, + $GroupMatchTableTable table, + ) : super( + TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$GroupMatchTableTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$GroupMatchTableTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$GroupMatchTableTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: + ({ + Value groupId = const Value.absent(), + Value matchId = const Value.absent(), + Value rowid = const Value.absent(), + }) => GroupMatchTableCompanion( + groupId: groupId, + matchId: matchId, + rowid: rowid, + ), + createCompanionCallback: + ({ + required String groupId, + required String matchId, + Value rowid = const Value.absent(), + }) => GroupMatchTableCompanion.insert( + groupId: groupId, + matchId: matchId, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map( + (e) => ( + e.readTable(table), + $$GroupMatchTableTableReferences(db, table, e), + ), + ) + .toList(), + prefetchHooksCallback: ({groupId = false, matchId = false}) { + return PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: + < + T extends TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic + > + >(state) { + if (groupId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.groupId, + referencedTable: + $$GroupMatchTableTableReferences + ._groupIdTable(db), + referencedColumn: + $$GroupMatchTableTableReferences + ._groupIdTable(db) + .id, + ) + as T; + } + if (matchId) { + state = + state.withJoin( + currentTable: table, + currentColumn: table.matchId, + referencedTable: + $$GroupMatchTableTableReferences + ._matchIdTable(db), + referencedColumn: + $$GroupMatchTableTableReferences + ._matchIdTable(db) + .id, + ) + as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + ), + ); +} + +typedef $$GroupMatchTableTableProcessedTableManager = + ProcessedTableManager< + _$AppDatabase, + $GroupMatchTableTable, + GroupMatchTableData, + $$GroupMatchTableTableFilterComposer, + $$GroupMatchTableTableOrderingComposer, + $$GroupMatchTableTableAnnotationComposer, + $$GroupMatchTableTableCreateCompanionBuilder, + $$GroupMatchTableTableUpdateCompanionBuilder, + (GroupMatchTableData, $$GroupMatchTableTableReferences), + GroupMatchTableData, + PrefetchHooks Function({bool groupId, bool matchId}) + >; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$PlayerTableTableTableManager get playerTable => + $$PlayerTableTableTableManager(_db, _db.playerTable); + $$GroupTableTableTableManager get groupTable => + $$GroupTableTableTableManager(_db, _db.groupTable); + $$MatchTableTableTableManager get matchTable => + $$MatchTableTableTableManager(_db, _db.matchTable); + $$PlayerGroupTableTableTableManager get playerGroupTable => + $$PlayerGroupTableTableTableManager(_db, _db.playerGroupTable); + $$PlayerMatchTableTableTableManager get playerMatchTable => + $$PlayerMatchTableTableTableManager(_db, _db.playerMatchTable); + $$GroupMatchTableTableTableManager get groupMatchTable => + $$GroupMatchTableTableTableManager(_db, _db.groupMatchTable); +} diff --git a/lib/data/db/tables/group_match_table.dart b/lib/data/db/tables/group_match_table.dart new file mode 100644 index 0000000..3f77dcb --- /dev/null +++ b/lib/data/db/tables/group_match_table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/tables/group_table.dart'; +import 'package:game_tracker/data/db/tables/match_table.dart'; + +class GroupMatchTable extends Table { + TextColumn get groupId => + text().references(GroupTable, #id, onDelete: KeyAction.cascade)(); + TextColumn get matchId => + text().references(MatchTable, #id, onDelete: KeyAction.cascade)(); + + @override + Set> get primaryKey => {groupId, matchId}; +} diff --git a/lib/data/db/tables/group_table.dart b/lib/data/db/tables/group_table.dart new file mode 100644 index 0000000..5c52355 --- /dev/null +++ b/lib/data/db/tables/group_table.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; + +class GroupTable extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get createdAt => dateTime()(); + + @override + Set> get primaryKey => {id}; +} diff --git a/lib/data/db/tables/match_table.dart b/lib/data/db/tables/match_table.dart new file mode 100644 index 0000000..96aff2a --- /dev/null +++ b/lib/data/db/tables/match_table.dart @@ -0,0 +1,11 @@ +import 'package:drift/drift.dart'; + +class MatchTable extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + late final winnerId = text().nullable()(); + DateTimeColumn get createdAt => dateTime()(); + + @override + Set> get primaryKey => {id}; +} diff --git a/lib/data/db/tables/player_group_table.dart b/lib/data/db/tables/player_group_table.dart new file mode 100644 index 0000000..da2521b --- /dev/null +++ b/lib/data/db/tables/player_group_table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/tables/group_table.dart'; +import 'package:game_tracker/data/db/tables/player_table.dart'; + +class PlayerGroupTable extends Table { + TextColumn get playerId => + text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); + TextColumn get groupId => + text().references(GroupTable, #id, onDelete: KeyAction.cascade)(); + + @override + Set> get primaryKey => {playerId, groupId}; +} diff --git a/lib/data/db/tables/player_match_table.dart b/lib/data/db/tables/player_match_table.dart new file mode 100644 index 0000000..e155cd5 --- /dev/null +++ b/lib/data/db/tables/player_match_table.dart @@ -0,0 +1,13 @@ +import 'package:drift/drift.dart'; +import 'package:game_tracker/data/db/tables/match_table.dart'; +import 'package:game_tracker/data/db/tables/player_table.dart'; + +class PlayerMatchTable extends Table { + TextColumn get playerId => + text().references(PlayerTable, #id, onDelete: KeyAction.cascade)(); + TextColumn get matchId => + text().references(MatchTable, #id, onDelete: KeyAction.cascade)(); + + @override + Set> get primaryKey => {playerId, matchId}; +} diff --git a/lib/data/db/tables/player_table.dart b/lib/data/db/tables/player_table.dart new file mode 100644 index 0000000..794958e --- /dev/null +++ b/lib/data/db/tables/player_table.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; + +class PlayerTable extends Table { + TextColumn get id => text()(); + TextColumn get name => text()(); + DateTimeColumn get createdAt => dateTime()(); + + @override + Set> get primaryKey => {id}; +} diff --git a/lib/data/dto/group.dart b/lib/data/dto/group.dart new file mode 100644 index 0000000..92dbd09 --- /dev/null +++ b/lib/data/dto/group.dart @@ -0,0 +1,40 @@ +import 'package:clock/clock.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:uuid/uuid.dart'; + +class Group { + final String id; + final DateTime createdAt; + final String name; + final List members; + + Group({ + String? id, + DateTime? createdAt, + required this.name, + required this.members, + }) : id = id ?? const Uuid().v4(), + createdAt = createdAt ?? clock.now(); + + @override + String toString() { + return 'Group{id: $id, name: $name,members: $members}'; + } + + /// Creates a Group instance from a JSON object. + Group.fromJson(Map json) + : id = json['id'], + createdAt = DateTime.parse(json['createdAt']), + name = json['name'], + members = (json['members'] as List) + .map((memberJson) => Player.fromJson(memberJson)) + .toList(); + + /// Converts the Group instance to a JSON object. + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + 'members': members.map((member) => member.toJson()).toList(), + }; +} diff --git a/lib/data/dto/match.dart b/lib/data/dto/match.dart new file mode 100644 index 0000000..fcb4dae --- /dev/null +++ b/lib/data/dto/match.dart @@ -0,0 +1,51 @@ +import 'package:clock/clock.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:uuid/uuid.dart'; + +class Match { + final String id; + final DateTime createdAt; + final String name; + final List? players; + final Group? group; + final Player? winner; + + Match({ + String? id, + DateTime? createdAt, + required this.name, + this.players, + this.group, + this.winner, + }) : id = id ?? const Uuid().v4(), + createdAt = createdAt ?? clock.now(); + + @override + String toString() { + return 'Match{\n\tid: $id,\n\tname: $name,\n\tplayers: $players,\n\tgroup: $group,\n\twinner: $winner\n}'; + } + + /// Creates a Match instance from a JSON object. + Match.fromJson(Map json) + : id = json['id'], + name = json['name'], + createdAt = DateTime.parse(json['createdAt']), + players = json['players'] != null + ? (json['players'] as List) + .map((playerJson) => Player.fromJson(playerJson)) + .toList() + : null, + group = json['group'] != null ? Group.fromJson(json['group']) : null, + winner = json['winner'] != null ? Player.fromJson(json['winner']) : null; + + /// Converts the Match instance to a JSON object. + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + 'players': players?.map((player) => player.toJson()).toList(), + 'group': group?.toJson(), + 'winner': winner?.toJson(), + }; +} diff --git a/lib/data/dto/player.dart b/lib/data/dto/player.dart new file mode 100644 index 0000000..cfb4f4b --- /dev/null +++ b/lib/data/dto/player.dart @@ -0,0 +1,30 @@ +import 'package:clock/clock.dart'; +import 'package:uuid/uuid.dart'; + +class Player { + final String id; + final DateTime createdAt; + final String name; + + Player({String? id, DateTime? createdAt, required this.name}) + : id = id ?? const Uuid().v4(), + createdAt = createdAt ?? clock.now(); + + @override + String toString() { + return 'Player{id: $id,name: $name}'; + } + + /// Creates a Player instance from a JSON object. + Player.fromJson(Map json) + : id = json['id'], + createdAt = DateTime.parse(json['createdAt']), + name = json['name']; + + /// Converts the Player instance to a JSON object. + Map toJson() => { + 'id': id, + 'createdAt': createdAt.toIso8601String(), + 'name': name, + }; +} diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb new file mode 100644 index 0000000..4d86460 --- /dev/null +++ b/lib/l10n/arb/app_de.arb @@ -0,0 +1,83 @@ +{ + "@@locale": "de", + "all_players": "Alle Spieler:innen", + "all_players_selected": "Alle Spieler:innen ausgewählt", + "amount_of_matches": "Anzahl der Spiele", + "cancel": "Abbrechen", + "choose_game": "Spielvorlage wählen", + "choose_group": "Gruppe wählen", + "choose_ruleset": "Regelwerk wählen", + "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "create_group": "Gruppe erstellen", + "create_match": "Spiel erstellen", + "create_new_group": "Neue Gruppe erstellen", + "create_new_match": "Neues Spiel erstellen", + "data_successfully_deleted": "Daten erfolgreich gelöscht", + "data_successfully_exported": "Daten erfolgreich exportiert", + "data_successfully_imported": "Daten erfolgreich importiert", + "days_ago": "vor {count} Tagen", + "delete": "Löschen", + "delete_all_data": "Alle Daten löschen?", + "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "error_reading_file": "Fehler beim Lesen der Datei", + "export_canceled": "Export abgebrochen", + "export_data": "Daten exportieren", + "format_exception": "Formatfehler (siehe Konsole)", + "game": "Spielvorlage", + "game_name": "Spielvorlagenname", + "group": "Gruppe", + "group_name": "Gruppenname", + "groups": "Gruppen", + "home": "Startseite", + "import_canceled": "Import abgebrochen", + "import_data": "Daten importieren", + "info": "Info", + "invalid_schema": "Ungültiges Schema", + "least_points": "Niedrigste Punkte", + "match_in_progress": "Spiel läuft...", + "match_name": "Spieltitel", + "matches": "Spiele", + "menu": "Menü", + "most_points": "Höchste Punkte", + "no_data_available": "Keine Daten verfügbar", + "no_groups_created_yet": "Noch keine Gruppen erstellt", + "no_matches_created_yet": "Noch keine Spiele erstellt", + "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", + "no_recent_matches_available": "Keine letzten Spiele verfügbar", + "no_second_match_available": "Kein zweites Spiel verfügbar", + "no_statistics_available": "Keine Statistiken verfügbar", + "none": "Kein", + "none_group": "Keine", + "not_available": "Nicht verfügbar", + "player_name": "Spieler:innenname", + "players": "Spieler:innen", + "players_count": "{count} Spieler", + "quick_create": "Schnellzugriff", + "recent_matches": "Letzte Spiele", + "ruleset": "Regelwerk", + "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", + "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", + "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", + "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", + "search_for_groups": "Nach Gruppen suchen", + "search_for_players": "Nach Spieler:innen suchen", + "select_winner": "Gewinner:in wählen:", + "selected_players": "Ausgewählte Spieler:innen", + "settings": "Einstellungen", + "single_loser": "Ein:e Verlierer:in", + "single_winner": "Ein:e Gewinner:in", + "statistics": "Statistiken", + "stats": "Statistiken", + "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", + "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", + "today_at": "Heute um", + "undo": "Rückgängig", + "unknown_exception": "Unbekannter Fehler (siehe Konsole)", + "winner": "Gewinner:in", + "winrate": "Siegquote", + "wins": "Siege", + "yesterday_at": "Gestern um" +} \ No newline at end of file diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb new file mode 100644 index 0000000..17c3b06 --- /dev/null +++ b/lib/l10n/arb/app_en.arb @@ -0,0 +1,343 @@ +{ + "@@locale": "en", + "@all_players": { + "description": "Label for all players list" + }, + "@all_players_selected": { + "description": "Message when all players are added to selection" + }, + "@amount_of_matches": { + "description": "Label for amount of matches statistic" + }, + "@app_name": { + "description": "The name of the App" + }, + "@cancel": { + "description": "Cancel button text" + }, + "@choose_game": { + "description": "Label for choosing a game" + }, + "@choose_group": { + "description": "Label for choosing a group" + }, + "@choose_ruleset": { + "description": "Label for choosing a ruleset" + }, + "@could_not_add_player": { + "description": "Error message when adding a player fails" + }, + "@create_group": { + "description": "Button text to create a group" + }, + "@create_match": { + "description": "Button text to create a match" + }, + "@create_new_group": { + "description": "Button text to create a new group" + }, + "@create_new_match": { + "description": "Button text to create a new match" + }, + "@data_successfully_deleted": { + "description": "Success message after deleting data" + }, + "@data_successfully_exported": { + "description": "Success message after exporting data" + }, + "@data_successfully_imported": { + "description": "Success message after importing data" + }, + "@days_ago": { + "description": "Date format for days ago", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@delete": { + "description": "Delete button text" + }, + "@delete_all_data": { + "description": "Confirmation dialog for deleting all data" + }, + "@error_creating_group": { + "description": "Error message when group creation fails" + }, + "@error_reading_file": { + "description": "Error message when file cannot be read" + }, + "@export_canceled": { + "description": "Message when export is canceled" + }, + "@export_data": { + "description": "Export data menu item" + }, + "@format_exception": { + "description": "Error message for format exceptions" + }, + "@game": { + "description": "Game label" + }, + "@game_name": { + "description": "Placeholder for game name search" + }, + "@group": { + "description": "Group label" + }, + "@group_name": { + "description": "Placeholder for group name input" + }, + "@groups": { + "description": "Label for groups" + }, + "@home": { + "description": "Home tab label" + }, + "@import_canceled": { + "description": "Message when import is canceled" + }, + "@import_data": { + "description": "Import data menu item" + }, + "@info": { + "description": "Info label" + }, + "@invalid_schema": { + "description": "Error message for invalid schema" + }, + "@least_points": { + "description": "Title for least points ruleset" + }, + "@match_in_progress": { + "description": "Message when match is in progress" + }, + "@match_name": { + "description": "Placeholder for match name input" + }, + "@matches": { + "description": "Label for matches" + }, + "@menu": { + "description": "Menu label" + }, + "@most_points": { + "description": "Title for most points ruleset" + }, + "@no_data_available": { + "description": "Message when no data in the statistic tiles is given" + }, + "@no_groups_created_yet": { + "description": "Message when no groups exist" + }, + "@no_matches_created_yet": { + "description": "Message when no matches exist" + }, + "@no_players_created_yet": { + "description": "Message when no players exist" + }, + "@no_players_found_with_that_name": { + "description": "Message when search returns no results" + }, + "@no_players_selected": { + "description": "Message when no players are selected" + }, + "@no_recent_matches_available": { + "description": "Message when no recent matches exist" + }, + "@no_second_match_available": { + "description": "Message when no second match exists" + }, + "@no_statistics_available": { + "description": "Message when no statistics are available, because no matches were played yet" + }, + "@none": { + "description": "None option label" + }, + "@none_group": { + "description": "None group option label" + }, + "@not_available": { + "description": "Abbreviation for not available" + }, + "@player_name": { + "description": "Placeholder for player name input" + }, + "@players": { + "description": "Players label" + }, + "@players_count": { + "description": "Shows the number of players", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "@quick_create": { + "description": "Title for quick create section" + }, + "@recent_matches": { + "description": "Title for recent matches section" + }, + "@ruleset": { + "description": "Ruleset label" + }, + "@ruleset_least_points": { + "description": "Description for least points ruleset" + }, + "@ruleset_most_points": { + "description": "Description for most points ruleset" + }, + "@ruleset_single_loser": { + "description": "Description for single loser ruleset" + }, + "@ruleset_single_winner": { + "description": "Description for single winner ruleset" + }, + "@search_for_groups": { + "description": "Hint text for group search input field" + }, + "@search_for_players": { + "description": "Hint text for player search input field" + }, + "@select_winner": { + "description": "Label to select the winner" + }, + "@selected_players": { + "description": "Shows the number of selected players" + }, + "@settings": { + "description": "Settings label" + }, + "@single_loser": { + "description": "Title for single loser ruleset" + }, + "@single_winner": { + "description": "Title for single winner ruleset" + }, + "@statistics": { + "description": "Statistics tab label" + }, + "@stats": { + "description": "Stats tab label (short)" + }, + "@successfully_added_player": { + "description": "Success message when adding a player", + "placeholders": { + "playerName": { + "type": "String", + "example": "John" + } + } + }, + "@there_is_no_group_matching_your_search": { + "description": "Message when search returns no groups" + }, + "@this_cannot_be_undone": { + "description": "Warning message for irreversible actions" + }, + "@today_at": { + "description": "Date format for today" + }, + "@undo": { + "description": "Undo button text" + }, + "@unknown_exception": { + "description": "Error message for unknown exceptions" + }, + "@winner": { + "description": "Winner label" + }, + "@winrate": { + "description": "Label for winrate statistic" + }, + "@wins": { + "description": "Label for wins statistic" + }, + "@yesterday_at": { + "description": "Date format for yesterday" + }, + "all_players": "All players", + "all_players_selected": "All players selected", + "amount_of_matches": "Amount of Matches", + "app_name": "Game Tracker", + "cancel": "Cancel", + "choose_game": "Choose Game", + "choose_group": "Choose Group", + "choose_ruleset": "Choose Ruleset", + "could_not_add_player": "Could not add player", + "create_group": "Create Group", + "create_match": "Create match", + "create_new_group": "Create new group", + "create_new_match": "Create new match", + "data_successfully_deleted": "Data successfully deleted", + "data_successfully_exported": "Data successfully exported", + "data_successfully_imported": "Data successfully imported", + "days_ago": "{count} days ago", + "delete": "Delete", + "delete_all_data": "Delete all data?", + "error_creating_group": "Error while creating group, please try again", + "error_reading_file": "Error reading file", + "export_canceled": "Export canceled", + "export_data": "Export data", + "format_exception": "Format Exception (see console)", + "game": "Game", + "game_name": "Game Name", + "group": "Group", + "group_name": "Group name", + "groups": "Groups", + "home": "Home", + "import_canceled": "Import canceled", + "import_data": "Import data", + "info": "Info", + "invalid_schema": "Invalid Schema", + "least_points": "Least Points", + "match_in_progress": "Match in progress...", + "match_name": "Match name", + "matches": "Matches", + "menu": "Menu", + "most_points": "Most Points", + "no_data_available": "No data available", + "no_groups_created_yet": "No groups created yet", + "no_matches_created_yet": "No matches created yet", + "no_players_created_yet": "No players created yet", + "no_players_found_with_that_name": "No players found with that name", + "no_players_selected": "No players selected", + "no_recent_matches_available": "No recent matches available", + "no_second_match_available": "No second match available", + "no_statistics_available": "No statistics available", + "none": "None", + "none_group": "None", + "not_available": "Not available", + "player_name": "Player name", + "players": "Players", + "players_count": "{count} Players", + "quick_create": "Quick Create", + "recent_matches": "Recent Matches", + "ruleset": "Ruleset", + "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", + "ruleset_most_points": "Traditional ruleset: the player with the most points wins.", + "ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.", + "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", + "search_for_groups": "Search for groups", + "search_for_players": "Search for players", + "select_winner": "Select Winner:", + "selected_players": "Selected players", + "settings": "Settings", + "single_loser": "Single Loser", + "single_winner": "Single Winner", + "statistics": "Statistics", + "stats": "Stats", + "successfully_added_player": "Successfully added player {playerName}", + "there_is_no_group_matching_your_search": "There is no group matching your search", + "this_cannot_be_undone": "This can't be undone", + "today_at": "Today at", + "undo": "Undo", + "unknown_exception": "Unknown Exception (see console)", + "winner": "Winner", + "winrate": "Winrate", + "wins": "Wins", + "yesterday_at": "Yesterday at" +} \ No newline at end of file diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart new file mode 100644 index 0000000..5080ff3 --- /dev/null +++ b/lib/l10n/generated/app_localizations.dart @@ -0,0 +1,620 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_de.dart'; +import 'app_localizations_en.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'generated/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations of(BuildContext context) { + return Localizations.of(context, AppLocalizations)!; + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('de'), + Locale('en'), + ]; + + /// Label for all players list + /// + /// In en, this message translates to: + /// **'All players'** + String get all_players; + + /// Message when all players are added to selection + /// + /// In en, this message translates to: + /// **'All players selected'** + String get all_players_selected; + + /// Label for amount of matches statistic + /// + /// In en, this message translates to: + /// **'Amount of Matches'** + String get amount_of_matches; + + /// The name of the App + /// + /// In en, this message translates to: + /// **'Game Tracker'** + String get app_name; + + /// Cancel button text + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// Label for choosing a game + /// + /// In en, this message translates to: + /// **'Choose Game'** + String get choose_game; + + /// Label for choosing a group + /// + /// In en, this message translates to: + /// **'Choose Group'** + String get choose_group; + + /// Label for choosing a ruleset + /// + /// In en, this message translates to: + /// **'Choose Ruleset'** + String get choose_ruleset; + + /// Error message when adding a player fails + /// + /// In en, this message translates to: + /// **'Could not add player'** + String could_not_add_player(Object playerName); + + /// Button text to create a group + /// + /// In en, this message translates to: + /// **'Create Group'** + String get create_group; + + /// Button text to create a match + /// + /// In en, this message translates to: + /// **'Create match'** + String get create_match; + + /// Button text to create a new group + /// + /// In en, this message translates to: + /// **'Create new group'** + String get create_new_group; + + /// Button text to create a new match + /// + /// In en, this message translates to: + /// **'Create new match'** + String get create_new_match; + + /// Success message after deleting data + /// + /// In en, this message translates to: + /// **'Data successfully deleted'** + String get data_successfully_deleted; + + /// Success message after exporting data + /// + /// In en, this message translates to: + /// **'Data successfully exported'** + String get data_successfully_exported; + + /// Success message after importing data + /// + /// In en, this message translates to: + /// **'Data successfully imported'** + String get data_successfully_imported; + + /// Date format for days ago + /// + /// In en, this message translates to: + /// **'{count} days ago'** + String days_ago(int count); + + /// Delete button text + /// + /// In en, this message translates to: + /// **'Delete'** + String get delete; + + /// Confirmation dialog for deleting all data + /// + /// In en, this message translates to: + /// **'Delete all data?'** + String get delete_all_data; + + /// Error message when group creation fails + /// + /// In en, this message translates to: + /// **'Error while creating group, please try again'** + String get error_creating_group; + + /// Error message when file cannot be read + /// + /// In en, this message translates to: + /// **'Error reading file'** + String get error_reading_file; + + /// Message when export is canceled + /// + /// In en, this message translates to: + /// **'Export canceled'** + String get export_canceled; + + /// Export data menu item + /// + /// In en, this message translates to: + /// **'Export data'** + String get export_data; + + /// Error message for format exceptions + /// + /// In en, this message translates to: + /// **'Format Exception (see console)'** + String get format_exception; + + /// Game label + /// + /// In en, this message translates to: + /// **'Game'** + String get game; + + /// Placeholder for game name search + /// + /// In en, this message translates to: + /// **'Game Name'** + String get game_name; + + /// Group label + /// + /// In en, this message translates to: + /// **'Group'** + String get group; + + /// Placeholder for group name input + /// + /// In en, this message translates to: + /// **'Group name'** + String get group_name; + + /// Label for groups + /// + /// In en, this message translates to: + /// **'Groups'** + String get groups; + + /// Home tab label + /// + /// In en, this message translates to: + /// **'Home'** + String get home; + + /// Message when import is canceled + /// + /// In en, this message translates to: + /// **'Import canceled'** + String get import_canceled; + + /// Import data menu item + /// + /// In en, this message translates to: + /// **'Import data'** + String get import_data; + + /// Info label + /// + /// In en, this message translates to: + /// **'Info'** + String get info; + + /// Error message for invalid schema + /// + /// In en, this message translates to: + /// **'Invalid Schema'** + String get invalid_schema; + + /// Title for least points ruleset + /// + /// In en, this message translates to: + /// **'Least Points'** + String get least_points; + + /// Message when match is in progress + /// + /// In en, this message translates to: + /// **'Match in progress...'** + String get match_in_progress; + + /// Placeholder for match name input + /// + /// In en, this message translates to: + /// **'Match name'** + String get match_name; + + /// Label for matches + /// + /// In en, this message translates to: + /// **'Matches'** + String get matches; + + /// Menu label + /// + /// In en, this message translates to: + /// **'Menu'** + String get menu; + + /// Title for most points ruleset + /// + /// In en, this message translates to: + /// **'Most Points'** + String get most_points; + + /// Message when no data in the statistic tiles is given + /// + /// In en, this message translates to: + /// **'No data available'** + String get no_data_available; + + /// Message when no groups exist + /// + /// In en, this message translates to: + /// **'No groups created yet'** + String get no_groups_created_yet; + + /// Message when no matches exist + /// + /// In en, this message translates to: + /// **'No matches created yet'** + String get no_matches_created_yet; + + /// Message when no players exist + /// + /// In en, this message translates to: + /// **'No players created yet'** + String get no_players_created_yet; + + /// Message when search returns no results + /// + /// In en, this message translates to: + /// **'No players found with that name'** + String get no_players_found_with_that_name; + + /// Message when no players are selected + /// + /// In en, this message translates to: + /// **'No players selected'** + String get no_players_selected; + + /// Message when no recent matches exist + /// + /// In en, this message translates to: + /// **'No recent matches available'** + String get no_recent_matches_available; + + /// Message when no second match exists + /// + /// In en, this message translates to: + /// **'No second match available'** + String get no_second_match_available; + + /// Message when no statistics are available, because no matches were played yet + /// + /// In en, this message translates to: + /// **'No statistics available'** + String get no_statistics_available; + + /// None option label + /// + /// In en, this message translates to: + /// **'None'** + String get none; + + /// None group option label + /// + /// In en, this message translates to: + /// **'None'** + String get none_group; + + /// Abbreviation for not available + /// + /// In en, this message translates to: + /// **'Not available'** + String get not_available; + + /// Placeholder for player name input + /// + /// In en, this message translates to: + /// **'Player name'** + String get player_name; + + /// Players label + /// + /// In en, this message translates to: + /// **'Players'** + String get players; + + /// Shows the number of players + /// + /// In en, this message translates to: + /// **'{count} Players'** + String players_count(int count); + + /// Title for quick create section + /// + /// In en, this message translates to: + /// **'Quick Create'** + String get quick_create; + + /// Title for recent matches section + /// + /// In en, this message translates to: + /// **'Recent Matches'** + String get recent_matches; + + /// Ruleset label + /// + /// In en, this message translates to: + /// **'Ruleset'** + String get ruleset; + + /// Description for least points ruleset + /// + /// In en, this message translates to: + /// **'Inverse scoring: the player with the fewest points wins.'** + String get ruleset_least_points; + + /// Description for most points ruleset + /// + /// In en, this message translates to: + /// **'Traditional ruleset: the player with the most points wins.'** + String get ruleset_most_points; + + /// Description for single loser ruleset + /// + /// In en, this message translates to: + /// **'Exactly one loser is determined; last place receives the penalty or consequence.'** + String get ruleset_single_loser; + + /// Description for single winner ruleset + /// + /// In en, this message translates to: + /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** + String get ruleset_single_winner; + + /// Hint text for group search input field + /// + /// In en, this message translates to: + /// **'Search for groups'** + String get search_for_groups; + + /// Hint text for player search input field + /// + /// In en, this message translates to: + /// **'Search for players'** + String get search_for_players; + + /// Label to select the winner + /// + /// In en, this message translates to: + /// **'Select Winner:'** + String get select_winner; + + /// Shows the number of selected players + /// + /// In en, this message translates to: + /// **'Selected players'** + String get selected_players; + + /// Settings label + /// + /// In en, this message translates to: + /// **'Settings'** + String get settings; + + /// Title for single loser ruleset + /// + /// In en, this message translates to: + /// **'Single Loser'** + String get single_loser; + + /// Title for single winner ruleset + /// + /// In en, this message translates to: + /// **'Single Winner'** + String get single_winner; + + /// Statistics tab label + /// + /// In en, this message translates to: + /// **'Statistics'** + String get statistics; + + /// Stats tab label (short) + /// + /// In en, this message translates to: + /// **'Stats'** + String get stats; + + /// Success message when adding a player + /// + /// In en, this message translates to: + /// **'Successfully added player {playerName}'** + String successfully_added_player(String playerName); + + /// Message when search returns no groups + /// + /// In en, this message translates to: + /// **'There is no group matching your search'** + String get there_is_no_group_matching_your_search; + + /// Warning message for irreversible actions + /// + /// In en, this message translates to: + /// **'This can\'t be undone'** + String get this_cannot_be_undone; + + /// Date format for today + /// + /// In en, this message translates to: + /// **'Today at'** + String get today_at; + + /// Undo button text + /// + /// In en, this message translates to: + /// **'Undo'** + String get undo; + + /// Error message for unknown exceptions + /// + /// In en, this message translates to: + /// **'Unknown Exception (see console)'** + String get unknown_exception; + + /// Winner label + /// + /// In en, this message translates to: + /// **'Winner'** + String get winner; + + /// Label for winrate statistic + /// + /// In en, this message translates to: + /// **'Winrate'** + String get winrate; + + /// Label for wins statistic + /// + /// In en, this message translates to: + /// **'Wins'** + String get wins; + + /// Date format for yesterday + /// + /// In en, this message translates to: + /// **'Yesterday at'** + String get yesterday_at; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['de', 'en'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'de': + return AppLocalizationsDe(); + case 'en': + return AppLocalizationsEn(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart new file mode 100644 index 0000000..c720941 --- /dev/null +++ b/lib/l10n/generated/app_localizations_de.dart @@ -0,0 +1,269 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for German (`de`). +class AppLocalizationsDe extends AppLocalizations { + AppLocalizationsDe([String locale = 'de']) : super(locale); + + @override + String get all_players => 'Alle Spieler:innen'; + + @override + String get all_players_selected => 'Alle Spieler:innen ausgewählt'; + + @override + String get amount_of_matches => 'Anzahl der Spiele'; + + @override + String get app_name => 'Game Tracker'; + + @override + String get cancel => 'Abbrechen'; + + @override + String get choose_game => 'Spielvorlage wählen'; + + @override + String get choose_group => 'Gruppe wählen'; + + @override + String get choose_ruleset => 'Regelwerk wählen'; + + @override + String could_not_add_player(Object playerName) { + return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; + } + + @override + String get create_group => 'Gruppe erstellen'; + + @override + String get create_match => 'Spiel erstellen'; + + @override + String get create_new_group => 'Neue Gruppe erstellen'; + + @override + String get create_new_match => 'Neues Spiel erstellen'; + + @override + String get data_successfully_deleted => 'Daten erfolgreich gelöscht'; + + @override + String get data_successfully_exported => 'Daten erfolgreich exportiert'; + + @override + String get data_successfully_imported => 'Daten erfolgreich importiert'; + + @override + String days_ago(int count) { + return 'vor $count Tagen'; + } + + @override + String get delete => 'Löschen'; + + @override + String get delete_all_data => 'Alle Daten löschen?'; + + @override + String get error_creating_group => + 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + + @override + String get error_reading_file => 'Fehler beim Lesen der Datei'; + + @override + String get export_canceled => 'Export abgebrochen'; + + @override + String get export_data => 'Daten exportieren'; + + @override + String get format_exception => 'Formatfehler (siehe Konsole)'; + + @override + String get game => 'Spielvorlage'; + + @override + String get game_name => 'Spielvorlagenname'; + + @override + String get group => 'Gruppe'; + + @override + String get group_name => 'Gruppenname'; + + @override + String get groups => 'Gruppen'; + + @override + String get home => 'Startseite'; + + @override + String get import_canceled => 'Import abgebrochen'; + + @override + String get import_data => 'Daten importieren'; + + @override + String get info => 'Info'; + + @override + String get invalid_schema => 'Ungültiges Schema'; + + @override + String get least_points => 'Niedrigste Punkte'; + + @override + String get match_in_progress => 'Spiel läuft...'; + + @override + String get match_name => 'Spieltitel'; + + @override + String get matches => 'Spiele'; + + @override + String get menu => 'Menü'; + + @override + String get most_points => 'Höchste Punkte'; + + @override + String get no_data_available => 'Keine Daten verfügbar'; + + @override + String get no_groups_created_yet => 'Noch keine Gruppen erstellt'; + + @override + String get no_matches_created_yet => 'Noch keine Spiele erstellt'; + + @override + String get no_players_created_yet => 'Noch keine Spieler:in erstellt'; + + @override + String get no_players_found_with_that_name => + 'Keine Spieler:in mit diesem Namen gefunden'; + + @override + String get no_players_selected => 'Keine Spieler:innen ausgewählt'; + + @override + String get no_recent_matches_available => 'Keine letzten Spiele verfügbar'; + + @override + String get no_second_match_available => 'Kein zweites Spiel verfügbar'; + + @override + String get no_statistics_available => 'Keine Statistiken verfügbar'; + + @override + String get none => 'Kein'; + + @override + String get none_group => 'Keine'; + + @override + String get not_available => 'Nicht verfügbar'; + + @override + String get player_name => 'Spieler:innenname'; + + @override + String get players => 'Spieler:innen'; + + @override + String players_count(int count) { + return '$count Spieler'; + } + + @override + String get quick_create => 'Schnellzugriff'; + + @override + String get recent_matches => 'Letzte Spiele'; + + @override + String get ruleset => 'Regelwerk'; + + @override + String get ruleset_least_points => + 'Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.'; + + @override + String get ruleset_most_points => + 'Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.'; + + @override + String get ruleset_single_loser => + 'Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.'; + + @override + String get ruleset_single_winner => + 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.'; + + @override + String get search_for_groups => 'Nach Gruppen suchen'; + + @override + String get search_for_players => 'Nach Spieler:innen suchen'; + + @override + String get select_winner => 'Gewinner:in wählen:'; + + @override + String get selected_players => 'Ausgewählte Spieler:innen'; + + @override + String get settings => 'Einstellungen'; + + @override + String get single_loser => 'Ein:e Verlierer:in'; + + @override + String get single_winner => 'Ein:e Gewinner:in'; + + @override + String get statistics => 'Statistiken'; + + @override + String get stats => 'Statistiken'; + + @override + String successfully_added_player(String playerName) { + return 'Spieler:in $playerName erfolgreich hinzugefügt'; + } + + @override + String get there_is_no_group_matching_your_search => + 'Es gibt keine Gruppe, die deiner Suche entspricht'; + + @override + String get this_cannot_be_undone => + 'Dies kann nicht rückgängig gemacht werden'; + + @override + String get today_at => 'Heute um'; + + @override + String get undo => 'Rückgängig'; + + @override + String get unknown_exception => 'Unbekannter Fehler (siehe Konsole)'; + + @override + String get winner => 'Gewinner:in'; + + @override + String get winrate => 'Siegquote'; + + @override + String get wins => 'Siege'; + + @override + String get yesterday_at => 'Gestern um'; +} diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart new file mode 100644 index 0000000..cd71035 --- /dev/null +++ b/lib/l10n/generated/app_localizations_en.dart @@ -0,0 +1,268 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get all_players => 'All players'; + + @override + String get all_players_selected => 'All players selected'; + + @override + String get amount_of_matches => 'Amount of Matches'; + + @override + String get app_name => 'Game Tracker'; + + @override + String get cancel => 'Cancel'; + + @override + String get choose_game => 'Choose Game'; + + @override + String get choose_group => 'Choose Group'; + + @override + String get choose_ruleset => 'Choose Ruleset'; + + @override + String could_not_add_player(Object playerName) { + return 'Could not add player'; + } + + @override + String get create_group => 'Create Group'; + + @override + String get create_match => 'Create match'; + + @override + String get create_new_group => 'Create new group'; + + @override + String get create_new_match => 'Create new match'; + + @override + String get data_successfully_deleted => 'Data successfully deleted'; + + @override + String get data_successfully_exported => 'Data successfully exported'; + + @override + String get data_successfully_imported => 'Data successfully imported'; + + @override + String days_ago(int count) { + return '$count days ago'; + } + + @override + String get delete => 'Delete'; + + @override + String get delete_all_data => 'Delete all data?'; + + @override + String get error_creating_group => + 'Error while creating group, please try again'; + + @override + String get error_reading_file => 'Error reading file'; + + @override + String get export_canceled => 'Export canceled'; + + @override + String get export_data => 'Export data'; + + @override + String get format_exception => 'Format Exception (see console)'; + + @override + String get game => 'Game'; + + @override + String get game_name => 'Game Name'; + + @override + String get group => 'Group'; + + @override + String get group_name => 'Group name'; + + @override + String get groups => 'Groups'; + + @override + String get home => 'Home'; + + @override + String get import_canceled => 'Import canceled'; + + @override + String get import_data => 'Import data'; + + @override + String get info => 'Info'; + + @override + String get invalid_schema => 'Invalid Schema'; + + @override + String get least_points => 'Least Points'; + + @override + String get match_in_progress => 'Match in progress...'; + + @override + String get match_name => 'Match name'; + + @override + String get matches => 'Matches'; + + @override + String get menu => 'Menu'; + + @override + String get most_points => 'Most Points'; + + @override + String get no_data_available => 'No data available'; + + @override + String get no_groups_created_yet => 'No groups created yet'; + + @override + String get no_matches_created_yet => 'No matches created yet'; + + @override + String get no_players_created_yet => 'No players created yet'; + + @override + String get no_players_found_with_that_name => + 'No players found with that name'; + + @override + String get no_players_selected => 'No players selected'; + + @override + String get no_recent_matches_available => 'No recent matches available'; + + @override + String get no_second_match_available => 'No second match available'; + + @override + String get no_statistics_available => 'No statistics available'; + + @override + String get none => 'None'; + + @override + String get none_group => 'None'; + + @override + String get not_available => 'Not available'; + + @override + String get player_name => 'Player name'; + + @override + String get players => 'Players'; + + @override + String players_count(int count) { + return '$count Players'; + } + + @override + String get quick_create => 'Quick Create'; + + @override + String get recent_matches => 'Recent Matches'; + + @override + String get ruleset => 'Ruleset'; + + @override + String get ruleset_least_points => + 'Inverse scoring: the player with the fewest points wins.'; + + @override + String get ruleset_most_points => + 'Traditional ruleset: the player with the most points wins.'; + + @override + String get ruleset_single_loser => + 'Exactly one loser is determined; last place receives the penalty or consequence.'; + + @override + String get ruleset_single_winner => + 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'; + + @override + String get search_for_groups => 'Search for groups'; + + @override + String get search_for_players => 'Search for players'; + + @override + String get select_winner => 'Select Winner:'; + + @override + String get selected_players => 'Selected players'; + + @override + String get settings => 'Settings'; + + @override + String get single_loser => 'Single Loser'; + + @override + String get single_winner => 'Single Winner'; + + @override + String get statistics => 'Statistics'; + + @override + String get stats => 'Stats'; + + @override + String successfully_added_player(String playerName) { + return 'Successfully added player $playerName'; + } + + @override + String get there_is_no_group_matching_your_search => + 'There is no group matching your search'; + + @override + String get this_cannot_be_undone => 'This can\'t be undone'; + + @override + String get today_at => 'Today at'; + + @override + String get undo => 'Undo'; + + @override + String get unknown_exception => 'Unknown Exception (see console)'; + + @override + String get winner => 'Winner'; + + @override + String get winrate => 'Winrate'; + + @override + String get wins => 'Wins'; + + @override + String get yesterday_at => 'Yesterday at'; +} diff --git a/lib/main.dart b/lib/main.dart index 1188332..1dee10b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,51 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/presentation/views/home_view.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/custom_navigation_bar.dart'; +import 'package:provider/provider.dart'; void main() { - runApp(const MyApp()); + runApp( + Provider( + create: (context) => AppDatabase(), + child: const GameTracker(), + dispose: (context, db) => db.close(), + ), + ); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class GameTracker extends StatelessWidget { + const GameTracker({super.key}); @override Widget build(BuildContext context) { return MaterialApp( - title: 'Game Tracker', + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + localeResolutionCallback: (locale, supportedLocales) { + for (final supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale?.languageCode) { + return supportedLocale; + } + } + return supportedLocales.firstWhere( + (locale) => locale.languageCode == 'en', + ); + }, + debugShowCheckedModeBanner: false, + onGenerateTitle: (context) => AppLocalizations.of(context).app_name, + themeMode: ThemeMode.dark, // forces dark mode theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + primaryColor: CustomTheme.primaryColor, + scaffoldBackgroundColor: CustomTheme.backgroundColor, + appBarTheme: CustomTheme.appBarTheme, + colorScheme: ColorScheme.fromSeed( + seedColor: CustomTheme.primaryColor, + brightness: Brightness.dark, + ).copyWith(surface: CustomTheme.backgroundColor), ), - home: const HomeView(), + home: const CustomNavigationBar(), ); } } diff --git a/lib/presentation/views/home_view.dart b/lib/presentation/views/home_view.dart deleted file mode 100644 index 234dc1a..0000000 --- a/lib/presentation/views/home_view.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; - -class HomeView extends StatelessWidget { - const HomeView({super.key}); - - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart new file mode 100644 index 0000000..a8b18c8 --- /dev/null +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/settings_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart'; +import 'package:game_tracker/presentation/widgets/navbar_item.dart'; + +class CustomNavigationBar extends StatefulWidget { + const CustomNavigationBar({super.key}); + + @override + State createState() => _CustomNavigationBarState(); +} + +class _CustomNavigationBarState extends State + with SingleTickerProviderStateMixin { + /// Currently selected tab index + int currentIndex = 0; + + /// Key count to force rebuild of tab views + int tabKeyCount = 0; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + // Pretty ugly but works + final List tabs = [ + KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()), + KeyedSubtree( + key: ValueKey('matches_$tabKeyCount'), + child: const MatchView(), + ), + KeyedSubtree( + key: ValueKey('groups_$tabKeyCount'), + child: const GroupsView(), + ), + KeyedSubtree( + key: ValueKey('stats_$tabKeyCount'), + child: const StatisticsView(), + ), + ]; + return Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text( + _currentTabTitle(context), + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + backgroundColor: CustomTheme.backgroundColor, + scrolledUnderElevation: 0, + actions: [ + IconButton( + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute(builder: (_) => const SettingsView()), + ); + setState(() { + tabKeyCount++; + }); + }, + icon: const Icon(Icons.settings), + ), + ], + elevation: 0, + ), + backgroundColor: CustomTheme.backgroundColor, + body: tabs[currentIndex], + extendBody: true, + bottomNavigationBar: SafeArea( + minimum: const EdgeInsets.only(bottom: 30), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: CustomTheme.primaryColor, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: SizedBox( + height: 60, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + NavbarItem( + index: 0, + isSelected: currentIndex == 0, + icon: Icons.home_rounded, + label: loc.home, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 1, + isSelected: currentIndex == 1, + icon: Icons.gamepad_rounded, + label: loc.matches, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 2, + isSelected: currentIndex == 2, + icon: Icons.group_rounded, + label: loc.groups, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: loc.statistics, + onTabTapped: onTabTapped, + ), + ], + ), + ), + ), + ), + ), + ); + } + + /// Handles tab tap events. Updates the current [index] state. + void onTabTapped(int index) { + setState(() { + currentIndex = index; + }); + } + + /// Returns the title of the current tab based on [currentIndex]. + String _currentTabTitle(context) { + final loc = AppLocalizations.of(context); + switch (currentIndex) { + case 0: + return loc.home; + case 1: + return loc.matches; + case 2: + return loc.groups; + case 3: + return loc.statistics; + default: + return ''; + } + } +} diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart new file mode 100644 index 0000000..f92df0f --- /dev/null +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/player_selection.dart'; +import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; +import 'package:provider/provider.dart'; + +class CreateGroupView extends StatefulWidget { + const CreateGroupView({super.key}); + + @override + State createState() => _CreateGroupViewState(); +} + +class _CreateGroupViewState extends State { + late final AppDatabase db; + + /// Controller for the group name input field + final _groupNameController = TextEditingController(); + + /// List of currently selected players + List selectedPlayers = []; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + _groupNameController.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _groupNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(loc.create_new_group)), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + margin: CustomTheme.standardMargin, + child: TextInputField( + controller: _groupNameController, + hintText: loc.group_name, + ), + ), + Expanded( + child: PlayerSelection( + onChanged: (value) { + setState(() { + selectedPlayers = [...value]; + }); + }, + ), + ), + CustomWidthButton( + text: loc.create_group, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.primary, + onPressed: + (_groupNameController.text.isEmpty || + (selectedPlayers.length < 2)) + ? null + : () async { + bool success = await db.groupDao.addGroup( + group: Group( + name: _groupNameController.text.trim(), + members: selectedPlayers, + ), + ); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomTheme.boxColor, + content: Center( + child: Text( + AppLocalizations.of( + context, + ).error_creating_group, + style: const TextStyle(color: Colors.white), + ), + ), + ), + ); + } + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart new file mode 100644 index 0000000..57d05a4 --- /dev/null +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; +import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:provider/provider.dart'; + +class GroupsView extends StatefulWidget { + const GroupsView({super.key}); + + @override + State createState() => _GroupsViewState(); +} + +class _GroupsViewState extends State { + late final AppDatabase db; + + /// Loaded groups from the database + late List loadedGroups; + + /// Loading state + bool isLoading = true; + + List groups = List.filled( + 7, + Group( + name: 'Skeleton Group', + members: List.filled(6, Player(name: 'Skeleton Player')), + ), + ); + + @override + void initState() { + super.initState(); + + db = Provider.of(context, listen: false); + loadGroups(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + body: Stack( + alignment: Alignment.center, + children: [ + AppSkeleton( + enabled: isLoading, + child: Visibility( + visible: groups.isNotEmpty, + replacement: Center( + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_groups_created_yet, + ), + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: groups.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == groups.length) { + return SizedBox( + height: MediaQuery.paddingOf(context).bottom - 20, + ); + } + return GroupTile(group: groups[index]); + }, + ), + ), + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom, + child: CustomWidthButton( + text: loc.create_group, + sizeRelativeToWidth: 0.90, + onPressed: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const CreateGroupView(); + }, + ), + ); + setState(() { + loadGroups(); + }); + }, + ), + ), + ], + ), + ); + } + + void loadGroups() { + Future.wait([ + db.groupDao.getAllGroups(), + Future.delayed(Constants.minimumSkeletonDuration), + ]).then((results) { + loadedGroups = results[0] as List; + setState(() { + groups = loadedGroups + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + }); + if (mounted) { + setState(() { + isLoading = false; + }); + } + }); + } +} diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart new file mode 100644 index 0000000..170adb4 --- /dev/null +++ b/lib/presentation/views/main_menu/home_view.dart @@ -0,0 +1,261 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; +import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; +import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; +import 'package:game_tracker/presentation/widgets/tiles/match_summary_tile.dart'; +import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; +import 'package:provider/provider.dart'; + +class HomeView extends StatefulWidget { + const HomeView({super.key}); + + @override + State createState() => _HomeViewState(); +} + +class _HomeViewState extends State { + bool isLoading = true; + + /// Amount of matches in the database + int matchCount = 0; + + /// Amount of groups in the database + int groupCount = 0; + + /// Loaded recent matches from the database + List loadedRecentMatches = []; + + /// Recent matches to display, initially filled with skeleton matches + List recentMatches = List.filled( + 2, + Match( + name: 'Skeleton Match', + group: Group( + name: 'Skeleton Group', + members: [ + Player(name: 'Skeleton Player 1'), + Player(name: 'Skeleton Player 2'), + ], + ), + winner: Player(name: 'Skeleton Player 1'), + ), + ); + + @override + void initState() { + super.initState(); + loadHomeViewData(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return AppSkeleton( + fixLayoutBuilder: true, + enabled: isLoading, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: loc.matches, + icon: Icons.groups_rounded, + value: matchCount, + ), + SizedBox(width: constraints.maxWidth * 0.05), + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: loc.groups, + icon: Icons.groups_rounded, + value: groupCount, + ), + ], + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: InfoTile( + width: constraints.maxWidth * 0.95, + title: loc.recent_matches, + icon: Icons.timer, + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40.0), + child: Visibility( + visible: !isLoading && loadedRecentMatches.isNotEmpty, + replacement: Center( + heightFactor: 12, + child: Text( + AppLocalizations.of( + context, + ).no_recent_matches_available, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MatchSummaryTile( + matchTitle: recentMatches[0].name, + game: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText( + recentMatches[0], + context, + ), + winner: recentMatches[0].winner == null + ? AppLocalizations.of( + context, + ).match_in_progress + : recentMatches[0].winner!.name, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(), + ), + if (loadedRecentMatches.length > 1) ...[ + MatchSummaryTile( + matchTitle: recentMatches[1].name, + game: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText( + recentMatches[1], + context, + ), + winner: recentMatches[1].winner == null + ? AppLocalizations.of( + context, + ).match_in_progress + : recentMatches[1].winner!.name, + ), + const SizedBox(height: 8), + ] else ...[ + Center( + heightFactor: 5.35, + child: Text( + AppLocalizations.of( + context, + ).no_second_match_available, + ), + ), + ], + ], + ), + ), + ), + ), + ), + InfoTile( + width: constraints.maxWidth * 0.95, + title: loc.quick_create, + icon: Icons.add_box_rounded, + content: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + QuickCreateButton( + text: 'Category 1', + onPressed: () {}, + ), + QuickCreateButton( + text: 'Category 2', + onPressed: () {}, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + QuickCreateButton( + text: 'Category 3', + onPressed: () {}, + ), + QuickCreateButton( + text: 'Category 4', + onPressed: () {}, + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + QuickCreateButton( + text: 'Category 5', + onPressed: () {}, + ), + QuickCreateButton( + text: 'Category 6', + onPressed: () {}, + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + /// Loads the data for the HomeView from the database. + /// This includes the match count, group count, and recent matches. + void loadHomeViewData() { + final db = Provider.of(context, listen: false); + Future.wait([ + db.matchDao.getMatchCount(), + db.groupDao.getGroupCount(), + db.matchDao.getAllMatches(), + Future.delayed(Constants.minimumSkeletonDuration), + ]).then((results) { + matchCount = results[0] as int; + groupCount = results[1] as int; + loadedRecentMatches = results[2] as List; + recentMatches = + (loadedRecentMatches + ..sort((a, b) => b.createdAt.compareTo(a.createdAt))) + .take(2) + .toList(); + if (loadedRecentMatches.length < 2) { + recentMatches.add( + Match(name: 'Dummy Match', winner: null, group: null, players: null), + ); + } + if (mounted) { + setState(() { + isLoading = false; + }); + } + }); + } + + /// Generates a text representation of the players in the match. + /// If the match has a group, it returns the group name and the number of additional players. + /// If there is no group, it returns the count of players. + String _getPlayerText(Match game, context) { + final loc = AppLocalizations.of(context); + if (game.group == null) { + final playerCount = game.players?.length ?? 0; + return loc.players_count(playerCount); + } + if (game.players == null || game.players!.isEmpty) { + return game.group!.name; + } + return '${game.group!.name} + ${game.players!.length}'; + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart new file mode 100644 index 0000000..5976f72 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; +import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; + +class ChooseGameView extends StatefulWidget { + final List<(String, String, Ruleset)> games; + final int initialGameIndex; + + const ChooseGameView({ + super.key, + required this.games, + required this.initialGameIndex, + }); + + @override + State createState() => _ChooseGameViewState(); +} + +class _ChooseGameViewState extends State { + /// Controller for the search bar + final TextEditingController searchBarController = TextEditingController(); + + /// Currently selected game index + late int selectedGameIndex; + + @override + void initState() { + selectedGameIndex = widget.initialGameIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop(selectedGameIndex); + }, + ), + title: Text(loc.choose_game), + ), + body: PopScope( + // This fixes that the Android Back Gesture didn't return the + // selectedGameIndex and therefore the selected Game wasn't saved + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) { + return; + } + Navigator.of(context).pop(selectedGameIndex); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: CustomSearchBar( + controller: searchBarController, + hintText: loc.game_name, + ), + ), + const SizedBox(height: 5), + Expanded( + child: ListView.builder( + itemCount: widget.games.length, + itemBuilder: (BuildContext context, int index) { + return TitleDescriptionListTile( + title: widget.games[index].$1, + description: widget.games[index].$2, + badgeText: translateRulesetToString( + widget.games[index].$3, + context, + ), + isHighlighted: selectedGameIndex == index, + onPressed: () async { + setState(() { + if (selectedGameIndex == index) { + selectedGameIndex = -1; + } else { + selectedGameIndex = index; + } + }); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart new file mode 100644 index 0000000..97fbcef --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -0,0 +1,149 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; +import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; + +class ChooseGroupView extends StatefulWidget { + final List groups; + final String initialGroupId; + + const ChooseGroupView({ + super.key, + required this.groups, + required this.initialGroupId, + }); + + @override + State createState() => _ChooseGroupViewState(); +} + +class _ChooseGroupViewState extends State { + late String selectedGroupId; + final TextEditingController controller = TextEditingController(); + late final List filteredGroups; + + @override + void initState() { + selectedGroupId = widget.initialGroupId; + filteredGroups = [...widget.groups]; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop( + selectedGroupId == '' + ? null + : widget.groups.firstWhere( + (group) => group.id == selectedGroupId, + ), + ); + }, + ), + title: Text(loc.choose_group), + ), + body: PopScope( + // This fixes that the Android Back Gesture didn't return the + // selectedGroupId and therefore the selected Group wasn't saved + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) { + return; + } + Navigator.of(context).pop( + selectedGroupId == '' + ? null + : widget.groups.firstWhere( + (group) => group.id == selectedGroupId, + ), + ); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: CustomSearchBar( + controller: controller, + hintText: loc.search_for_groups, + onChanged: (value) { + setState(() { + filterGroups(value); + }); + }, + ), + ), + Expanded( + child: Visibility( + visible: filteredGroups.isNotEmpty, + replacement: Visibility( + visible: widget.groups.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_groups_created_yet, + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( + context, + ).there_is_no_group_matching_your_search, + ), + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: filteredGroups.length, + itemBuilder: (BuildContext context, int index) { + return GestureDetector( + onTap: () { + setState(() { + if (selectedGroupId != filteredGroups[index].id) { + selectedGroupId = filteredGroups[index].id; + } else { + selectedGroupId = ''; + } + }); + }, + child: GroupTile( + group: filteredGroups[index], + isHighlighted: + selectedGroupId == filteredGroups[index].id, + ), + ); + }, + ), + ), + ), + ], + ), + ), + ); + } + + /// Filters the groups based on the search [query]. + void filterGroups(String query) { + setState(() { + if (query.isEmpty) { + filteredGroups.clear(); + filteredGroups.addAll(widget.groups); + } else { + filteredGroups.clear(); + filteredGroups.addAll( + widget.groups.where( + (group) => group.name.toLowerCase().contains(query.toLowerCase()), + ), + ); + } + }); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart new file mode 100644 index 0000000..ca021af --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; + +class ChooseRulesetView extends StatefulWidget { + final List<(Ruleset, String)> rulesets; + final int initialRulesetIndex; + + const ChooseRulesetView({ + super.key, + required this.rulesets, + required this.initialRulesetIndex, + }); + + @override + State createState() => _ChooseRulesetViewState(); +} + +class _ChooseRulesetViewState extends State { + /// Currently selected ruleset index + late int selectedRulesetIndex; + + @override + void initState() { + selectedRulesetIndex = widget.initialRulesetIndex; + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return DefaultTabController( + length: 2, + initialIndex: 0, + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + Navigator.of(context).pop( + selectedRulesetIndex == -1 + ? null + : widget.rulesets[selectedRulesetIndex].$1, + ); + }, + ), + title: Text(loc.choose_ruleset), + ), + body: PopScope( + // This fixes that the Android Back Gesture didn't return the + // selectedRulesetIndex and therefore the selected Ruleset wasn't saved + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + if (didPop) { + return; + } + Navigator.of(context).pop( + selectedRulesetIndex == -1 + ? null + : widget.rulesets[selectedRulesetIndex].$1, + ); + }, + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: widget.rulesets.length, + itemBuilder: (BuildContext context, int index) { + return TitleDescriptionListTile( + onPressed: () async { + setState(() { + if (selectedRulesetIndex == index) { + selectedRulesetIndex = -1; + } else { + selectedRulesetIndex = index; + } + }); + }, + title: translateRulesetToString( + widget.rulesets[index].$1, + context, + ), + description: widget.rulesets[index].$2, + isHighlighted: selectedRulesetIndex == index, + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart new file mode 100644 index 0000000..dc6690b --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -0,0 +1,271 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/player_selection.dart'; +import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; +import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart'; +import 'package:provider/provider.dart'; + +class CreateMatchView extends StatefulWidget { + final VoidCallback? onWinnerChanged; + const CreateMatchView({super.key, this.onWinnerChanged}); + + @override + State createState() => _CreateMatchViewState(); +} + +class _CreateMatchViewState extends State { + late final AppDatabase db; + + /// Controller for the match name input field + final TextEditingController _matchNameController = TextEditingController(); + + /// Hint text for the match name input field + String? hintText; + + /// List of all groups from the database + List groupsList = []; + + /// List of all players from the database + List playerList = []; + + /// List of players filtered based on the selected group + /// If a group is selected, this list contains all players from [playerList] + /// who are not members of the selected group. If no group is selected, + /// this list is identical to [playerList]. + List filteredPlayerList = []; + + /// The currently selected group + Group? selectedGroup; + + /// The index of the currently selected group in [groupsList] to mark it in + /// the [ChooseGroupView] + String selectedGroupId = ''; + + /// The currently selected ruleset + Ruleset? selectedRuleset; + + /// The index of the currently selected ruleset in [rulesets] to mark it in + /// the [ChooseRulesetView] + int selectedRulesetIndex = -1; + + /// The index of the currently selected game in [games] to mark it in + /// the [ChooseGameView] + int selectedGameIndex = -1; + + /// The currently selected players + List? selectedPlayers; + + /// List of available rulesets with their localized string representations + late final List<(Ruleset, String)> _rulesets; + + @override + void initState() { + super.initState(); + _matchNameController.addListener(() { + setState(() {}); + }); + + db = Provider.of(context, listen: false); + + Future.wait([ + db.groupDao.getAllGroups(), + db.playerDao.getAllPlayers(), + ]).then((result) async { + groupsList = result[0] as List; + playerList = result[1] as List; + setState(() { + filteredPlayerList = List.from(playerList); + }); + }); + } + + @override + void dispose() { + _matchNameController.dispose(); + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final loc = AppLocalizations.of(context); + hintText ??= loc.match_name; + _rulesets = [ + (Ruleset.singleWinner, loc.ruleset_single_winner), + (Ruleset.singleLoser, loc.ruleset_single_loser), + (Ruleset.mostPoints, loc.ruleset_most_points), + (Ruleset.leastPoints, loc.ruleset_least_points), + ]; + } + + // TODO: Replace when games are implemented + List<(String, String, Ruleset)> games = [ + ('Example Game 1', 'This is a description', Ruleset.leastPoints), + ('Example Game 2', '', Ruleset.singleWinner), + ]; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(loc.create_new_match)), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + margin: CustomTheme.tileMargin, + child: TextInputField( + controller: _matchNameController, + hintText: hintText ?? '', + ), + ), + ChooseTile( + title: loc.game, + trailingText: selectedGameIndex == -1 + ? loc.none + : games[selectedGameIndex].$1, + onPressed: () async { + selectedGameIndex = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChooseGameView( + games: games, + initialGameIndex: selectedGameIndex, + ), + ), + ); + setState(() { + if (selectedGameIndex != -1) { + hintText = games[selectedGameIndex].$1; + selectedRuleset = games[selectedGameIndex].$3; + selectedRulesetIndex = _rulesets.indexWhere( + (r) => r.$1 == selectedRuleset, + ); + } else { + hintText = loc.match_name; + selectedRuleset = null; + } + }); + }, + ), + ChooseTile( + title: loc.ruleset, + trailingText: selectedRuleset == null + ? loc.none + : translateRulesetToString(selectedRuleset!, context), + onPressed: () async { + selectedRuleset = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChooseRulesetView( + rulesets: _rulesets, + initialRulesetIndex: selectedRulesetIndex, + ), + ), + ); + if (!mounted) return; + selectedRulesetIndex = _rulesets.indexWhere( + (r) => r.$1 == selectedRuleset, + ); + selectedGameIndex = -1; + setState(() {}); + }, + ), + ChooseTile( + title: loc.group, + trailingText: selectedGroup == null + ? loc.none_group + : selectedGroup!.name, + onPressed: () async { + selectedGroup = await Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => ChooseGroupView( + groups: groupsList, + initialGroupId: selectedGroupId, + ), + ), + ); + selectedGroupId = selectedGroup?.id ?? ''; + if (selectedGroup != null) { + filteredPlayerList = playerList + .where( + (p) => !selectedGroup!.members.any((m) => m.id == p.id), + ) + .toList(); + } else { + filteredPlayerList = List.from(playerList); + } + setState(() {}); + }, + ), + Expanded( + child: PlayerSelection( + key: ValueKey(selectedGroup?.id ?? 'no_group'), + initialSelectedPlayers: selectedPlayers ?? [], + availablePlayers: filteredPlayerList, + onChanged: (value) { + setState(() { + selectedPlayers = value; + }); + }, + ), + ), + CustomWidthButton( + text: loc.create_match, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.primary, + onPressed: _enableCreateGameButton() + ? () async { + Match match = Match( + name: _matchNameController.text.isEmpty + ? (hintText ?? '') + : _matchNameController.text.trim(), + createdAt: DateTime.now(), + group: selectedGroup, + players: selectedPlayers, + ); + await db.matchDao.addMatch(match: match); + if (context.mounted) { + Navigator.pushReplacement( + context, + CupertinoPageRoute( + fullscreenDialog: true, + builder: (context) => MatchResultView( + match: match, + onWinnerChanged: widget.onWinnerChanged, + ), + ), + ); + } + } + : null, + ), + ], + ), + ), + ); + } + + /// Determines whether the "Create Match" button should be enabled. + /// + /// Returns `true` if: + /// - A ruleset is selected AND + /// - Either a group is selected OR at least 2 players are selected + bool _enableCreateGameButton() { + return (selectedGroup != null || + (selectedPlayers != null && selectedPlayers!.length > 1)) && + selectedRuleset != null; + } +} diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart new file mode 100644 index 0000000..0d624f0 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -0,0 +1,161 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart'; +import 'package:provider/provider.dart'; + +class MatchResultView extends StatefulWidget { + final Match match; + + final VoidCallback? onWinnerChanged; + + const MatchResultView({super.key, required this.match, this.onWinnerChanged}); + @override + State createState() => _MatchResultViewState(); +} + +class _MatchResultViewState extends State { + late final AppDatabase db; + + /// List of all players who participated in the match + late final List allPlayers; + + /// Currently selected winner player + Player? _selectedPlayer; + + @override + void initState() { + db = Provider.of(context, listen: false); + allPlayers = getAllPlayers(widget.match); + if (widget.match.winner != null) { + _selectedPlayer = allPlayers.firstWhere( + (p) => p.id == widget.match.winner!.id, + ); + } + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(); + }, + ), + title: Text(widget.match.name), + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + loc.select_winner, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + Expanded( + child: RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { + setState(() { + _selectedPlayer = value; + }); + await _handleWinnerSaving(); + }, + child: ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + text: allPlayers[index].name, + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + await _handleWinnerSaving(); + }, + ); + }, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + /// Handles saving or removing the winner in the database + /// based on the current selection. + Future _handleWinnerSaving() async { + if (_selectedPlayer == null) { + await db.matchDao.removeWinner(matchId: widget.match.id); + } else { + await db.matchDao.setWinner( + matchId: widget.match.id, + winnerId: _selectedPlayer!.id, + ); + } + widget.onWinnerChanged?.call(); + } + + /// Retrieves all players associated with the given [match]. + /// This includes players directly assigned to the match + /// as well as members of the group (if any). + /// The returned list is sorted alphabetically by player name. + List getAllPlayers(Match match) { + List players = []; + + if (match.group == null && match.players != null) { + players = [...match.players!]; + } else if (match.group != null && match.players != null) { + players = [...match.players!, ...match.group!.members]; + } else { + players = [...match.group!.members]; + } + + players.sort((a, b) => a.name.compareTo(b.name)); + return players; + } +} diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart new file mode 100644 index 0000000..45b957f --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -0,0 +1,137 @@ +import 'dart:core' hide Match; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:provider/provider.dart'; + +class MatchView extends StatefulWidget { + const MatchView({super.key}); + + @override + State createState() => _MatchViewState(); +} + +class _MatchViewState extends State { + late final AppDatabase db; + bool isLoading = true; + + /// Loaded matches from the database, + /// initially filled with skeleton matches + List matches = List.filled( + 4, + Match( + name: 'Skeleton match name', + group: Group( + name: 'Group name', + members: List.filled(5, Player(name: 'Player')), + ), + winner: Player(name: 'Player'), + players: [Player(name: 'Player')], + ), + ); + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + loadGames(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + body: Stack( + alignment: Alignment.center, + children: [ + AppSkeleton( + enabled: isLoading, + child: Visibility( + visible: matches.isNotEmpty, + replacement: Center( + child: TopCenteredMessage( + icon: Icons.report, + title: loc.info, + message: loc.no_matches_created_yet, + ), + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: matches.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == matches.length) { + return SizedBox( + height: MediaQuery.paddingOf(context).bottom - 20, + ); + } + return MatchTile( + onTap: () async { + Navigator.push( + context, + CupertinoPageRoute( + fullscreenDialog: true, + builder: (context) => MatchResultView( + match: matches[index], + onWinnerChanged: loadGames, + ), + ), + ); + }, + match: matches[index], + ); + }, + ), + ), + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom, + child: CustomWidthButton( + text: loc.create_match, + sizeRelativeToWidth: 0.90, + onPressed: () async { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + CreateMatchView(onWinnerChanged: loadGames), + ), + ); + }, + ), + ), + ], + ), + ); + } + + /// Loads the games from the database and sorts them by creation date. + void loadGames() { + Future.wait([ + db.matchDao.getAllMatches(), + Future.delayed(Constants.minimumSkeletonDuration), + ]).then((results) { + if (mounted) { + setState(() { + final loadedMatches = results[0] as List; + matches = loadedMatches + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + isLoading = false; + }); + } + }); + } +} diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart new file mode 100644 index 0000000..374c463 --- /dev/null +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; +import 'package:game_tracker/services/data_transfer_service.dart'; + +class SettingsView extends StatefulWidget { + const SettingsView({super.key}); + + @override + State createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Scaffold( + appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), + backgroundColor: CustomTheme.backgroundColor, + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => + SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), + child: Text( + textAlign: TextAlign.start, + loc.menu, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + ), + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 10, + ), + child: Text( + textAlign: TextAlign.start, + loc.settings, + style: const TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + SettingsListTile( + title: loc.export_data, + icon: Icons.upload_rounded, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () async { + final String json = + await DataTransferService.getAppDataAsJson(context); + final result = await DataTransferService.exportData( + json, + 'game_tracker-data', + ); + if (!context.mounted) return; + showExportSnackBar(context: context, result: result); + }, + ), + SettingsListTile( + title: loc.import_data, + icon: Icons.download_rounded, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () async { + final result = await DataTransferService.importData( + context, + ); + if (!context.mounted) return; + showImportSnackBar(context: context, result: result); + }, + ), + SettingsListTile( + title: loc.delete_all_data, + icon: Icons.delete_rounded, + suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(loc.delete_all_data), + content: Text(loc.this_cannot_be_undone), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(loc.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(loc.delete), + ), + ], + ), + ).then((confirmed) { + if (confirmed == true && context.mounted) { + DataTransferService.deleteAllData(context); + showSnackbar( + context: context, + message: AppLocalizations.of( + context, + ).data_successfully_deleted, + ); + } + }); + }, + ), + ], + ), + ), + ), + ); + } + + /// Displays a snackbar based on the import result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the import operation. + void showImportSnackBar({ + required BuildContext context, + required ImportResult result, + }) { + final loc = AppLocalizations.of(context); + switch (result) { + case ImportResult.success: + showSnackbar(context: context, message: loc.data_successfully_imported); + case ImportResult.invalidSchema: + showSnackbar(context: context, message: loc.invalid_schema); + case ImportResult.fileReadError: + showSnackbar(context: context, message: loc.error_reading_file); + case ImportResult.canceled: + showSnackbar(context: context, message: loc.import_canceled); + case ImportResult.formatException: + showSnackbar(context: context, message: loc.format_exception); + case ImportResult.unknownException: + showSnackbar(context: context, message: loc.unknown_exception); + } + } + + /// Displays a snackbar based on the export result. + /// + /// [context] The BuildContext to show the snackbar in. + /// [result] The result of the export operation. + void showExportSnackBar({ + required BuildContext context, + required ExportResult result, + }) { + final loc = AppLocalizations.of(context); + switch (result) { + case ExportResult.success: + showSnackbar(context: context, message: loc.data_successfully_exported); + case ExportResult.canceled: + showSnackbar(context: context, message: loc.export_canceled); + case ExportResult.unknownException: + showSnackbar(context: context, message: loc.unknown_exception); + } + } + + /// Displays a snackbar with the given message and optional action. + /// + /// [context] The BuildContext to show the snackbar in. + /// [message] The message to display in the snackbar. + /// [duration] The duration for which the snackbar is displayed. + /// [action] An optional callback function to execute when the action button is pressed. + void showSnackbar({ + required BuildContext context, + required String message, + Duration duration = const Duration(seconds: 3), + VoidCallback? action, + }) { + final loc = AppLocalizations.of(context); + final messenger = ScaffoldMessenger.of(context); + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.onBoxColor, + duration: duration, + action: action != null + ? SnackBarAction(label: loc.undo, onPressed: action) + : null, + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart new file mode 100644 index 0000000..53569ad --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -0,0 +1,278 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; +import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:provider/provider.dart'; + +class StatisticsView extends StatefulWidget { + const StatisticsView({super.key}); + + @override + State createState() => _StatisticsViewState(); +} + +class _StatisticsViewState extends State { + List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1)); + List<(String, int)> matchCounts = List.filled(6, ('Skeleton Player', 1)); + List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1)); + bool isLoading = true; + + @override + void initState() { + super.initState(); + loadStatisticData(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return SingleChildScrollView( + child: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: constraints.maxWidth), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: constraints.maxHeight * 0.01), + Visibility( + visible: + winCounts.isEmpty && + matchCounts.isEmpty && + winRates.isEmpty, + replacement: Column( + children: [ + StatisticsTile( + icon: Icons.sports_score, + title: loc.wins, + width: constraints.maxWidth * 0.95, + values: winCounts, + itemCount: 3, + barColor: Colors.green, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.percent, + title: loc.winrate, + width: constraints.maxWidth * 0.95, + values: winRates, + itemCount: 5, + barColor: Colors.orange[700]!, + ), + SizedBox(height: constraints.maxHeight * 0.02), + StatisticsTile( + icon: Icons.casino, + title: loc.amount_of_matches, + width: constraints.maxWidth * 0.95, + values: matchCounts, + itemCount: 10, + barColor: Colors.blue, + ), + ], + ), + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: AppLocalizations.of( + context, + ).no_statistics_available, + ), + ), + SizedBox(height: MediaQuery.paddingOf(context).bottom), + ], + ), + ), + ), + ); + }, + ); + } + + /// Loads matches and players from the database + /// and calculates statistics for each player + void loadStatisticData() { + final db = Provider.of(context, listen: false); + + Future.wait([ + db.matchDao.getAllMatches(), + db.playerDao.getAllPlayers(), + Future.delayed(Constants.minimumSkeletonDuration), + ]).then((results) async { + if (!mounted) return; + final matches = results[0] as List; + final players = results[1] as List; + winCounts = _calculateWinsForAllPlayers( + matches: matches, + players: players, + context: context, + ); + matchCounts = _calculateMatchAmountsForAllPlayers( + matches: matches, + players: players, + context: context, + ); + winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts); + setState(() { + isLoading = false; + }); + }); + } + + /// Calculates the number of wins for each player + /// and returns a sorted list of tuples (playerName, winCount) + List<(String, int)> _calculateWinsForAllPlayers({ + required List matches, + required List players, + required BuildContext context, + }) { + List<(String, int)> winCounts = []; + final loc = AppLocalizations.of(context); + + // Getting the winners + for (var match in matches) { + final winner = match.winner; + if (winner != null) { + final index = winCounts.indexWhere((entry) => entry.$1 == winner.id); + // -1 means winner not found in winCounts + if (index != -1) { + final current = winCounts[index].$2; + winCounts[index] = (winner.id, current + 1); + } else { + winCounts.add((winner.id, 1)); + } + } + } + + // Adding all players with zero wins + for (var player in players) { + final index = winCounts.indexWhere((entry) => entry.$1 == player.id); + // -1 means player not found in winCounts + if (index == -1) { + winCounts.add((player.id, 0)); + } + } + + // Replace player IDs with names + for (int i = 0; i < winCounts.length; i++) { + final playerId = winCounts[i].$1; + final player = players.firstWhere( + (p) => p.id == playerId, + orElse: () => Player(id: playerId, name: loc.not_available), + ); + winCounts[i] = (player.name, winCounts[i].$2); + } + + winCounts.sort((a, b) => b.$2.compareTo(a.$2)); + + return winCounts; + } + + /// Calculates the number of matches played for each player + /// and returns a sorted list of tuples (playerName, matchCount) + List<(String, int)> _calculateMatchAmountsForAllPlayers({ + required List matches, + required List players, + required BuildContext context, + }) { + List<(String, int)> matchCounts = []; + final loc = AppLocalizations.of(context); + + // Counting matches for each player + for (var match in matches) { + if (match.group != null) { + final members = match.group!.members.map((p) => p.id).toList(); + for (var playerId in members) { + final index = matchCounts.indexWhere((entry) => entry.$1 == playerId); + // -1 means player not found in matchCounts + if (index != -1) { + final current = matchCounts[index].$2; + matchCounts[index] = (playerId, current + 1); + } else { + matchCounts.add((playerId, 1)); + } + } + } + if (match.players != null) { + final members = match.players!.map((p) => p.id).toList(); + for (var playerId in members) { + final index = matchCounts.indexWhere((entry) => entry.$1 == playerId); + // -1 means player not found in matchCounts + if (index != -1) { + final current = matchCounts[index].$2; + matchCounts[index] = (playerId, current + 1); + } else { + matchCounts.add((playerId, 1)); + } + } + } + } + + // Adding all players with zero matches + for (var player in players) { + final index = matchCounts.indexWhere((entry) => entry.$1 == player.id); + // -1 means player not found in matchCounts + if (index == -1) { + matchCounts.add((player.id, 0)); + } + } + + // Replace player IDs with names + for (int i = 0; i < matchCounts.length; i++) { + final playerId = matchCounts[i].$1; + final player = players.firstWhere( + (p) => p.id == playerId, + orElse: () => Player(id: playerId, name: loc.not_available), + ); + matchCounts[i] = (player.name, matchCounts[i].$2); + } + + matchCounts.sort((a, b) => b.$2.compareTo(a.$2)); + + return matchCounts; + } + + // dart + List<(String, double)> computeWinRatePercent({ + required List<(String, int)> wins, + required List<(String, int)> matches, + }) { + final Map winsMap = {for (var e in wins) e.$1: e.$2}; + final Map matchesMap = {for (var e in matches) e.$1: e.$2}; + + // Get all unique player names + final names = {...winsMap.keys, ...matchesMap.keys}; + + // Calculate win rates + final result = names.map((name) { + final int w = winsMap[name] ?? 0; + final int g = matchesMap[name] ?? 0; + // Calculate percentage and round to 2 decimal places + // Avoid division by zero + final double percent = (g > 0) + ? double.parse(((w / g)).toStringAsFixed(2)) + : 0; + return (name, percent); + }).toList(); + + // Sort the result: first by winrate descending, + // then by wins descending in case of a tie + result.sort((a, b) { + final cmp = b.$2.compareTo(a.$2); + if (cmp != 0) return cmp; + final wa = winsMap[a.$1] ?? 0; + final wb = winsMap[b.$1] ?? 0; + return wb.compareTo(wa); + }); + + return result; + } +} diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart new file mode 100644 index 0000000..1d74456 --- /dev/null +++ b/lib/presentation/widgets/app_skeleton.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +/// A widget that provides a skeleton loading effect to its child widget tree. +/// - [child]: The widget tree to apply the skeleton effect to. +/// - [enabled]: A boolean to enable or disable the skeleton effect. +/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. +class AppSkeleton extends StatefulWidget { + const AppSkeleton({ + super.key, + required this.child, + this.enabled = true, + this.fixLayoutBuilder = false, + }); + + /// The widget tree to apply the skeleton effect to. + final Widget child; + + /// A boolean to enable or disable the skeleton effect. + final bool enabled; + + /// A boolean to fix the layout builder for AnimatedSwitcher. + final bool fixLayoutBuilder; + + @override + State createState() => _AppSkeletonState(); +} + +class _AppSkeletonState extends State { + @override + Widget build(BuildContext context) { + return Skeletonizer( + effect: PulseEffect( + from: Colors.grey[800]!, + to: Colors.grey[600]!, + duration: const Duration(milliseconds: 800), + ), + enabled: widget.enabled, + enableSwitchAnimation: true, + switchAnimationConfig: SwitchAnimationConfig( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, + layoutBuilder: !widget.fixLayoutBuilder + ? AnimatedSwitcher.defaultLayoutBuilder + : (Widget? currentChild, List previousChildren) { + return Stack( + alignment: Alignment.topCenter, + children: [ + ...previousChildren, + if (currentChild != null) currentChild, + ], + ); + }, + ), + child: widget.child, + ); + } +} diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart new file mode 100644 index 0000000..7e52648 --- /dev/null +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -0,0 +1,133 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; + +/// A custom button widget that is designed to have a width relative to the screen size. +/// It supports three types of buttons: primary, secondary, and text buttons. +/// - [text]: The text to display on the button. +/// - [buttonType]: The type of button to display. Defaults to [ButtonType.primary]. +/// - [sizeRelativeToWidth]: The size of the button relative to the width of the screen. +/// - [onPressed]: The callback to be invoked when the button is pressed. +class CustomWidthButton extends StatelessWidget { + const CustomWidthButton({ + super.key, + required this.text, + this.buttonType = ButtonType.primary, + required this.sizeRelativeToWidth, + this.onPressed, + }); + + /// The text to display on the button. + final String text; + + /// The size of the button relative to the width of the screen. + final double sizeRelativeToWidth; + + /// The callback to be invoked when the button is pressed. + final VoidCallback? onPressed; + + /// The type of button to display. Depends on the enum [ButtonType]. + final ButtonType buttonType; + + @override + Widget build(BuildContext context) { + final Color buttonBackgroundColor; + final Color disabledBackgroundColor; + final Color borderSideColor; + final Color textcolor; + final Color disabledTextColor; + + if (buttonType == ButtonType.primary) { + textcolor = Colors.white; + disabledTextColor = Color.lerp(textcolor, Colors.black, 0.5)!; + buttonBackgroundColor = CustomTheme.primaryColor; + disabledBackgroundColor = Color.lerp( + buttonBackgroundColor, + Colors.black, + 0.5, + )!; + + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + foregroundColor: textcolor, + disabledForegroundColor: disabledTextColor, + backgroundColor: buttonBackgroundColor, + disabledBackgroundColor: disabledBackgroundColor, + animationDuration: const Duration(), + minimumSize: Size( + MediaQuery.sizeOf(context).width * sizeRelativeToWidth, + 60, + ), + shape: RoundedRectangleBorder( + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + ), + child: Text( + text, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22), + ), + ); + } else if (buttonType == ButtonType.secondary) { + textcolor = CustomTheme.primaryColor; + disabledTextColor = Color.lerp(textcolor, Colors.black, 0.5)!; + buttonBackgroundColor = Colors.transparent; + disabledBackgroundColor = Colors.transparent; + borderSideColor = onPressed != null + ? CustomTheme.primaryColor + : Color.lerp(CustomTheme.primaryColor, Colors.black, 0.5)!; + + return OutlinedButton( + onPressed: onPressed, + style: OutlinedButton.styleFrom( + foregroundColor: textcolor, + disabledForegroundColor: disabledTextColor, + backgroundColor: buttonBackgroundColor, + disabledBackgroundColor: disabledBackgroundColor, + animationDuration: const Duration(), + minimumSize: Size( + MediaQuery.sizeOf(context).width * sizeRelativeToWidth, + 60, + ), + side: BorderSide(color: borderSideColor, width: 2), + shape: RoundedRectangleBorder( + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + ), + child: Text( + text, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22), + ), + ); + } else { + textcolor = CustomTheme.primaryColor; + disabledTextColor = Color.lerp( + CustomTheme.primaryColor, + Colors.black, + 0.5, + )!; + buttonBackgroundColor = Colors.transparent; + disabledBackgroundColor = Colors.transparent; + + return TextButton( + onPressed: onPressed, + style: TextButton.styleFrom( + foregroundColor: textcolor, + disabledForegroundColor: disabledTextColor, + backgroundColor: buttonBackgroundColor, + disabledBackgroundColor: disabledBackgroundColor, + animationDuration: const Duration(), + minimumSize: Size( + MediaQuery.sizeOf(context).width * sizeRelativeToWidth, + 60, + ), + side: const BorderSide(style: BorderStyle.none), + ), + child: Text( + text, + style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 22), + ), + ); + } + } +} diff --git a/lib/presentation/widgets/buttons/quick_create_button.dart b/lib/presentation/widgets/buttons/quick_create_button.dart new file mode 100644 index 0000000..40ebeab --- /dev/null +++ b/lib/presentation/widgets/buttons/quick_create_button.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A button widget designed for quick creating matches in the [HomeView] +/// - [text]: The text to display on the button. +/// - [onPressed]: The callback to be invoked when the button is pressed. +class QuickCreateButton extends StatefulWidget { + const QuickCreateButton({ + super.key, + required this.text, + required this.onPressed, + }); + + /// The text to display on the button. + final String text; + + /// The callback to be invoked when the button is pressed. + final VoidCallback? onPressed; + + @override + State createState() => _QuickCreateButtonState(); +} + +class _QuickCreateButtonState extends State { + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: widget.onPressed, + style: ElevatedButton.styleFrom( + minimumSize: const Size(140, 45), + backgroundColor: CustomTheme.primaryColor, + shape: RoundedRectangleBorder( + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + ), + child: Text( + widget.text, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + ); + } +} diff --git a/lib/presentation/widgets/navbar_item.dart b/lib/presentation/widgets/navbar_item.dart new file mode 100644 index 0000000..13a8d4d --- /dev/null +++ b/lib/presentation/widgets/navbar_item.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +/// A navigation bar item widget that represents a single tab in a navigation bar. +/// - [index]: The index of the tab. +/// - [isSelected]: A boolean indicating whether the tab is currently selected. +/// - [icon]: The icon to display for the tab. +/// - [label]: The label to display for the tab. +/// - [onTabTapped]: The callback to be invoked when the tab is tapped. +class NavbarItem extends StatefulWidget { + const NavbarItem({ + super.key, + required this.index, + required this.isSelected, + required this.icon, + required this.label, + required this.onTabTapped, + }); + + /// The index of the tab. + final int index; + + /// A boolean indicating whether the tab is currently selected. + final bool isSelected; + + /// The icon to display for the tab. + final IconData icon; + + /// The label to display for the tab. + final String label; + + /// The callback to be invoked when the tab is tapped. + final Function(int) onTabTapped; + + @override + State createState() => _NavbarItemState(); +} + +class _NavbarItemState extends State { + @override + Widget build(BuildContext context) { + return Expanded( + child: GestureDetector( + onTap: () => widget.onTabTapped(widget.index), + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + widget.icon, + color: widget.isSelected ? Colors.white : Colors.black, + ), + const SizedBox(height: 4), + Text( + widget.label, + style: TextStyle( + color: widget.isSelected ? Colors.white : Colors.black, + fontSize: 12, + fontWeight: widget.isSelected + ? FontWeight.bold + : FontWeight.normal, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart new file mode 100644 index 0000000..9280ae0 --- /dev/null +++ b/lib/presentation/widgets/player_selection.dart @@ -0,0 +1,324 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; +import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart'; +import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart'; +import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:provider/provider.dart'; + +/// A widget that allows users to select players from a list, +/// with search functionality and the ability to add new players. +/// - [availablePlayers]: An optional list of players to choose from. If null, all +/// players from the database are used. +/// - [initialSelectedPlayers]: An optional list of players that should be pre-selected. +/// - [onChanged]: A callback function that is invoked whenever the selection changes, +/// providing the updated list of selected players. +class PlayerSelection extends StatefulWidget { + const PlayerSelection({ + super.key, + this.availablePlayers, + this.initialSelectedPlayers, + required this.onChanged, + }); + + /// An optional list of players to choose from. If null, all players from the database are used. + final List? availablePlayers; + + /// An optional list of players that should be pre-selected. + final List? initialSelectedPlayers; + + /// A callback function that is invoked whenever the selection changes, + final Function(List value) onChanged; + + @override + State createState() => _PlayerSelectionState(); +} + +class _PlayerSelectionState extends State { + late final AppDatabase db; + bool isLoading = true; + + /// Future that loads all players from the database. + late Future> _allPlayersFuture; + + /// The complete list of all available players. + List allPlayers = []; + + /// The list of players suggested based on the search input. + List suggestedPlayers = []; + + /// The list of currently selected players. + List selectedPlayers = []; + + /// Controller for the search bar input. + late final TextEditingController _searchBarController = + TextEditingController(); + + /// Skeleton data used while loading players. + late final List skeletonData = List.filled( + 7, + Player(name: 'Player 0'), + ); + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + suggestedPlayers = skeletonData; + loadPlayerList(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return Container( + margin: CustomTheme.standardMargin, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + decoration: CustomTheme.standardBoxDecoration, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomSearchBar( + controller: _searchBarController, + constraints: const BoxConstraints(maxHeight: 45, minHeight: 45), + hintText: loc.search_for_players, + trailingButtonShown: true, + trailingButtonicon: Icons.add_circle, + trailingButtonEnabled: _searchBarController.text.trim().isNotEmpty, + onTrailingButtonPressed: () async { + addNewPlayerFromSearch(context: context); + }, + onChanged: (value) { + setState(() { + // Filters the list of suggested players based on the search input. + if (value.isEmpty) { + // If the search is empty, it shows all unselected players. + suggestedPlayers = allPlayers.where((player) { + return !selectedPlayers.contains(player); + }).toList(); + } else { + // If there is input, it filters by name match (case-insensitive) and ensures + // that already selected players are excluded from the results. + suggestedPlayers = allPlayers.where((player) { + final bool nameMatches = player.name.toLowerCase().contains( + value.toLowerCase(), + ); + final bool isNotSelected = !selectedPlayers.contains( + player, + ); + return nameMatches && isNotSelected; + }).toList(); + } + }); + }, + ), + const SizedBox(height: 10), + Text( + loc.selected_players, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + SizedBox( + height: 50, + child: selectedPlayers.isEmpty + ? Center(child: Text(loc.no_players_selected)) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var player in selectedPlayers) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextIconTile( + text: player.name, + onIconTap: () { + setState(() { + // Removes the player from the selection and notifies the parent. + selectedPlayers.remove(player); + widget.onChanged([...selectedPlayers]); + + // Get the current search query + final currentSearch = _searchBarController + .text + .toLowerCase(); + + // If the player matches the current search query (or search is empty), + // they are added back to the `suggestedPlayers` and the list is re-sorted. + if (currentSearch.isEmpty || + player.name.toLowerCase().contains( + currentSearch, + )) { + suggestedPlayers.add(player); + suggestedPlayers.sort( + (a, b) => a.name.compareTo(b.name), + ); + } + }); + }, + ), + ), + ], + ), + ), + ), + const SizedBox(height: 10), + Text( + loc.all_players, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + Expanded( + child: AppSkeleton( + enabled: isLoading, + child: Visibility( + visible: suggestedPlayers.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: _getInfoText(context), + ), + child: ListView.builder( + itemCount: suggestedPlayers.length, + itemBuilder: (BuildContext context, int index) { + return TextIconListTile( + text: suggestedPlayers[index].name, + onPressed: () { + setState(() { + // If the player is not already selected + if (!selectedPlayers.contains( + suggestedPlayers[index], + )) { + // Add to player to the front of the selectedPlayers + selectedPlayers.insert(0, suggestedPlayers[index]); + // Notify the parent widget of the change + widget.onChanged([...selectedPlayers]); + // Remove the player from the suggestedPlayers + suggestedPlayers.remove(suggestedPlayers[index]); + } + }); + }, + ); + }, + ), + ), + ), + ), + ], + ), + ); + } + + /// Loads the list of players from the database or uses the provided available players. + /// Sets the loading state and updates the player lists accordingly. + void loadPlayerList() { + _allPlayersFuture = Future.wait([ + db.playerDao.getAllPlayers(), + Future.delayed(Constants.minimumSkeletonDuration), + ]).then((results) => results[0] as List); + if (mounted) { + _allPlayersFuture.then((loadedPlayers) { + setState(() { + // If a list of available players is provided (even if empty), use that list. + if (widget.availablePlayers != null) { + widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name)); + allPlayers = [...widget.availablePlayers!]; + suggestedPlayers = [...allPlayers]; + + if (widget.initialSelectedPlayers != null) { + // Ensures that only players available for selection are pre-selected. + selectedPlayers = widget.initialSelectedPlayers! + .where( + (p) => widget.availablePlayers!.any( + (available) => available.id == p.id, + ), + ) + .toList(); + } + } else { + // Otherwise, use the loaded players from the database. + loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); + allPlayers = [...loadedPlayers]; + suggestedPlayers = [...loadedPlayers]; + } + isLoading = false; + }); + }); + } + } + + /// Adds a new player to the database from the search bar input. + /// Shows a snackbar indicating success or failure. + /// [context] - BuildContext to show the snackbar. + Future addNewPlayerFromSearch({required BuildContext context}) async { + final loc = AppLocalizations.of(context); + final playerName = _searchBarController.text.trim(); + + final createdPlayer = Player(name: playerName); + final success = await db.playerDao.addPlayer(player: createdPlayer); + + if (!context.mounted) return; + + if (success) { + _handleSuccessfulPlayerCreation(createdPlayer); + showSnackBarMessage(loc.successfully_added_player(playerName)); + } else { + showSnackBarMessage(loc.could_not_add_player(playerName)); + } + } + + /// Updates the state after successfully adding a new player. + void _handleSuccessfulPlayerCreation(Player player) { + selectedPlayers.insert(0, player); + widget.onChanged([...selectedPlayers]); + allPlayers.add(player); + + setState(() { + _searchBarController.clear(); + _updateSuggestedPlayers(); + }); + } + + /// Updates the suggested players list based on current selection. + void _updateSuggestedPlayers() { + suggestedPlayers = allPlayers + .where((player) => !selectedPlayers.contains(player)) + .toList(); + } + + /// Displays a snackbar message at the bottom of the screen. + /// [message] - The message to display in the snackbar. + void showSnackBarMessage(String message) { + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + backgroundColor: CustomTheme.boxColor, + content: Center( + child: Text(message, style: const TextStyle(color: Colors.white)), + ), + ), + ); + } + + /// Determines the appropriate info text to display when no players + /// are available in the suggested players list. + String _getInfoText(BuildContext context) { + final loc = AppLocalizations.of(context); + if (allPlayers.isEmpty) { + // No players exist in the database + return loc.no_players_created_yet; + } else if (selectedPlayers.length == allPlayers.length || + widget.availablePlayers?.isEmpty == true) { + // All players have been selected or + // available players list is provided but empty + return loc.all_players_selected; + } else { + // No players match the search query + return loc.no_players_found_with_that_name; + } + } +} diff --git a/lib/presentation/widgets/text_input/custom_search_bar.dart b/lib/presentation/widgets/text_input/custom_search_bar.dart new file mode 100644 index 0000000..bf7971a --- /dev/null +++ b/lib/presentation/widgets/text_input/custom_search_bar.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A custom search bar widget that encapsulates a [SearchBar] with additional customization options. +/// - [controller]: The controller for the search bar's text input. +/// - [hintText]: The hint text displayed in the search bar when it is empty. +/// - [trailingButtonShown]: Whether to show the trailing button. +/// - [trailingButtonicon]: The icon for the trailing button. +/// - [trailingButtonEnabled]: Whether the trailing button is in enabled state. +/// - [onTrailingButtonPressed]: The callback invoked when the trailing button is pressed. +/// - [onChanged]: The callback invoked when the text in the search bar changes. +/// - [constraints]: The constraints for the search bar. +class CustomSearchBar extends StatelessWidget { + const CustomSearchBar({ + super.key, + required this.controller, + required this.hintText, + this.trailingButtonShown = false, + this.trailingButtonicon = Icons.clear, + this.trailingButtonEnabled = true, + this.onTrailingButtonPressed, + this.onChanged, + this.constraints, + }); + + /// The controller for the search bar's text input. + final TextEditingController controller; + + /// The hint text displayed in the search bar when it is empty. + final String hintText; + + /// Whether to show the trailing button. + final bool trailingButtonShown; + + /// The icon for the trailing button. + final IconData trailingButtonicon; + + /// Whether the trailing button is in enabled state. + final bool trailingButtonEnabled; + + /// The callback invoked when the trailing button is pressed. + final VoidCallback? onTrailingButtonPressed; + + /// The callback invoked when the text in the search bar changes. + final ValueChanged? onChanged; + + /// The constraints for the search bar. + final BoxConstraints? constraints; + + @override + Widget build(BuildContext context) { + return SearchBar( + controller: controller, + constraints: + constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45), + hintText: hintText, + onChanged: onChanged, + hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)), + leading: const Icon(Icons.search), + trailing: [ + Visibility( + visible: trailingButtonShown, + child: GestureDetector( + onTap: trailingButtonEnabled ? onTrailingButtonPressed : null, + child: Icon( + trailingButtonicon, + color: trailingButtonEnabled + ? null + : Colors.grey.withValues(alpha: 0.2), + ), + ), + ), + const SizedBox(width: 5), + ], + backgroundColor: WidgetStateProperty.all(CustomTheme.boxColor), + side: WidgetStateProperty.all(BorderSide(color: CustomTheme.boxBorder)), + shape: WidgetStateProperty.all( + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + elevation: WidgetStateProperty.all(0), + ); + } +} diff --git a/lib/presentation/widgets/text_input/text_input_field.dart b/lib/presentation/widgets/text_input/text_input_field.dart new file mode 100644 index 0000000..a409c68 --- /dev/null +++ b/lib/presentation/widgets/text_input/text_input_field.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A custom text input field widget that encapsulates a [TextField] with specific styling. +/// - [controller]: The controller for the text input field. +/// - [onChanged]: The callback invoked when the text in the field changes. +/// - [hintText]: The hint text displayed in the text input field when it is empty +class TextInputField extends StatelessWidget { + const TextInputField({ + super.key, + required this.controller, + required this.hintText, + this.onChanged, + }); + + /// The controller for the text input field. + final TextEditingController controller; + + /// The callback invoked when the text in the field changes. + final ValueChanged? onChanged; + + /// The hint text displayed in the text input field when it is empty. + final String hintText; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + onChanged: onChanged, + decoration: InputDecoration( + filled: true, + fillColor: CustomTheme.boxColor, + hintText: hintText, + hintStyle: const TextStyle(fontSize: 18), + enabledBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + borderSide: BorderSide(color: CustomTheme.boxBorder), + ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + borderSide: BorderSide(color: CustomTheme.boxBorder), + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/choose_tile.dart b/lib/presentation/widgets/tiles/choose_tile.dart new file mode 100644 index 0000000..f6ec940 --- /dev/null +++ b/lib/presentation/widgets/tiles/choose_tile.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A tile widget that allows users to choose an option by tapping on it. +/// - [title]: The title text displayed on the tile. +/// - [trailingText]: Optional trailing text displayed on the tile. +/// - [onPressed]: The callback invoked when the tile is tapped. +class ChooseTile extends StatefulWidget { + const ChooseTile({ + super.key, + required this.title, + this.trailingText, + this.onPressed, + }); + + /// The title text displayed on the tile. + final String title; + + /// The callback invoked when the tile is tapped. + final VoidCallback? onPressed; + + /// Optional trailing text displayed on the tile. + final String? trailingText; + + @override + State createState() => _ChooseTileState(); +} + +class _ChooseTileState extends State { + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onPressed, + child: Container( + margin: CustomTheme.tileMargin, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: CustomTheme.standardBoxDecoration, + child: Row( + children: [ + Text( + widget.title, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const Spacer(), + if (widget.trailingText != null) Text(widget.trailingText!), + const SizedBox(width: 10), + const Icon(Icons.arrow_forward_ios, size: 16), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart new file mode 100644 index 0000000..706aabb --- /dev/null +++ b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A custom radio list tile widget that encapsulates a [Radio] button with additional styling and functionality. +/// - [text]: The text to display next to the radio button. +/// - [value]: The value associated with the radio button. +/// - [onContainerTap]: The callback invoked when the container is tapped. +class CustomRadioListTile extends StatelessWidget { + const CustomRadioListTile({ + super.key, + required this.text, + required this.value, + required this.onContainerTap, + }); + + /// The text to display next to the radio button. + final String text; + + /// The value associated with the radio button. + final T value; + + /// The callback invoked when the container is tapped. + final ValueChanged onContainerTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => onContainerTap(value), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: CustomTheme.standardBorderRadiusAll, + ), + child: Row( + children: [ + Radio( + value: value, + activeColor: CustomTheme.primaryColor, + toggleable: true, + ), + Expanded( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart new file mode 100644 index 0000000..64d9caa --- /dev/null +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; + +/// A tile widget that displays information about a group, including its name and members. +/// - [group]: The group data to be displayed. +/// - [isHighlighted]: Whether the tile should be highlighted. +class GroupTile extends StatelessWidget { + const GroupTile({super.key, required this.group, this.isHighlighted = false}); + + /// The group data to be displayed. + final Group group; + + /// Whether the tile should be highlighted. + final bool isHighlighted; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + margin: CustomTheme.standardMargin, + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: isHighlighted + ? CustomTheme.highlightedBoxDecoration + : CustomTheme.standardBoxDecoration, + duration: const Duration(milliseconds: 150), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + group.name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + Row( + children: [ + Text( + '${group.members.length}', + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 18, + ), + ), + const SizedBox(width: 3), + const Icon(Icons.group, size: 22), + ], + ), + ], + ), + const SizedBox(height: 5), + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12.0, + runSpacing: 8.0, + children: [ + for (var member in [ + ...group.members, + ]..sort((a, b) => a.name.compareTo(b.name))) + TextIconTile(text: member.name, iconEnabled: false), + ], + ), + const SizedBox(height: 2.5), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/info_tile.dart b/lib/presentation/widgets/tiles/info_tile.dart new file mode 100644 index 0000000..3e11679 --- /dev/null +++ b/lib/presentation/widgets/tiles/info_tile.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A tile widget that displays a title with an icon and some content below it. +/// - [title]: The title text displayed on the tile. +/// - [icon]: The icon displayed next to the title. +/// - [content]: The content widget displayed below the title. +/// - [padding]: Optional padding for the tile content. +/// - [height]: Optional height for the tile. +/// - [width]: Optional width for the tile. +class InfoTile extends StatefulWidget { + const InfoTile({ + super.key, + required this.title, + required this.icon, + required this.content, + this.padding, + this.height, + this.width, + }); + + /// The title text displayed on the tile. + final String title; + + /// The icon displayed next to the title. + final IconData icon; + + /// The content widget displayed below the title. + final Widget content; + + /// Optional padding for the tile content. + final EdgeInsets? padding; + + /// Optional height for the tile. + final double? height; + + /// Optional width for the tile. + final double? width; + + @override + State createState() => _InfoTileState(); +} + +class _InfoTileState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: widget.padding ?? const EdgeInsets.all(12), + height: widget.height, + width: widget.width ?? 380, + decoration: CustomTheme.standardBoxDecoration, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + Icon(widget.icon), + const SizedBox(width: 5), + Text( + widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 10), + widget.content, + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_summary_tile.dart b/lib/presentation/widgets/tiles/match_summary_tile.dart new file mode 100644 index 0000000..719037b --- /dev/null +++ b/lib/presentation/widgets/tiles/match_summary_tile.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class MatchSummaryTile extends StatefulWidget { + final String matchTitle; + final String game; + final String ruleset; + final String players; + final String winner; + + const MatchSummaryTile({ + super.key, + required this.matchTitle, + required this.game, + required this.ruleset, + required this.players, + required this.winner, + }); + + @override + State createState() => _MatchSummaryTileState(); +} + +class _MatchSummaryTileState extends State { + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + widget.matchTitle, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(width: 5), + Text( + widget.game, + style: const TextStyle(fontSize: 14, color: Colors.grey), + ), + ], + ), + const SizedBox(height: 5), + Container( + padding: const EdgeInsets.symmetric(horizontal: 4), + height: 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: CustomTheme.primaryColor, + ), + child: Skeleton.ignore( + child: Text( + widget.ruleset, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + Center( + heightFactor: 1.5, + child: Text( + widget.players, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 4), + width: 220, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.yellow.shade300, + ), + child: Skeleton.ignore( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.emoji_events, color: Colors.black, size: 20), + Text( + widget.winner, + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart new file mode 100644 index 0000000..55d81c3 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; +import 'package:intl/intl.dart'; + +/// A tile widget that displays information about a match, including its name, +/// creation date, associated group, winner, and players. +/// - [match]: The match data to be displayed. +/// - [onTap]: The callback invoked when the tile is tapped. +class MatchTile extends StatefulWidget { + const MatchTile({super.key, required this.match, required this.onTap}); + + /// The match data to be displayed. + final Match match; + + /// The callback invoked when the tile is tapped. + final VoidCallback onTap; + + @override + State createState() => _MatchTileState(); +} + +class _MatchTileState extends State { + late final List _allPlayers; + + @override + void initState() { + super.initState(); + _allPlayers = _getCombinedPlayers(); + } + + @override + Widget build(BuildContext context) { + final group = widget.match.group; + final winner = widget.match.winner; + final loc = AppLocalizations.of(context); + + return GestureDetector( + onTap: widget.onTap, + child: Container( + margin: CustomTheme.tileMargin, + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + widget.match.name, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + _formatDate(widget.match.createdAt, context), + style: const TextStyle(fontSize: 12, color: Colors.grey), + ), + ], + ), + + const SizedBox(height: 8), + + if (group != null) ...[ + Row( + children: [ + const Icon(Icons.group, size: 16, color: Colors.grey), + const SizedBox(width: 6), + Expanded( + child: Text( + group.name, + style: const TextStyle(fontSize: 14, color: Colors.grey), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + ], + + if (winner != null) ...[ + 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: [ + const Icon( + Icons.emoji_events, + size: 20, + color: Colors.amber, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + '${loc.winner}: ${winner.name}', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: CustomTheme.textColor, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + const SizedBox(height: 12), + ], + + if (_allPlayers.isNotEmpty) ...[ + Text( + loc.players, + style: const TextStyle( + fontSize: 13, + color: Colors.grey, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 6), + Wrap( + spacing: 6, + runSpacing: 6, + children: _allPlayers.map((player) { + return TextIconTile(text: player.name, iconEnabled: false); + }).toList(), + ), + ], + ], + ), + ), + ); + } + + /// Formats the given [dateTime] into a human-readable string based on its + /// difference from the current date. + String _formatDate(DateTime dateTime, BuildContext context) { + final now = DateTime.now(); + final difference = now.difference(dateTime); + final loc = AppLocalizations.of(context); + + if (difference.inDays == 0) { + return "${loc.today_at} ${DateFormat('HH:mm').format(dateTime)}"; + } else if (difference.inDays == 1) { + return "${loc.yesterday_at} ${DateFormat('HH:mm').format(dateTime)}"; + } else if (difference.inDays < 7) { + return loc.days_ago(difference.inDays); + } else { + return DateFormat('MMM d, yyyy').format(dateTime); + } + } + + /// Retrieves all unique players associated with the match, + /// combining players from both the match and its group. + List _getCombinedPlayers() { + final allPlayers = []; + final playerIds = {}; + + // Add players from game.players + if (widget.match.players != null) { + for (var player in widget.match.players!) { + if (!playerIds.contains(player.id)) { + allPlayers.add(player); + playerIds.add(player.id); + } + } + } + + // Add players from game.group.players + if (widget.match.group?.members != null) { + for (var player in widget.match.group!.members) { + if (!playerIds.contains(player.id)) { + allPlayers.add(player); + playerIds.add(player.id); + } + } + } + + allPlayers.sort((a, b) => a.name.compareTo(b.name)); + return allPlayers; + } +} diff --git a/lib/presentation/widgets/tiles/quick_info_tile.dart b/lib/presentation/widgets/tiles/quick_info_tile.dart new file mode 100644 index 0000000..839f6c3 --- /dev/null +++ b/lib/presentation/widgets/tiles/quick_info_tile.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A tile widget that displays a title with an icon and a numeric value below it. +/// - [title]: The title text displayed on the tile. +/// - [icon]: The icon displayed next to the title. +/// - [value]: The numeric value displayed below the title. +/// - [height]: Optional height for the tile. +/// - [width]: Optional width for the tile. +/// - [padding]: Optional padding for the tile content. +class QuickInfoTile extends StatefulWidget { + const QuickInfoTile({ + super.key, + required this.title, + required this.icon, + required this.value, + this.height, + this.width, + this.padding, + }); + + /// The title text displayed on the tile. + final String title; + + /// The icon displayed next to the title. + final IconData icon; + + /// The numeric value displayed below the title. + final int value; + + /// Optional height for the tile. + final double? height; + + /// Optional width for the tile. + final double? width; + + /// Optional padding for the tile content. + final EdgeInsets? padding; + + @override + State createState() => _QuickInfoTileState(); +} + +class _QuickInfoTileState extends State { + @override + Widget build(BuildContext context) { + return Container( + padding: widget.padding ?? const EdgeInsets.all(12), + height: widget.height ?? 110, + width: widget.width ?? 180, + decoration: CustomTheme.standardBoxDecoration, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Row( + children: [ + Icon(widget.icon), + const SizedBox(width: 5), + Text( + widget.title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const Spacer(), + Text( + widget.value.toString(), + style: const TextStyle(fontSize: 32, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart new file mode 100644 index 0000000..7fb0f80 --- /dev/null +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget. +/// - [icon]: The icon displayed on the left side of the tile. +/// - [title]: The title text displayed next to the icon. +/// - [suffixWidget]: An optional widget displayed on the right side of the tile. +/// - [onPressed]: The callback invoked when the tile is tapped. +class SettingsListTile extends StatelessWidget { + const SettingsListTile({ + super.key, + required this.icon, + required this.title, + this.suffixWidget, + this.onPressed, + }); + + /// The icon displayed on the left side of the tile. + final IconData icon; + + /// The title text displayed next to the icon. + final String title; + + /// An optional widget displayed on the right side of the tile. + final Widget? suffixWidget; + + /// The callback invoked when the tile is tapped. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Center( + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.95, + child: GestureDetector( + onTap: onPressed ?? () {}, + child: Container( + margin: EdgeInsets.zero, + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), + decoration: CustomTheme.standardBoxDecoration, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: CustomTheme.primaryColor, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 24), + ), + const SizedBox(width: 16), + Text(title, style: const TextStyle(fontSize: 18)), + ], + ), + if (suffixWidget != null) + suffixWidget! + else + const SizedBox.shrink(), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart new file mode 100644 index 0000000..2c0ced0 --- /dev/null +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; + +/// A tile widget that displays statistical data using horizontal bars. +/// - [icon]: The icon displayed next to the title. +/// - [title]: The title text displayed on the tile. +/// - [width]: The width of the tile. +/// - [values]: A list of tuples containing labels and their corresponding numeric values. +/// - [itemCount]: The maximum number of items to display. +/// - [barColor]: The color of the bars representing the values. +class StatisticsTile extends StatelessWidget { + const StatisticsTile({ + super.key, + required this.icon, + required this.title, + required this.width, + required this.values, + required this.itemCount, + required this.barColor, + }); + + /// The icon displayed next to the title. + final IconData icon; + + /// The title text displayed on the tile. + final String title; + + /// The width of the tile. + final double width; + + /// A list of tuples containing labels and their corresponding numeric values. + final List<(String, num)> values; + + /// The maximum number of items to display. + final int itemCount; + + /// The color of the bars representing the values. + final Color barColor; + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + return InfoTile( + width: width, + title: title, + icon: icon, + content: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20.0), + child: Visibility( + visible: values.isNotEmpty, + replacement: Center( + heightFactor: 4, + child: Text(loc.no_data_available), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final maxBarWidth = constraints.maxWidth * 0.65; + return Column( + children: List.generate(min(values.length, itemCount), (index) { + /// The maximum wins among all players + final maxMatches = values.isNotEmpty ? values[0].$2 : 0; + + /// Fraction of wins + final double fraction = (maxMatches > 0) + ? (values[index].$2 / maxMatches) + : 0.0; + + /// Calculated width for current the bar + final double barWidth = maxBarWidth * fraction; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + Container( + height: 24, + width: barWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: barColor, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + values[index].$1, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + const Spacer(), + Center( + child: Text( + values[index].$2 <= 1 && values[index].$2 is double + ? values[index].$2.toStringAsFixed(2) + : values[index].$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ); + }), + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/text_icon_list_tile.dart b/lib/presentation/widgets/tiles/text_icon_list_tile.dart new file mode 100644 index 0000000..7d3fe1c --- /dev/null +++ b/lib/presentation/widgets/tiles/text_icon_list_tile.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A list tile widget that displays text with an optional icon button. +/// - [text]: The text to display in the tile. +/// - [onPressed]: The callback to be invoked when the icon is pressed. +/// - [iconEnabled]: A boolean to determine if the icon should be displayed. +class TextIconListTile extends StatelessWidget { + const TextIconListTile({ + super.key, + required this.text, + this.iconEnabled = true, + this.onPressed, + }); + + /// The text to display in the tile. + final String text; + + /// A boolean to determine if the icon should be displayed. + final bool iconEnabled; + + /// The callback to be invoked when the icon is pressed. + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5), + padding: const EdgeInsets.symmetric(horizontal: 15), + decoration: CustomTheme.standardBoxDecoration, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12.5), + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + if (iconEnabled) + GestureDetector( + onTap: onPressed, + child: const Icon(Icons.add, size: 20), + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/text_icon_tile.dart b/lib/presentation/widgets/tiles/text_icon_tile.dart new file mode 100644 index 0000000..7142b27 --- /dev/null +++ b/lib/presentation/widgets/tiles/text_icon_tile.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// 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. +class TextIconTile extends StatelessWidget { + const TextIconTile({ + super.key, + required this.text, + this.iconEnabled = true, + this.onIconTap, + }); + + /// The text to display in the tile. + final String text; + + /// 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; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(5), + decoration: BoxDecoration( + color: CustomTheme.onBoxColor, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: [ + if (iconEnabled) const SizedBox(width: 3), + Flexible( + child: Text( + text, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500), + ), + ), + if (iconEnabled) ...[ + const SizedBox(width: 3), + GestureDetector( + onTap: onIconTap, + child: const Icon(Icons.close, size: 20), + ), + ], + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart new file mode 100644 index 0000000..781149e --- /dev/null +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +/// A list tile widget that displays a title and description, with optional highlighting and badge. +/// - [title]: The title text displayed on the tile. +/// - [description]: The description text displayed below the title. +/// - [onPressed]: The callback invoked when the tile is tapped. +/// - [isHighlighted]: A boolean to determine if the tile should be highlighted. +/// - [badgeText]: Optional text to display in a badge on the right side of the title. +/// - [badgeColor]: Optional color for the badge background. +class TitleDescriptionListTile extends StatelessWidget { + const TitleDescriptionListTile({ + super.key, + required this.title, + required this.description, + this.onPressed, + this.isHighlighted = false, + this.badgeText, + this.badgeColor, + }); + + /// The title text displayed on the tile. + final String title; + + /// The description text displayed below the title. + final String description; + + /// The callback invoked when the tile is tapped. + final VoidCallback? onPressed; + + /// 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 + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + child: AnimatedContainer( + margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), + decoration: isHighlighted + ? CustomTheme.highlightedBoxDecoration + : CustomTheme.standardBoxDecoration, + duration: const Duration(milliseconds: 200), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 230, + child: Text( + title, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + ), + if (badgeText != null) ...[ + const Spacer(), + Container( + constraints: const BoxConstraints(maxWidth: 115), + margin: const EdgeInsets.only(top: 4), + padding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 6, + ), + decoration: BoxDecoration( + color: badgeColor ?? CustomTheme.primaryColor, + borderRadius: BorderRadius.circular(4), + ), + child: Text( + badgeText!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + softWrap: false, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + if (description.isNotEmpty) ...[ + const SizedBox(height: 5), + Text(description, style: const TextStyle(fontSize: 14)), + const SizedBox(height: 2.5), + ], + ], + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/top_centered_message.dart b/lib/presentation/widgets/top_centered_message.dart new file mode 100644 index 0000000..c15c93d --- /dev/null +++ b/lib/presentation/widgets/top_centered_message.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + +/// A widget that displays a message centered at the top of the screen with an icon, title, and message. +/// - [icon]: The icon to display above the title. +/// - [title]: The title text to display. +/// - [message]: The message text to display below the title. +class TopCenteredMessage extends StatelessWidget { + const TopCenteredMessage({ + super.key, + required this.icon, + required this.title, + required this.message, + }); + + /// The icon to display above the title. + final IconData icon; + + /// The title text to display. + final String title; + + /// The message text to display below the title. + final String message; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 100), + margin: const EdgeInsets.symmetric(horizontal: 10), + alignment: Alignment.topCenter, + child: Column( + children: [ + Icon(icon, size: 45), + const SizedBox(height: 10), + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + Text( + message, + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + ], + ), + ); + } +} diff --git a/lib/services/data_transfer_service.dart b/lib/services/data_transfer_service.dart new file mode 100644 index 0000000..8767c59 --- /dev/null +++ b/lib/services/data_transfer_service.dart @@ -0,0 +1,215 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:json_schema/json_schema.dart'; +import 'package:provider/provider.dart'; + +class DataTransferService { + /// Deletes all data from the database. + static Future deleteAllData(BuildContext context) async { + final db = Provider.of(context, listen: false); + await db.matchDao.deleteAllMatches(); + await db.groupDao.deleteAllGroups(); + await db.playerDao.deleteAllPlayers(); + } + + /// Retrieves all application data and converts it to a JSON string. + /// Returns the JSON string representation of the data. + static Future getAppDataAsJson(BuildContext context) async { + final db = Provider.of(context, listen: false); + final matches = await db.matchDao.getAllMatches(); + final groups = await db.groupDao.getAllGroups(); + final players = await db.playerDao.getAllPlayers(); + + // Construct a JSON representation of the data + final Map jsonMap = { + 'players': players.map((p) => p.toJson()).toList(), + + 'groups': groups + .map((g) => { + 'id': g.id, + 'name': g.name, + 'createdAt': g.createdAt.toIso8601String(), + 'memberIds': (g.members).map((m) => m.id).toList(), + }).toList(), + + 'matches': matches + .map((m) => { + 'id': m.id, + 'name': m.name, + 'createdAt': m.createdAt.toIso8601String(), + 'groupId': m.group?.id, + 'playerIds': (m.players ?? []).map((p) => p.id).toList(), + 'winnerId': m.winner?.id, + }).toList(), + }; + + return json.encode(jsonMap); + } + + /// Exports the given JSON string to a file with the specified name. + /// Returns an [ExportResult] indicating the outcome. + /// + /// [jsonString] The JSON string to be exported. + /// [fileName] The desired name for the exported file (without extension). + static Future exportData( + String jsonString, + String fileName + ) async { + try { + final bytes = Uint8List.fromList(utf8.encode(jsonString)); + final path = await FilePicker.platform.saveFile( + fileName: '$fileName.json', + bytes: bytes, + ); + + if (path == null) { + return ExportResult.canceled; + } else { + return ExportResult.success; + } + + } catch (e, stack) { + print('[exportData] $e'); + print(stack); + return ExportResult.unknownException; + } + } + + /// Imports data from a selected JSON file into the database. + static Future importData(BuildContext context) async { + final db = Provider.of(context, listen: false); + + final path = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + + if (path == null) { + return ImportResult.canceled; + } + + try { + final jsonString = await _readFileContent(path.files.single); + if (jsonString == null) return ImportResult.fileReadError; + + final isValid = await _validateJsonSchema(jsonString); + if (!isValid) return ImportResult.invalidSchema; + + final Map decoded = json.decode(jsonString) as Map; + + final List playersJson = (decoded['players'] as List?) ?? []; + final List groupsJson = (decoded['groups'] as List?) ?? []; + final List matchesJson = (decoded['matches'] as List?) ?? []; + + // Players + final List importedPlayers = playersJson + .map((p) => Player.fromJson(p as Map)) + .toList(); + + final Map playerById = { + for (final p in importedPlayers) p.id: p, + }; + + // Groups + final List importedGroups = groupsJson.map((g) { + final map = g as Map; + final memberIds = (map['memberIds'] as List? ?? []).cast(); + + final members = memberIds + .map((id) => playerById[id]) + .whereType() + .toList(); + + return Group( + id: map['id'] as String, + name: map['name'] as String, + members: members, + createdAt: DateTime.parse(map['createdAt'] as String), + ); + }).toList(); + + final Map groupById = { + for (final g in importedGroups) g.id: g, + }; + + // Matches + final List importedMatches = matchesJson.map((m) { + final map = m as Map; + + final String? groupId = map['groupId'] as String?; + final List playerIds = (map['playerIds'] as List? ?? []).cast(); + final String? winnerId = map['winnerId'] as String?; + + final group = (groupId == null) ? null : groupById[groupId]; + final players = playerIds + .map((id) => playerById[id]) + .whereType() + .toList(); + final winner = (winnerId == null) ? null : playerById[winnerId]; + + return Match( + id: map['id'] as String, + name: map['name'] as String, + group: group, + players: players, + createdAt: DateTime.parse(map['createdAt'] as String), + winner: winner, + ); + }).toList(); + + await db.playerDao.addPlayersAsList(players: importedPlayers); + await db.groupDao.addGroupsAsList(groups: importedGroups); + await db.matchDao.addMatchAsList(matches: importedMatches); + + return ImportResult.success; + } on FormatException catch (e, stack) { + print('[importData] FormatException'); + print('[importData] $e'); + print(stack); + return ImportResult.formatException; + } on Exception catch (e, stack) { + print('[importData] Exception'); + print('[importData] $e'); + print(stack); + return ImportResult.unknownException; + } + } + + /// Helper method to read file content from either bytes or path + static Future _readFileContent(PlatformFile file) async { + if (file.bytes != null) return utf8.decode(file.bytes!); + if (file.path != null) return await File(file.path!).readAsString(); + return null; + } + + /// Validates the given JSON string against the predefined schema. + static Future _validateJsonSchema(String jsonString) async { + final String schemaString; + + schemaString = await rootBundle.loadString('assets/schema.json'); + + try { + final schema = JsonSchema.create(json.decode(schemaString)); + final jsonData = json.decode(jsonString); + final result = schema.validate(jsonData); + + if (result.isValid) { + return true; + } + return false; + } catch (e, stack) { + print('[validateJsonSchema] $e'); + print(stack); + return false; + } + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index aa5fa8a..e79ca17 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,89 +1,42 @@ name: game_tracker -description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+1 +description: "Game Tracking App for Card Games" +publish_to: 'none' +version: 0.0.1+21 environment: sdk: ^3.8.1 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter + material_symbols_icons: ^4.2815.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + drift: ^2.27.0 + drift_flutter: ^0.2.4 + path_provider: ^2.1.5 + provider: ^6.1.5 + skeletonizer: ^2.1.0+1 + uuid: ^4.5.2 + file_picker: ^10.3.6 + json_schema: ^5.2.2 + file_saver: ^0.3.1 + clock: ^1.1.2 + intl: any + flutter_localizations: + sdk: flutter dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^5.0.0 + drift_dev: ^2.27.0 + build_runner: ^2.5.4 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package + generate: true + assets: + - assets/schema.json diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart new file mode 100644 index 0000000..0ec2cfc --- /dev/null +++ b/test/db_tests/game_test.dart @@ -0,0 +1,362 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; + late Group testGroup1; + late Group testGroup2; + late Match testMatch1; + late Match testMatch2; + late Match testMatchOnlyPlayers; + late Match testMatchOnlyGroup; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() async { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testGroup1 = Group( + name: 'Test Group 2', + members: [testPlayer1, testPlayer2, testPlayer3], + ); + testGroup2 = Group( + name: 'Test Group 2', + members: [testPlayer4, testPlayer5], + ); + testMatch1 = Match( + name: 'First Test Match', + group: testGroup1, + players: [testPlayer4, testPlayer5], + winner: testPlayer4, + ); + testMatch2 = Match( + name: 'Second Test Match', + group: testGroup2, + players: [testPlayer1, testPlayer2, testPlayer3], + winner: testPlayer2, + ); + testMatchOnlyPlayers = Match( + name: 'Test Match with Players', + players: [testPlayer1, testPlayer2, testPlayer3], + winner: testPlayer3, + ); + testMatchOnlyGroup = Match( + name: 'Test Match with Group', + group: testGroup2, + ); + }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); + }); + tearDown(() async { + await database.close(); + }); + + group('Match Tests', () { + test('Adding and fetching single match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final result = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + + expect(result.id, testMatch1.id); + expect(result.name, testMatch1.name); + expect(result.createdAt, testMatch1.createdAt); + + if (result.winner != null && testMatch1.winner != null) { + expect(result.winner!.id, testMatch1.winner!.id); + expect(result.winner!.name, testMatch1.winner!.name); + expect(result.winner!.createdAt, testMatch1.winner!.createdAt); + } else { + expect(result.winner, testMatch1.winner); + } + + if (result.group != null) { + expect(result.group!.members.length, testGroup1.members.length); + + for (int i = 0; i < testGroup1.members.length; i++) { + expect(result.group!.members[i].id, testGroup1.members[i].id); + expect(result.group!.members[i].name, testGroup1.members[i].name); + } + } else { + fail('Group is null'); + } + if (result.players != null) { + expect(result.players!.length, testMatch1.players!.length); + + for (int i = 0; i < testMatch1.players!.length; i++) { + expect(result.players![i].id, testMatch1.players![i].id); + expect(result.players![i].name, testMatch1.players![i].name); + expect( + result.players![i].createdAt, + testMatch1.players![i].createdAt, + ); + } + } else { + fail('Players is null'); + } + }); + + test('Adding and fetching multiple matches works correctly', () async { + await database.matchDao.addMatchAsList( + matches: [ + testMatch1, + testMatch2, + testMatchOnlyGroup, + testMatchOnlyPlayers, + ], + ); + + final allMatches = await database.matchDao.getAllMatches(); + expect(allMatches.length, 4); + + final testMatches = { + testMatch1.id: testMatch1, + testMatch2.id: testMatch2, + testMatchOnlyGroup.id: testMatchOnlyGroup, + testMatchOnlyPlayers.id: testMatchOnlyPlayers, + }; + + for (final match in allMatches) { + final testMatch = testMatches[match.id]!; + + // Match-Checks + expect(match.id, testMatch.id); + expect(match.name, testMatch.name); + expect(match.createdAt, testMatch.createdAt); + if (match.winner != null && testMatch.winner != null) { + expect(match.winner!.id, testMatch.winner!.id); + expect(match.winner!.name, testMatch.winner!.name); + expect(match.winner!.createdAt, testMatch.winner!.createdAt); + } else { + expect(match.winner, testMatch.winner); + } + + // Group-Checks + if (testMatch.group != null) { + expect(match.group!.id, testMatch.group!.id); + expect(match.group!.name, testMatch.group!.name); + expect(match.group!.createdAt, testMatch.group!.createdAt); + + // Group Members-Checks + expect(match.group!.members.length, testMatch.group!.members.length); + for (int i = 0; i < testMatch.group!.members.length; i++) { + expect(match.group!.members[i].id, testMatch.group!.members[i].id); + expect( + match.group!.members[i].name, + testMatch.group!.members[i].name, + ); + expect( + match.group!.members[i].createdAt, + testMatch.group!.members[i].createdAt, + ); + } + } else { + expect(match.group, null); + } + + // Players-Checks + if (testMatch.players != null) { + expect(match.players!.length, testMatch.players!.length); + for (int i = 0; i < testMatch.players!.length; i++) { + expect(match.players![i].id, testMatch.players![i].id); + expect(match.players![i].name, testMatch.players![i].name); + expect( + match.players![i].createdAt, + testMatch.players![i].createdAt, + ); + } + } else { + expect(match.players, null); + } + } + }); + + test('Adding the same match twice does not create duplicates', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: testMatch1); + + final matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + }); + + test('Match existence check works correctly', () async { + var matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, false); + + await database.matchDao.addMatch(match: testMatch1); + + matchExists = await database.matchDao.matchExists(matchId: testMatch1.id); + expect(matchExists, true); + }); + + test('Deleting a match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final matchDeleted = await database.matchDao.deleteMatch( + matchId: testMatch1.id, + ); + expect(matchDeleted, true); + + final matchExists = await database.matchDao.matchExists( + matchId: testMatch1.id, + ); + expect(matchExists, false); + }); + + test('Getting the match count works correctly', () async { + var matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 0); + + await database.matchDao.addMatch(match: testMatch1); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + + await database.matchDao.addMatch(match: testMatch2); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 2); + + await database.matchDao.deleteMatch(matchId: testMatch1.id); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 1); + + await database.matchDao.deleteMatch(matchId: testMatch2.id); + + matchCount = await database.matchDao.getMatchCount(); + expect(matchCount, 0); + }); + + test('Checking if match has winner works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + await database.matchDao.addMatch(match: testMatchOnlyGroup); + + var hasWinner = await database.matchDao.hasWinner(matchId: testMatch1.id); + expect(hasWinner, true); + + hasWinner = await database.matchDao.hasWinner( + matchId: testMatchOnlyGroup.id, + ); + expect(hasWinner, false); + }); + + test('Fetching the winner of a match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final winner = await database.matchDao.getWinner(matchId: testMatch1.id); + if (winner == null) { + fail('Winner is null'); + } else { + expect(winner.id, testMatch1.winner!.id); + expect(winner.name, testMatch1.winner!.name); + expect(winner.createdAt, testMatch1.winner!.createdAt); + } + }); + + test('Updating the winner of a match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + final winner = await database.matchDao.getWinner(matchId: testMatch1.id); + if (winner == null) { + fail('Winner is null'); + } else { + expect(winner.id, testMatch1.winner!.id); + expect(winner.name, testMatch1.winner!.name); + expect(winner.createdAt, testMatch1.winner!.createdAt); + expect(winner.id, testPlayer4.id); + expect(winner.id != testPlayer5.id, true); + } + + await database.matchDao.setWinner( + matchId: testMatch1.id, + winnerId: testPlayer5.id, + ); + + final newWinner = await database.matchDao.getWinner( + matchId: testMatch1.id, + ); + + if (newWinner == null) { + fail('New winner is null'); + } else { + expect(newWinner.id, testPlayer5.id); + expect(newWinner.name, testPlayer5.name); + expect(newWinner.createdAt, testPlayer5.createdAt); + } + }); + + test('Removing a winner works correctly', () async { + await database.matchDao.addMatch(match: testMatch2); + + var hasWinner = await database.matchDao.hasWinner(matchId: testMatch2.id); + expect(hasWinner, true); + + await database.matchDao.removeWinner(matchId: testMatch2.id); + + hasWinner = await database.matchDao.hasWinner(matchId: testMatch2.id); + expect(hasWinner, false); + + final removedWinner = await database.matchDao.getWinner( + matchId: testMatch2.id, + ); + + expect(removedWinner, null); + }); + + test('Renaming a match works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + var fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.name, testMatch1.name); + + const newName = 'Updated Match Name'; + await database.matchDao.updateMatchName( + matchId: testMatch1.id, + newName: newName, + ); + + fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.name, newName); + }); + }); +} diff --git a/test/db_tests/group_match_test.dart b/test/db_tests/group_match_test.dart new file mode 100644 index 0000000..7d812bd --- /dev/null +++ b/test/db_tests/group_match_test.dart @@ -0,0 +1,221 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' hide isNotNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; + late Group testGroup1; + late Group testGroup2; + late Match testMatchWithGroup; + late Match testMatchWithPlayers; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() async { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testGroup1 = Group( + name: 'Test Group', + members: [testPlayer1, testPlayer2, testPlayer3], + ); + testGroup2 = Group( + name: 'Test Group', + members: [testPlayer3, testPlayer2], + ); + testMatchWithPlayers = Match( + name: 'Test Match with Players', + players: [testPlayer4, testPlayer5], + ); + testMatchWithGroup = Match( + name: 'Test Match with Group', + group: testGroup1, + ); + }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); + }); + tearDown(() async { + await database.close(); + }); + group('Group-Match Tests', () { + test('matchHasGroup() has group works correctly', () async { + await database.matchDao.addMatch(match: testMatchWithPlayers); + await database.groupDao.addGroup(group: testGroup1); + + var matchHasGroup = await database.groupMatchDao.matchHasGroup( + matchId: testMatchWithPlayers.id, + ); + + expect(matchHasGroup, false); + + await database.groupMatchDao.addGroupToMatch( + matchId: testMatchWithPlayers.id, + groupId: testGroup1.id, + ); + + matchHasGroup = await database.groupMatchDao.matchHasGroup( + matchId: testMatchWithPlayers.id, + ); + + expect(matchHasGroup, true); + }); + + test('Adding a group to a match works correctly', () async { + await database.matchDao.addMatch(match: testMatchWithPlayers); + await database.groupDao.addGroup(group: testGroup1); + await database.groupMatchDao.addGroupToMatch( + matchId: testMatchWithPlayers.id, + groupId: testGroup1.id, + ); + + var groupAdded = await database.groupMatchDao.isGroupInMatch( + matchId: testMatchWithPlayers.id, + groupId: testGroup1.id, + ); + expect(groupAdded, true); + + groupAdded = await database.groupMatchDao.isGroupInMatch( + matchId: testMatchWithPlayers.id, + groupId: '', + ); + expect(groupAdded, false); + }); + + test('Removing group from match works correctly', () async { + await database.matchDao.addMatch(match: testMatchWithGroup); + + final groupToRemove = testMatchWithGroup.group!; + + final removed = await database.groupMatchDao.removeGroupFromMatch( + groupId: groupToRemove.id, + matchId: testMatchWithGroup.id, + ); + expect(removed, true); + + final result = await database.matchDao.getMatchById( + matchId: testMatchWithGroup.id, + ); + expect(result.group, null); + }); + + test('Retrieving group of a match works correctly', () async { + await database.matchDao.addMatch(match: testMatchWithGroup); + final group = await database.groupMatchDao.getGroupOfMatch( + matchId: testMatchWithGroup.id, + ); + + if (group == null) { + fail('Group should not be null'); + } + + expect(group.id, testGroup1.id); + expect(group.name, testGroup1.name); + expect(group.createdAt, testGroup1.createdAt); + expect(group.members.length, testGroup1.members.length); + for (int i = 0; i < group.members.length; i++) { + expect(group.members[i].id, testGroup1.members[i].id); + expect(group.members[i].name, testGroup1.members[i].name); + expect(group.members[i].createdAt, testGroup1.members[i].createdAt); + } + }); + + test('Updating the group of a match works correctly', () async { + await database.matchDao.addMatch(match: testMatchWithGroup); + + var group = await database.groupMatchDao.getGroupOfMatch( + matchId: testMatchWithGroup.id, + ); + + if (group == null) { + fail('Initial group should not be null'); + } else { + expect(group.id, testGroup1.id); + expect(group.name, testGroup1.name); + expect(group.createdAt, testGroup1.createdAt); + expect(group.members.length, testGroup1.members.length); + } + + await database.groupDao.addGroup(group: testGroup2); + await database.groupMatchDao.updateGroupOfMatch( + matchId: testMatchWithGroup.id, + newGroupId: testGroup2.id, + ); + + group = await database.groupMatchDao.getGroupOfMatch( + matchId: testMatchWithGroup.id, + ); + + if (group == null) { + fail('Updated group should not be null'); + } else { + expect(group.id, testGroup2.id); + expect(group.name, testGroup2.name); + expect(group.createdAt, testGroup2.createdAt); + expect(group.members.length, testGroup2.members.length); + for (int i = 0; i < group.members.length; i++) { + expect(group.members[i].id, testGroup2.members[i].id); + expect(group.members[i].name, testGroup2.members[i].name); + expect(group.members[i].createdAt, testGroup2.members[i].createdAt); + } + } + }); + + test('Adding the same group to seperate matches works correctly', () async { + final match1 = Match(name: 'Match 1', group: testGroup1); + final match2 = Match(name: 'Match 2', group: testGroup1); + + await Future.wait([ + database.matchDao.addMatch(match: match1), + database.matchDao.addMatch(match: match2), + ]); + + final group1 = await database.groupMatchDao.getGroupOfMatch( + matchId: match1.id, + ); + final group2 = await database.groupMatchDao.getGroupOfMatch( + matchId: match2.id, + ); + + expect(group1, isNotNull); + expect(group2, isNotNull); + + final groups = [group1!, group2!]; + for (final group in groups) { + expect(group.members.length, testGroup1.members.length); + expect(group.id, testGroup1.id); + expect(group.name, testGroup1.name); + expect(group.createdAt, testGroup1.createdAt); + } + }); + }); +} diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart new file mode 100644 index 0000000..5104c65 --- /dev/null +++ b/test/db_tests/group_test.dart @@ -0,0 +1,177 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Group testGroup1; + late Group testGroup2; + late Group testGroup3; + late Group testGroup4; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testGroup1 = Group( + name: 'Test Group', + members: [testPlayer1, testPlayer2, testPlayer3], + ); + testGroup2 = Group( + id: 'gr2', + name: 'Second Group', + members: [testPlayer2, testPlayer3, testPlayer4], + ); + testGroup3 = Group( + id: 'gr2', + name: 'Second Group', + members: [testPlayer2, testPlayer4], + ); + testGroup4 = Group( + id: 'gr2', + name: 'Second Group', + members: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], + ); + }); + }); + tearDown(() async { + await database.close(); + }); + group('Group Tests', () { + test('Adding and fetching a single group works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + final fetchedGroup = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + + expect(fetchedGroup.id, testGroup1.id); + expect(fetchedGroup.name, testGroup1.name); + expect(fetchedGroup.createdAt, testGroup1.createdAt); + + expect(fetchedGroup.members.length, testGroup1.members.length); + for (int i = 0; i < testGroup1.members.length; i++) { + expect(fetchedGroup.members[i].id, testGroup1.members[i].id); + expect(fetchedGroup.members[i].name, testGroup1.members[i].name); + expect( + fetchedGroup.members[i].createdAt, + testGroup1.members[i].createdAt, + ); + } + }); + + test('Adding and fetching multiple groups works correctly', () async { + await database.groupDao.addGroupsAsList( + groups: [testGroup1, testGroup2, testGroup3, testGroup4], + ); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 2); + + final testGroups = {testGroup1.id: testGroup1, testGroup2.id: testGroup2}; + + for (final group in allGroups) { + final testGroup = testGroups[group.id]!; + + expect(group.id, testGroup.id); + expect(group.name, testGroup.name); + expect(group.createdAt, testGroup.createdAt); + + expect(group.members.length, testGroup.members.length); + for (int i = 0; i < testGroup.members.length; i++) { + expect(group.members[i].id, testGroup.members[i].id); + expect(group.members[i].name, testGroup.members[i].name); + expect(group.members[i].createdAt, testGroup.members[i].createdAt); + } + } + }); + + test('Adding the same group twice does not create duplicates', () async { + await database.groupDao.addGroup(group: testGroup1); + await database.groupDao.addGroup(group: testGroup1); + + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 1); + }); + + test('Group existence check works correctly', () async { + var groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, false); + + await database.groupDao.addGroup(group: testGroup1); + + groupExists = await database.groupDao.groupExists(groupId: testGroup1.id); + expect(groupExists, true); + }); + + test('Deleting a group works correctly', () async { + await database.groupDao.addGroup(group: testGroup1); + + final groupDeleted = await database.groupDao.deleteGroup( + groupId: testGroup1.id, + ); + expect(groupDeleted, true); + + final groupExists = await database.groupDao.groupExists( + groupId: testGroup1.id, + ); + expect(groupExists, false); + }); + + test('Updating a group name works correcly', () async { + await database.groupDao.addGroup(group: testGroup1); + + const newGroupName = 'new group name'; + + await database.groupDao.updateGroupname( + groupId: testGroup1.id, + newName: newGroupName, + ); + + final result = await database.groupDao.getGroupById( + groupId: testGroup1.id, + ); + expect(result.name, newGroupName); + }); + + test('Getting the group count works correctly', () async { + final initialCount = await database.groupDao.getGroupCount(); + expect(initialCount, 0); + + await database.groupDao.addGroup(group: testGroup1); + + final groupAdded = await database.groupDao.getGroupCount(); + expect(groupAdded, 1); + + final groupRemoved = await database.groupDao.deleteGroup( + groupId: testGroup1.id, + ); + expect(groupRemoved, true); + + final finalCount = await database.groupDao.getGroupCount(); + expect(finalCount, 0); + }); + }); +} diff --git a/test/db_tests/player_group_test.dart b/test/db_tests/player_group_test.dart new file mode 100644 index 0000000..2783430 --- /dev/null +++ b/test/db_tests/player_group_test.dart @@ -0,0 +1,103 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Group testgroup; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testgroup = Group( + name: 'Test Group', + members: [testPlayer1, testPlayer2, testPlayer3], + ); + }); + }); + tearDown(() async { + await database.close(); + }); + + group('Player-Group Tests', () { + /// No need to test if group has players since the members attribute is + /// not nullable + + test('Adding a player to a group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); + await database.playerDao.addPlayer(player: testPlayer4); + await database.playerGroupDao.addPlayerToGroup( + groupId: testgroup.id, + player: testPlayer4, + ); + + var playerAdded = await database.playerGroupDao.isPlayerInGroup( + groupId: testgroup.id, + playerId: testPlayer4.id, + ); + + expect(playerAdded, true); + + playerAdded = await database.playerGroupDao.isPlayerInGroup( + groupId: testgroup.id, + playerId: '', + ); + + expect(playerAdded, false); + }); + + test('Removing player from group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); + + final playerToRemove = testgroup.members[0]; + + final removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: playerToRemove.id, + groupId: testgroup.id, + ); + expect(removed, true); + + final result = await database.groupDao.getGroupById( + groupId: testgroup.id, + ); + expect(result.members.length, testgroup.members.length - 1); + + final playerExists = result.members.any((p) => p.id == playerToRemove.id); + expect(playerExists, false); + }); + + test('Retrieving players of a group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); + final players = await database.playerGroupDao.getPlayersOfGroup( + groupId: testgroup.id, + ); + + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testgroup.members[i].id); + expect(players[i].name, testgroup.members[i].name); + expect(players[i].createdAt, testgroup.members[i].createdAt); + } + }); + }); +} diff --git a/test/db_tests/player_match_test.dart b/test/db_tests/player_match_test.dart new file mode 100644 index 0000000..8a4f569 --- /dev/null +++ b/test/db_tests/player_match_test.dart @@ -0,0 +1,237 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart' hide isNotNull; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + late Player testPlayer5; + late Player testPlayer6; + late Group testgroup; + late Match testMatchOnlyGroup; + late Match testMatchOnlyPlayers; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() async { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Alice'); + testPlayer2 = Player(name: 'Bob'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + testPlayer5 = Player(name: 'Eve'); + testPlayer6 = Player(name: 'Frank'); + testgroup = Group( + name: 'Test Group', + members: [testPlayer1, testPlayer2, testPlayer3], + ); + testMatchOnlyGroup = Match( + name: 'Test Match with Group', + group: testgroup, + ); + testMatchOnlyPlayers = Match( + name: 'Test Match with Players', + players: [testPlayer4, testPlayer5, testPlayer6], + ); + }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + testPlayer6, + ], + ); + await database.groupDao.addGroup(group: testgroup); + }); + tearDown(() async { + await database.close(); + }); + + group('Player-Match Tests', () { + test('Match has player works correctly', () async { + await database.matchDao.addMatch(match: testMatchOnlyGroup); + await database.playerDao.addPlayer(player: testPlayer1); + + var matchHasPlayers = await database.playerMatchDao.matchHasPlayers( + matchId: testMatchOnlyGroup.id, + ); + + expect(matchHasPlayers, false); + + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatchOnlyGroup.id, + playerId: testPlayer1.id, + ); + + matchHasPlayers = await database.playerMatchDao.matchHasPlayers( + matchId: testMatchOnlyGroup.id, + ); + + expect(matchHasPlayers, true); + }); + + test('Adding a player to a match works correctly', () async { + await database.matchDao.addMatch(match: testMatchOnlyGroup); + await database.playerDao.addPlayer(player: testPlayer5); + await database.playerMatchDao.addPlayerToMatch( + matchId: testMatchOnlyGroup.id, + playerId: testPlayer5.id, + ); + + var playerAdded = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatchOnlyGroup.id, + playerId: testPlayer5.id, + ); + + expect(playerAdded, true); + + playerAdded = await database.playerMatchDao.isPlayerInMatch( + matchId: testMatchOnlyGroup.id, + playerId: '', + ); + + expect(playerAdded, false); + }); + + test('Removing player from match works correctly', () async { + await database.matchDao.addMatch(match: testMatchOnlyPlayers); + + final playerToRemove = testMatchOnlyPlayers.players![0]; + + final removed = await database.playerMatchDao.removePlayerFromMatch( + playerId: playerToRemove.id, + matchId: testMatchOnlyPlayers.id, + ); + expect(removed, true); + + final result = await database.matchDao.getMatchById( + matchId: testMatchOnlyPlayers.id, + ); + expect(result.players!.length, testMatchOnlyPlayers.players!.length - 1); + + final playerExists = result.players!.any( + (p) => p.id == playerToRemove.id, + ); + expect(playerExists, false); + }); + + test('Retrieving players of a match works correctly', () async { + await database.matchDao.addMatch(match: testMatchOnlyPlayers); + final players = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatchOnlyPlayers.id, + ); + + if (players == null) { + fail('Players should not be null'); + } + + for (int i = 0; i < players.length; i++) { + expect(players[i].id, testMatchOnlyPlayers.players![i].id); + expect(players[i].name, testMatchOnlyPlayers.players![i].name); + expect( + players[i].createdAt, + testMatchOnlyPlayers.players![i].createdAt, + ); + } + }); + + test('Updating the match players works coreclty', () async { + await database.matchDao.addMatch(match: testMatchOnlyPlayers); + + final newPlayers = [testPlayer1, testPlayer2, testPlayer4]; + await database.playerDao.addPlayersAsList(players: newPlayers); + + // First, remove all existing players + final existingPlayers = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatchOnlyPlayers.id, + ); + + if (existingPlayers == null || existingPlayers.isEmpty) { + fail('Existing players should not be null or empty'); + } + + await database.playerMatchDao.updatePlayersFromMatch( + matchId: testMatchOnlyPlayers.id, + newPlayer: newPlayers, + ); + + final updatedPlayers = await database.playerMatchDao.getPlayersOfMatch( + matchId: testMatchOnlyPlayers.id, + ); + + if (updatedPlayers == null) { + fail('Updated players should not be null'); + } + + expect(updatedPlayers.length, newPlayers.length); + + /// Create a map of new players for easy lookup + final testPlayers = {for (var p in newPlayers) p.id: p}; + + /// Verify each updated player matches the new players + for (final player in updatedPlayers) { + final testPlayer = testPlayers[player.id]!; + + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); + } + }); + + test( + 'Adding the same player to seperate matches works correctly', + () async { + final playersList = [testPlayer1, testPlayer2, testPlayer3]; + final match1 = Match(name: 'Match 1', players: playersList); + final match2 = Match(name: 'Match 2', players: playersList); + + await Future.wait([ + database.matchDao.addMatch(match: match1), + database.matchDao.addMatch(match: match2), + ]); + + final players1 = await database.playerMatchDao.getPlayersOfMatch( + matchId: match1.id, + ); + final players2 = await database.playerMatchDao.getPlayersOfMatch( + matchId: match2.id, + ); + + expect(players1, isNotNull); + expect(players2, isNotNull); + + expect( + players1!.map((p) => p.id).toList(), + equals(players2!.map((p) => p.id).toList()), + ); + expect( + players1.map((p) => p.name).toList(), + equals(players2.map((p) => p.name).toList()), + ); + expect( + players1.map((p) => p.createdAt).toList(), + equals(players2.map((p) => p.createdAt).toList()), + ); + }, + ); + }); +} diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart new file mode 100644 index 0000000..5bd10ad --- /dev/null +++ b/test/db_tests/player_test.dart @@ -0,0 +1,159 @@ +import 'package:clock/clock.dart'; +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/player.dart'; + +void main() { + late AppDatabase database; + late Player testPlayer1; + late Player testPlayer2; + late Player testPlayer3; + late Player testPlayer4; + final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); + final fakeClock = Clock(() => fixedDate); + + setUp(() { + database = AppDatabase( + DatabaseConnection( + NativeDatabase.memory(), + // Recommended for widget tests to avoid test errors. + closeStreamsSynchronously: true, + ), + ); + + withClock(fakeClock, () { + testPlayer1 = Player(name: 'Test Player'); + testPlayer2 = Player(name: 'Second Player'); + testPlayer3 = Player(name: 'Charlie'); + testPlayer4 = Player(name: 'Diana'); + }); + }); + tearDown(() async { + await database.close(); + }); + + group('Player Tests', () { + test('Adding and fetching single player works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer2); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); + + final fetchedPlayer1 = allPlayers.firstWhere( + (g) => g.id == testPlayer1.id, + ); + expect(fetchedPlayer1.name, testPlayer1.name); + expect(fetchedPlayer1.createdAt, testPlayer1.createdAt); + + final fetchedPlayer2 = allPlayers.firstWhere( + (g) => g.id == testPlayer2.id, + ); + expect(fetchedPlayer2.name, testPlayer2.name); + expect(fetchedPlayer2.createdAt, testPlayer2.createdAt); + }); + + test('Adding and fetching multiple players works correctly', () async { + await database.playerDao.addPlayersAsList( + players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4], + ); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 4); + + // Map for connencting fetched players with expected players + final testPlayers = { + testPlayer1.id: testPlayer1, + testPlayer2.id: testPlayer2, + testPlayer3.id: testPlayer3, + testPlayer4.id: testPlayer4, + }; + + for (final player in allPlayers) { + final testPlayer = testPlayers[player.id]!; + + expect(player.id, testPlayer.id); + expect(player.name, testPlayer.name); + expect(player.createdAt, testPlayer.createdAt); + } + }); + + test('Adding the same player twice does not create duplicates', () async { + await database.playerDao.addPlayer(player: testPlayer1); + await database.playerDao.addPlayer(player: testPlayer1); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 1); + }); + + test('Player existence check works correctly', () async { + var playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, false); + + await database.playerDao.addPlayer(player: testPlayer1); + + playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, true); + }); + + test('Deleting a player works correctly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + final playerDeleted = await database.playerDao.deletePlayer( + playerId: testPlayer1.id, + ); + expect(playerDeleted, true); + + final playerExists = await database.playerDao.playerExists( + playerId: testPlayer1.id, + ); + expect(playerExists, false); + }); + + test('Updating a player name works correcly', () async { + await database.playerDao.addPlayer(player: testPlayer1); + + const newPlayerName = 'new player name'; + + await database.playerDao.updatePlayername( + playerId: testPlayer1.id, + newName: newPlayerName, + ); + + final result = await database.playerDao.getPlayerById( + playerId: testPlayer1.id, + ); + expect(result.name, newPlayerName); + }); + + test('Getting the player count works correctly', () async { + var playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + + await database.playerDao.addPlayer(player: testPlayer1); + + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.addPlayer(player: testPlayer2); + + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 2); + + await database.playerDao.deletePlayer(playerId: testPlayer1.id); + + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 1); + + await database.playerDao.deletePlayer(playerId: testPlayer2.id); + + playerCount = await database.playerDao.getPlayerCount(); + expect(playerCount, 0); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 25cbc0e..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:game_tracker/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}