Bearbeiten und Löschen von Matches #171
@@ -20,7 +20,5 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>13.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -2,12 +2,15 @@ import Flutter
|
||||
import UIKit
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
|
||||
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,27 @@
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneClassName</key>
|
||||
<string>UIWindowScene</string>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>flutter</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>FlutterSceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
45
lib/core/common.dart
Normal file
45
lib/core/common.dart
Normal file
@@ -0,0 +1,45 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/dto/match.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
|
||||
/// 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.highestScore:
|
||||
return loc.highest_score;
|
||||
case Ruleset.lowestScore:
|
||||
return loc.lowest_score;
|
||||
case Ruleset.singleWinner:
|
||||
return loc.single_winner;
|
||||
case Ruleset.singleLoser:
|
||||
return loc.single_loser;
|
||||
case Ruleset.multipleWinners:
|
||||
return loc.multiple_winners;
|
||||
}
|
||||
}
|
||||
|
||||
/// Counts how many players in the match are not part of the group
|
||||
/// Returns the count as a string, or an empty string if there is no group
|
||||
String getExtraPlayerCount(Match match) {
|
||||
int count = 0;
|
||||
|
||||
if (match.group == null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
final groupMembers = match.group!.members;
|
||||
final players = match.players;
|
||||
|
||||
for (var player in players) {
|
||||
if (!groupMembers.any((member) => member.id == player.id)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
return '';
|
||||
}
|
||||
return ' + ${count.toString()}';
|
||||
}
|
||||
@@ -27,6 +27,9 @@ class CustomTheme {
|
||||
/// Text color used throughout the app
|
||||
static const Color textColor = Color(0xFFFFFFFF);
|
||||
|
||||
/// Text color used throughout the app
|
||||
static const Color hintColor = Color(0xFF888888);
|
||||
|
||||
/// Background color for the navigation bar
|
||||
static const Color navBarBackgroundColor = Color(0xFF131313);
|
||||
|
||||
@@ -65,7 +68,7 @@ class CustomTheme {
|
||||
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
|
||||
);
|
||||
|
||||
// ==================== App Bar Theme ====================
|
||||
// ==================== Component Themes ====================
|
||||
static const AppBarTheme appBarTheme = AppBarTheme(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: textColor,
|
||||
@@ -80,4 +83,23 @@ class CustomTheme {
|
||||
),
|
||||
iconTheme: IconThemeData(color: textColor),
|
||||
);
|
||||
|
||||
static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
|
||||
textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)),
|
||||
hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)),
|
||||
);
|
||||
|
||||
static final RadioThemeData radioTheme = RadioThemeData(
|
||||
fillColor: WidgetStateProperty.resolveWith<Color>((states) {
|
||||
if (states.contains(WidgetState.selected)) {
|
||||
return CustomTheme.primaryColor;
|
||||
}
|
||||
return CustomTheme.textColor;
|
||||
}),
|
||||
);
|
||||
|
||||
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
|
||||
labelStyle: TextStyle(color: CustomTheme.textColor),
|
||||
hintStyle: TextStyle(color: CustomTheme.hintColor),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
|
||||
/// Button types used for styling the [CustomWidthButton]
|
||||
/// - [ButtonType.primary]: Primary button style.
|
||||
/// - [ButtonType.secondary]: Secondary button style.
|
||||
@@ -35,7 +32,13 @@ enum ExportResult { success, canceled, unknownException }
|
||||
/// - [Ruleset.singleWinner]: The match is won by a single player.
|
||||
/// - [Ruleset.singleLoser]: The match has a single loser.
|
||||
/// - [Ruleset.multipleWinners]: Multiple players can be winners.
|
||||
enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWinners }
|
||||
enum Ruleset {
|
||||
highestScore,
|
||||
lowestScore,
|
||||
singleWinner,
|
||||
singleLoser,
|
||||
multipleWinners,
|
||||
}
|
||||
|
||||
/// Different colors available for games
|
||||
/// - [GameColor.red]: Red color
|
||||
@@ -47,20 +50,3 @@ enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWin
|
||||
/// - [GameColor.pink]: Pink color
|
||||
/// - [GameColor.teal]: Teal color
|
||||
enum GameColor { red, blue, green, yellow, purple, orange, pink, teal }
|
||||
|
||||
/// 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.highestScore:
|
||||
return loc.highest_score;
|
||||
case Ruleset.lowestScore:
|
||||
return loc.lowest_score;
|
||||
case Ruleset.singleWinner:
|
||||
return loc.single_winner;
|
||||
case Ruleset.singleLoser:
|
||||
return loc.single_loser;
|
||||
case Ruleset.multipleWinners:
|
||||
return loc.multiple_winners;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
if (row.groupId != null) {
|
||||
group = await db.groupDao.getGroupById(groupId: row.groupId!);
|
||||
}
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(
|
||||
matchId: row.id,
|
||||
) ?? [];
|
||||
final players =
|
||||
await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? [];
|
||||
final winner = await getWinner(matchId: row.id);
|
||||
return Match(
|
||||
id: row.id,
|
||||
name: row.name ?? '',
|
||||
@@ -39,6 +39,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
notes: row.notes ?? '',
|
||||
createdAt: row.createdAt,
|
||||
endedAt: row.endedAt,
|
||||
winner: winner,
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -56,7 +57,10 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
group = await db.groupDao.getGroupById(groupId: result.groupId!);
|
||||
}
|
||||
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
final players =
|
||||
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
|
||||
final winner = await getWinner(matchId: matchId);
|
||||
|
||||
return Match(
|
||||
id: result.id,
|
||||
@@ -67,6 +71,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
notes: result.notes ?? '',
|
||||
createdAt: result.createdAt,
|
||||
endedAt: result.endedAt,
|
||||
winner: winner,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,6 +99,10 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
playerId: p.id,
|
||||
);
|
||||
}
|
||||
|
||||
if (match.winner != null) {
|
||||
await setWinner(matchId: match.id, winnerId: match.winner!.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -315,15 +324,16 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
}
|
||||
|
||||
/// Updates the group of the match with the given [matchId].
|
||||
/// Replaces the existing group association with the new group specified by [newGroupId].
|
||||
/// Pass null to remove the group association.
|
||||
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||
Future<bool> updateMatchGroup({
|
||||
required String matchId,
|
||||
required String? groupId,
|
||||
required String? newGroupId,
|
||||
}) async {
|
||||
final query = update(matchTable)..where((g) => g.id.equals(matchId));
|
||||
final rowsAffected = await query.write(
|
||||
MatchTableCompanion(groupId: Value(groupId)),
|
||||
MatchTableCompanion(groupId: Value(newGroupId)),
|
||||
);
|
||||
return rowsAffected > 0;
|
||||
}
|
||||
@@ -379,10 +389,12 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
|
||||
// Add the new players to the match
|
||||
await Future.wait(
|
||||
newPlayers.map((player) => db.playerMatchDao.addPlayerToMatch(
|
||||
newPlayers.map(
|
||||
(player) => db.playerMatchDao.addPlayerToMatch(
|
||||
matchId: matchId,
|
||||
playerId: player.id,
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -394,7 +406,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Checks if a match has a winner.
|
||||
/// Returns true if any player in the match has their score set to 1.
|
||||
Future<bool> hasWinner({required String matchId}) async {
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
final players =
|
||||
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
|
||||
for (final player in players) {
|
||||
final score = await db.playerMatchDao.getPlayerScore(
|
||||
@@ -411,7 +424,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
/// Gets the winner of a match.
|
||||
/// Returns the player with score 1, or null if no winner is set.
|
||||
Future<Player?> getWinner({required String matchId}) async {
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
final players =
|
||||
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
|
||||
for (final player in players) {
|
||||
final score = await db.playerMatchDao.getPlayerScore(
|
||||
@@ -433,7 +447,8 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
|
||||
required String winnerId,
|
||||
}) async {
|
||||
await db.transaction(() async {
|
||||
final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
final players =
|
||||
await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? [];
|
||||
|
||||
// Set all players' scores to 0
|
||||
for (final player in players) {
|
||||
|
||||
@@ -22,9 +22,14 @@
|
||||
"days_ago": "vor {count} Tagen",
|
||||
"delete": "Löschen",
|
||||
"delete_all_data": "Alle Daten löschen",
|
||||
"delete_group": "Gruppe löschen",
|
||||
"delete_group": "Diese Gruppe löschen",
|
||||
"delete_match": "Spiel löschen",
|
||||
"edit_group": "Gruppe bearbeiten",
|
||||
"edit_match": "Gruppe bearbeiten",
|
||||
"enter_results": "Ergebnisse eintragen",
|
||||
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
|
||||
"error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
|
||||
"error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen",
|
||||
"error_reading_file": "Fehler beim Lesen der Datei",
|
||||
"export_canceled": "Export abgebrochen",
|
||||
"export_data": "Daten exportieren",
|
||||
@@ -46,6 +51,7 @@
|
||||
"licenses": "Lizenzen",
|
||||
"match_in_progress": "Spiel läuft...",
|
||||
"match_name": "Spieltitel",
|
||||
"match_profile": "Spielprofil",
|
||||
"matches": "Spiele",
|
||||
"members": "Mitglieder",
|
||||
"most_points": "Höchste Punkte",
|
||||
@@ -58,6 +64,7 @@
|
||||
"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_results_entered_yet": "Noch keine Ergebnisse eingetragen",
|
||||
"no_second_match_available": "Kein zweites Spiel verfügbar",
|
||||
"no_statistics_available": "Keine Statistiken verfügbar",
|
||||
"none": "Kein",
|
||||
@@ -70,11 +77,14 @@
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"quick_create": "Schnellzugriff",
|
||||
"recent_matches": "Letzte Spiele",
|
||||
"result": "Ergebnis",
|
||||
"results": "Ergebnisse",
|
||||
"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.",
|
||||
"save_changes": "Änderungen speichern",
|
||||
"search_for_groups": "Nach Gruppen suchen",
|
||||
"search_for_players": "Nach Spieler:innen suchen",
|
||||
"select_winner": "Gewinner:in wählen:",
|
||||
|
||||
@@ -37,10 +37,10 @@
|
||||
"description": "Button text to create a match"
|
||||
},
|
||||
"@create_new_group": {
|
||||
"description": "Button text to create a new group"
|
||||
"description": "Appbar text to create a new group"
|
||||
},
|
||||
"@create_new_match": {
|
||||
"description": "Button text to create a new match"
|
||||
"description": "Appbar text to create a new match"
|
||||
},
|
||||
"@created_on": {
|
||||
"description": "Label for creation date"
|
||||
@@ -72,14 +72,29 @@
|
||||
"description": "Confirmation dialog for deleting all data"
|
||||
},
|
||||
"@delete_group": {
|
||||
"description": "Button text to delete a group"
|
||||
"description": "Confirmation dialog for deleting a group"
|
||||
},
|
||||
"@delete_match": {
|
||||
"description": "Button text to delete a match"
|
||||
},
|
||||
"@edit_group": {
|
||||
"description": "Button text to edit a group"
|
||||
"description": "Button & Appbar label for editing a group"
|
||||
},
|
||||
"@edit_match": {
|
||||
"description": "Button & Appbar label for editing a match"
|
||||
},
|
||||
"@enter_results": {
|
||||
"description": "Button text to enter match results"
|
||||
},
|
||||
"@error_creating_group": {
|
||||
"description": "Error message when group creation fails"
|
||||
},
|
||||
"@error_deleting_group": {
|
||||
"description": "Error message when group deletion fails"
|
||||
},
|
||||
"@error_editing_group": {
|
||||
"description": "Error message when group editing fails"
|
||||
},
|
||||
"@error_reading_file": {
|
||||
"description": "Error message when file cannot be read"
|
||||
},
|
||||
@@ -143,6 +158,9 @@
|
||||
"@match_name": {
|
||||
"description": "Placeholder for match name input"
|
||||
},
|
||||
"@match_profile": {
|
||||
"description": "Title for match profile view"
|
||||
},
|
||||
"@matches": {
|
||||
"description": "Label for matches"
|
||||
},
|
||||
@@ -179,6 +197,9 @@
|
||||
"@no_recent_matches_available": {
|
||||
"description": "Message when no recent matches exist"
|
||||
},
|
||||
"@no_results_entered_yet": {
|
||||
"description": "Message when no results have been entered yet"
|
||||
},
|
||||
"@no_second_match_available": {
|
||||
"description": "Message when no second match exists"
|
||||
},
|
||||
@@ -220,6 +241,9 @@
|
||||
"@recent_matches": {
|
||||
"description": "Title for recent matches section"
|
||||
},
|
||||
"@results": {
|
||||
"description": "Label for match results"
|
||||
},
|
||||
"@ruleset": {
|
||||
"description": "Ruleset label"
|
||||
},
|
||||
@@ -235,6 +259,9 @@
|
||||
"@ruleset_single_winner": {
|
||||
"description": "Description for single winner ruleset"
|
||||
},
|
||||
"@save_changes": {
|
||||
"description": "Save changes button text"
|
||||
},
|
||||
"@search_for_groups": {
|
||||
"description": "Hint text for group search input field"
|
||||
},
|
||||
@@ -321,8 +348,13 @@
|
||||
"delete": "Delete",
|
||||
"delete_all_data": "Delete all data",
|
||||
"delete_group": "Delete Group",
|
||||
"delete_match": "Delete Match",
|
||||
"edit_group": "Edit Group",
|
||||
"edit_match": "Edit Match",
|
||||
"enter_results": "Enter Results",
|
||||
"error_creating_group": "Error while creating group, please try again",
|
||||
"error_deleting_group": "Error while deleting group, please try again",
|
||||
"error_editing_group": "Error while editing group, please try again",
|
||||
"error_reading_file": "Error reading file",
|
||||
"export_canceled": "Export canceled",
|
||||
"export_data": "Export data",
|
||||
@@ -344,6 +376,7 @@
|
||||
"licenses": "Licenses",
|
||||
"match_in_progress": "Match in progress...",
|
||||
"match_name": "Match name",
|
||||
"match_profile": "Match Profile",
|
||||
"matches": "Matches",
|
||||
"members": "Members",
|
||||
"most_points": "Most Points",
|
||||
@@ -356,6 +389,7 @@
|
||||
"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_results_entered_yet": "No results entered yet",
|
||||
"no_second_match_available": "No second match available",
|
||||
"no_statistics_available": "No statistics available",
|
||||
"none": "None",
|
||||
@@ -368,11 +402,13 @@
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"quick_create": "Quick Create",
|
||||
"recent_matches": "Recent Matches",
|
||||
"results": "Results",
|
||||
"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.",
|
||||
"save_changes": "Save Changes",
|
||||
"search_for_groups": "Search for groups",
|
||||
"search_for_players": "Search for players",
|
||||
"select_winner": "Select Winner:",
|
||||
|
||||
@@ -170,7 +170,7 @@ abstract class AppLocalizations {
|
||||
/// **'Create match'**
|
||||
String get create_match;
|
||||
|
||||
/// Button text to create a new group
|
||||
/// Appbar text to create a new group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create new group'**
|
||||
@@ -182,7 +182,7 @@ abstract class AppLocalizations {
|
||||
/// **'Created on'**
|
||||
String get created_on;
|
||||
|
||||
/// Button text to create a new match
|
||||
/// Appbar text to create a new match
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Create new match'**
|
||||
@@ -230,24 +230,54 @@ abstract class AppLocalizations {
|
||||
/// **'Delete all data'**
|
||||
String get delete_all_data;
|
||||
|
||||
/// Button text to delete a group
|
||||
/// Confirmation dialog for deleting a group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Group'**
|
||||
String get delete_group;
|
||||
|
||||
/// Button text to edit a group
|
||||
/// Button text to delete a match
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Match'**
|
||||
String get delete_match;
|
||||
|
||||
/// Button & Appbar label for editing a group
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Group'**
|
||||
String get edit_group;
|
||||
|
||||
/// Button & Appbar label for editing a match
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit Match'**
|
||||
String get edit_match;
|
||||
|
||||
/// Button text to enter match results
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter Results'**
|
||||
String get enter_results;
|
||||
|
||||
/// 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 group deletion fails
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Error while deleting group, please try again'**
|
||||
String get error_deleting_group;
|
||||
|
||||
/// Error message when group editing fails
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Error while editing group, please try again'**
|
||||
String get error_editing_group;
|
||||
|
||||
/// Error message when file cannot be read
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -374,6 +404,12 @@ abstract class AppLocalizations {
|
||||
/// **'Match name'**
|
||||
String get match_name;
|
||||
|
||||
/// Title for match profile view
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Match Profile'**
|
||||
String get match_profile;
|
||||
|
||||
/// Label for matches
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -446,6 +482,12 @@ abstract class AppLocalizations {
|
||||
/// **'No recent matches available'**
|
||||
String get no_recent_matches_available;
|
||||
|
||||
/// Message when no results have been entered yet
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No results entered yet'**
|
||||
String get no_results_entered_yet;
|
||||
|
||||
/// Message when no second match exists
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -518,6 +560,12 @@ abstract class AppLocalizations {
|
||||
/// **'Recent Matches'**
|
||||
String get recent_matches;
|
||||
|
||||
/// Label for match results
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Results'**
|
||||
String get results;
|
||||
|
||||
/// Ruleset label
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -548,6 +596,12 @@ abstract class AppLocalizations {
|
||||
/// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'**
|
||||
String get ruleset_single_winner;
|
||||
|
||||
/// Save changes button text
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save Changes'**
|
||||
String get save_changes;
|
||||
|
||||
/// Hint text for group search input field
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -79,15 +79,32 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get delete_all_data => 'Alle Daten löschen';
|
||||
|
||||
@override
|
||||
String get delete_group => 'Gruppe löschen';
|
||||
String get delete_group => 'Diese Gruppe löschen';
|
||||
|
||||
@override
|
||||
String get delete_match => 'Spiel löschen';
|
||||
|
||||
@override
|
||||
String get edit_group => 'Gruppe bearbeiten';
|
||||
|
||||
@override
|
||||
String get edit_match => 'Gruppe bearbeiten';
|
||||
|
||||
@override
|
||||
String get enter_results => 'Ergebnisse eintragen';
|
||||
|
||||
@override
|
||||
String get error_creating_group =>
|
||||
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
|
||||
|
||||
@override
|
||||
String get error_deleting_group =>
|
||||
'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
|
||||
|
||||
@override
|
||||
String get error_editing_group =>
|
||||
'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen';
|
||||
|
||||
@override
|
||||
String get error_reading_file => 'Fehler beim Lesen der Datei';
|
||||
|
||||
@@ -151,6 +168,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get match_name => 'Spieltitel';
|
||||
|
||||
@override
|
||||
String get match_profile => 'Spielprofil';
|
||||
|
||||
@override
|
||||
String get matches => 'Spiele';
|
||||
|
||||
@@ -188,6 +208,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get no_recent_matches_available => 'Keine letzten Spiele verfügbar';
|
||||
|
||||
@override
|
||||
String get no_results_entered_yet => 'Noch keine Ergebnisse eingetragen';
|
||||
|
||||
@override
|
||||
String get no_second_match_available => 'Kein zweites Spiel verfügbar';
|
||||
|
||||
@@ -226,6 +249,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get recent_matches => 'Letzte Spiele';
|
||||
|
||||
@override
|
||||
String get results => 'Ergebnisse';
|
||||
|
||||
@override
|
||||
String get ruleset => 'Regelwerk';
|
||||
|
||||
@@ -245,6 +271,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
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 save_changes => 'Änderungen speichern';
|
||||
|
||||
@override
|
||||
String get search_for_groups => 'Nach Gruppen suchen';
|
||||
|
||||
|
||||
@@ -81,13 +81,30 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get delete_group => 'Delete Group';
|
||||
|
||||
@override
|
||||
String get delete_match => 'Delete Match';
|
||||
|
||||
@override
|
||||
String get edit_group => 'Edit Group';
|
||||
|
||||
@override
|
||||
String get edit_match => 'Edit Match';
|
||||
|
||||
@override
|
||||
String get enter_results => 'Enter Results';
|
||||
|
||||
@override
|
||||
String get error_creating_group =>
|
||||
'Error while creating group, please try again';
|
||||
|
||||
@override
|
||||
String get error_deleting_group =>
|
||||
'Error while deleting group, please try again';
|
||||
|
||||
@override
|
||||
String get error_editing_group =>
|
||||
'Error while editing group, please try again';
|
||||
|
||||
@override
|
||||
String get error_reading_file => 'Error reading file';
|
||||
|
||||
@@ -151,6 +168,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get match_name => 'Match name';
|
||||
|
||||
@override
|
||||
String get match_profile => 'Match Profile';
|
||||
|
||||
@override
|
||||
String get matches => 'Matches';
|
||||
|
||||
@@ -188,6 +208,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get no_recent_matches_available => 'No recent matches available';
|
||||
|
||||
@override
|
||||
String get no_results_entered_yet => 'No results entered yet';
|
||||
|
||||
@override
|
||||
String get no_second_match_available => 'No second match available';
|
||||
|
||||
@@ -226,6 +249,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get recent_matches => 'Recent Matches';
|
||||
|
||||
@override
|
||||
String get results => 'Results';
|
||||
|
||||
@override
|
||||
String get ruleset => 'Ruleset';
|
||||
|
||||
@@ -245,6 +271,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get ruleset_single_winner =>
|
||||
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.';
|
||||
|
||||
@override
|
||||
String get save_changes => 'Save Changes';
|
||||
|
||||
@override
|
||||
String get search_for_groups => 'Search for groups';
|
||||
|
||||
|
||||
@@ -29,19 +29,31 @@ class GameTracker extends StatelessWidget {
|
||||
return supportedLocale;
|
||||
}
|
||||
}
|
||||
return supportedLocales.firstWhere((locale) => locale.languageCode == 'en');
|
||||
return supportedLocales.firstWhere(
|
||||
(locale) => locale.languageCode == 'en',
|
||||
);
|
||||
},
|
||||
debugShowCheckedModeBanner: false,
|
||||
onGenerateTitle: (context) => AppLocalizations.of(context).app_name,
|
||||
themeMode: ThemeMode.dark, // forces dark mode
|
||||
themeMode: ThemeMode.dark,
|
||||
theme: ThemeData(
|
||||
// main colors
|
||||
primaryColor: CustomTheme.primaryColor,
|
||||
scaffoldBackgroundColor: CustomTheme.backgroundColor,
|
||||
// themes
|
||||
appBarTheme: CustomTheme.appBarTheme,
|
||||
inputDecorationTheme: CustomTheme.inputDecorationTheme,
|
||||
searchBarTheme: CustomTheme.searchBarTheme,
|
||||
radioTheme: CustomTheme.radioTheme,
|
||||
// color scheme
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: CustomTheme.primaryColor,
|
||||
seedColor: CustomTheme.textColor,
|
||||
brightness: Brightness.dark,
|
||||
).copyWith(surface: CustomTheme.backgroundColor),
|
||||
primary: CustomTheme.primaryColor,
|
||||
onPrimary: CustomTheme.textColor,
|
||||
surface: CustomTheme.backgroundColor,
|
||||
onSurface: CustomTheme.textColor,
|
||||
),
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: {
|
||||
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/groups_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/home_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
|
||||
@@ -38,7 +38,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('groups_$tabKeyCount'),
|
||||
child: const GroupsView(),
|
||||
child: const GroupView(),
|
||||
),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('stats_$tabKeyCount'),
|
||||
@@ -141,7 +141,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
}
|
||||
|
||||
/// Returns the title of the current tab based on [currentIndex].
|
||||
String _currentTabTitle(context) {
|
||||
String _currentTabTitle(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
switch (currentIndex) {
|
||||
case 0:
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/constants.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
@@ -12,8 +11,10 @@ import 'package:tallee/presentation/widgets/player_selection.dart';
|
||||
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
|
||||
|
||||
class CreateGroupView extends StatefulWidget {
|
||||
/// A view that allows the user to create a new group
|
||||
const CreateGroupView({super.key});
|
||||
const CreateGroupView({super.key, this.groupToEdit});
|
||||
|
||||
/// The group to edit, if any
|
||||
final Group? groupToEdit;
|
||||
|
||||
@override
|
||||
State<CreateGroupView> createState() => _CreateGroupViewState();
|
||||
@@ -22,16 +23,29 @@ class CreateGroupView extends StatefulWidget {
|
||||
class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
/// GlobalKey for ScaffoldMessenger to show snackbars
|
||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
/// Controller for the group name input field
|
||||
final _groupNameController = TextEditingController();
|
||||
|
||||
/// List of currently selected players
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
/// List of initially selected players (when editing a group)
|
||||
List<Player> initialSelectedPlayers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
if (widget.groupToEdit != null) {
|
||||
_groupNameController.text = widget.groupToEdit!.name;
|
||||
setState(() {
|
||||
initialSelectedPlayers = widget.groupToEdit!.members;
|
||||
selectedPlayers = widget.groupToEdit!.members;
|
||||
});
|
||||
}
|
||||
_groupNameController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
@@ -47,10 +61,58 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
return ScaffoldMessenger(
|
||||
key: _scaffoldMessengerKey,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(title: Text(loc.create_new_group)),
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
widget.groupToEdit == null ? loc.create_new_group : loc.edit_group,
|
||||
),
|
||||
actions: widget.groupToEdit == null
|
||||
? []
|
||||
: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
if (widget.groupToEdit != null) {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(loc.delete_group),
|
||||
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) async {
|
||||
if (confirmed == true && context.mounted) {
|
||||
bool success = await db.groupDao.deleteGroup(
|
||||
groupId: widget.groupToEdit!.id,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
if (!mounted) return;
|
||||
showSnackbar(message: loc.error_deleting_group);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -60,11 +122,11 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
child: TextInputField(
|
||||
controller: _groupNameController,
|
||||
hintText: loc.group_name,
|
||||
maxLength: Constants.MAX_GROUP_NAME_LENGTH,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PlayerSelection(
|
||||
initialSelectedPlayers: initialSelectedPlayers,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = [...value];
|
||||
@@ -73,7 +135,9 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: loc.create_group,
|
||||
text: widget.groupToEdit == null
|
||||
? loc.create_group
|
||||
: loc.edit_group,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed:
|
||||
@@ -81,29 +145,38 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
(selectedPlayers.length < 2))
|
||||
? null
|
||||
: () async {
|
||||
bool success = await db.groupDao.addGroup(
|
||||
late Group? updatedGroup;
|
||||
late bool success;
|
||||
if (widget.groupToEdit == null) {
|
||||
success = await db.groupDao.addGroup(
|
||||
group: Group(
|
||||
name: _groupNameController.text.trim(),
|
||||
description: '',
|
||||
members: selectedPlayers,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
updatedGroup = Group(
|
||||
id: widget.groupToEdit!.id,
|
||||
name: _groupNameController.text.trim(),
|
||||
description: '',
|
||||
members: selectedPlayers,
|
||||
);
|
||||
//TODO: Implement group editing in database
|
||||
/*
|
||||
success = await db.groupDao.updateGroup(
|
||||
group: updatedGroup,
|
||||
);
|
||||
*/
|
||||
success = true;
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
Navigator.pop(context);
|
||||
Navigator.pop(context, updatedGroup);
|
||||
} 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
showSnackbar(
|
||||
message: widget.groupToEdit == null
|
||||
? loc.error_creating_group
|
||||
: loc.error_editing_group,
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -115,4 +188,20 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Displays a snackbar with the given message and optional action.
|
||||
///
|
||||
/// [message] The message to display in the snackbar.
|
||||
void showSnackbar({required String message}) {
|
||||
final messenger = _scaffoldMessengerKey.currentState;
|
||||
if (messenger != null) {
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message, style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/dto/group.dart';
|
||||
import 'package:tallee/data/dto/match.dart';
|
||||
import 'package:tallee/data/dto/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
@@ -15,10 +17,10 @@ import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class GroupProfileView extends StatefulWidget {
|
||||
class GroupDetailView extends StatefulWidget {
|
||||
/// A view that displays the profile of a group
|
||||
/// - [group]: The group to display
|
||||
const GroupProfileView({
|
||||
const GroupDetailView({
|
||||
super.key,
|
||||
required this.group,
|
||||
required this.callback,
|
||||
@@ -30,21 +32,24 @@ class GroupProfileView extends StatefulWidget {
|
||||
final VoidCallback callback;
|
||||
|
||||
@override
|
||||
State<GroupProfileView> createState() => _GroupProfileViewState();
|
||||
State<GroupDetailView> createState() => _GroupDetailViewState();
|
||||
}
|
||||
|
||||
class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
class _GroupDetailViewState extends State<GroupDetailView> {
|
||||
late final AppDatabase db;
|
||||
bool isLoading = true;
|
||||
late Group _group;
|
||||
|
||||
/// Total matches played in this group
|
||||
int totalMatches = 0;
|
||||
|
||||
/// The best player in this group
|
||||
String bestPlayer = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_group = widget.group;
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_loadStatistics();
|
||||
}
|
||||
@@ -87,7 +92,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
await db.groupDao.deleteGroup(groupId: widget.group.id);
|
||||
await db.groupDao.deleteGroup(groupId: _group.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
widget.callback.call();
|
||||
@@ -118,7 +123,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
widget.group.name,
|
||||
_group.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -128,7 +133,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.group.createdAt)}',
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_group.createdAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CustomTheme.textColor,
|
||||
@@ -145,7 +150,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: widget.group.members.map((member) {
|
||||
children: _group.members.map((member) {
|
||||
return TextIconTile(
|
||||
text: member.name,
|
||||
iconEnabled: false,
|
||||
@@ -163,7 +168,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
children: [
|
||||
_buildStatRow(
|
||||
loc.members,
|
||||
widget.group.members.length.toString(),
|
||||
_group.members.length.toString(),
|
||||
),
|
||||
_buildStatRow(
|
||||
loc.played_matches,
|
||||
@@ -181,19 +186,22 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
child: MainMenuButton(
|
||||
text: loc.edit_group,
|
||||
icon: Icons.edit,
|
||||
onPressed: () {
|
||||
// TODO: Uncomment when GroupDetailView is implemented
|
||||
/*
|
||||
await Navigator.push(
|
||||
onPressed: () async {
|
||||
final updatedGroup = await Navigator.push<Group?>(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
|
||||
return const GroupDetailView();
|
||||
return CreateGroupView(groupToEdit: _group);
|
||||
},
|
||||
),
|
||||
);*/
|
||||
print('Edit Group pressed');
|
||||
);
|
||||
if (updatedGroup != null && mounted) {
|
||||
setState(() {
|
||||
_group = updatedGroup;
|
||||
});
|
||||
_loadStatistics();
|
||||
widget.callback();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -236,7 +244,7 @@ class _GroupProfileViewState extends State<GroupProfileView> {
|
||||
Future<void> _loadStatistics() async {
|
||||
final matches = await db.matchDao.getAllMatches();
|
||||
final groupMatches = matches
|
||||
.where((match) => match.group?.id == widget.group.id)
|
||||
.where((match) => match.group?.id == _group.id)
|
||||
.toList();
|
||||
|
||||
setState(() {
|
||||
@@ -8,21 +8,21 @@ import 'package:tallee/data/dto/group.dart';
|
||||
import 'package:tallee/data/dto/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/group_profile_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/group_view/group_detail_view.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/group_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/top_centered_message.dart';
|
||||
|
||||
class GroupsView extends StatefulWidget {
|
||||
class GroupView extends StatefulWidget {
|
||||
/// A view that displays a list of groups
|
||||
const GroupsView({super.key});
|
||||
const GroupView({super.key});
|
||||
|
||||
@override
|
||||
State<GroupsView> createState() => _GroupsViewState();
|
||||
State<GroupView> createState() => _GroupViewState();
|
||||
}
|
||||
|
||||
class _GroupsViewState extends State<GroupsView> {
|
||||
class _GroupViewState extends State<GroupView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
/// Loaded groups from the database
|
||||
@@ -83,7 +83,7 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) {
|
||||
return GroupProfileView(
|
||||
return GroupDetailView(
|
||||
group: groups[index],
|
||||
callback: loadGroups,
|
||||
);
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
|
||||
@@ -21,11 +21,22 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart';
|
||||
class CreateMatchView extends StatefulWidget {
|
||||
/// A view that allows creating a new match
|
||||
/// [onWinnerChanged]: Optional callback invoked when the winner is changed
|
||||
const CreateMatchView({super.key, this.onWinnerChanged});
|
||||
const CreateMatchView({
|
||||
super.key,
|
||||
this.onWinnerChanged,
|
||||
this.matchToEdit,
|
||||
this.onMatchUpdated,
|
||||
});
|
||||
|
||||
/// Optional callback invoked when the winner is changed
|
||||
final VoidCallback? onWinnerChanged;
|
||||
|
||||
/// Optional callback invoked when the match is updated
|
||||
final void Function(Match)? onMatchUpdated;
|
||||
|
||||
/// An optional match to prefill the fields
|
||||
final Match? matchToEdit;
|
||||
|
||||
@override
|
||||
State<CreateMatchView> createState() => _CreateMatchViewState();
|
||||
}
|
||||
@@ -45,19 +56,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// List of all players from the database
|
||||
List<Player> 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<Player> 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 index of the currently selected game in [games] to mark it in
|
||||
/// the [ChooseGameView]
|
||||
int selectedGameIndex = -1;
|
||||
@@ -65,6 +66,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// The currently selected players
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
/// GlobalKey for ScaffoldMessenger to show snackbars
|
||||
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -80,9 +84,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
]).then((result) async {
|
||||
groupsList = result[0] as List<Group>;
|
||||
playerList = result[1] as List<Player>;
|
||||
setState(() {
|
||||
filteredPlayerList = List.from(playerList);
|
||||
});
|
||||
|
||||
// If a match is provided, prefill the fields
|
||||
if (widget.matchToEdit != null) {
|
||||
prefillMatchDetails();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,11 +113,19 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
final buttonText = widget.matchToEdit != null
|
||||
? loc.save_changes
|
||||
: loc.create_match;
|
||||
final viewTitle = widget.matchToEdit != null
|
||||
? loc.edit_match
|
||||
: loc.create_new_match;
|
||||
|
||||
return ScaffoldMessenger(
|
||||
key: _scaffoldMessengerKey,
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(title: Text(loc.create_new_match)),
|
||||
appBar: AppBar(title: Text(viewTitle)),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
@@ -153,95 +167,54 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
? loc.none_group
|
||||
: selectedGroup!.name,
|
||||
onPressed: () async {
|
||||
// Remove all players from the previously selected group from
|
||||
// the selected players list, in case the user deselects the
|
||||
// group or selects a different group.
|
||||
selectedPlayers.removeWhere(
|
||||
(player) =>
|
||||
selectedGroup?.members.any(
|
||||
(member) => member.id == player.id,
|
||||
) ??
|
||||
false,
|
||||
);
|
||||
|
||||
selectedGroup = await Navigator.of(context).push(
|
||||
adaptivePageRoute(
|
||||
builder: (context) => ChooseGroupView(
|
||||
groups: groupsList,
|
||||
initialGroupId: selectedGroupId,
|
||||
initialGroupId: selectedGroup?.id ?? '',
|
||||
),
|
||||
),
|
||||
);
|
||||
selectedGroupId = selectedGroup?.id ?? '';
|
||||
|
||||
setState(() {
|
||||
if (selectedGroup != null) {
|
||||
filteredPlayerList = playerList
|
||||
.where(
|
||||
(p) =>
|
||||
!selectedGroup!.members.any((m) => m.id == p.id),
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
filteredPlayerList = List.from(playerList);
|
||||
setState(() {
|
||||
selectedPlayers += [...selectedGroup!.members];
|
||||
});
|
||||
}
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: PlayerSelection(
|
||||
key: ValueKey(selectedGroup?.id ?? 'no_group'),
|
||||
initialSelectedPlayers: selectedPlayers,
|
||||
availablePlayers: filteredPlayerList,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
selectedPlayers = value;
|
||||
removeGroupWhenNoMemberLeft();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: loc.create_match,
|
||||
text: buttonText,
|
||||
sizeRelativeToWidth: 0.95,
|
||||
buttonType: ButtonType.primary,
|
||||
onPressed: _enableCreateGameButton()
|
||||
? () async {
|
||||
// Use a game from the games list
|
||||
Game? gameToUse;
|
||||
if (selectedGameIndex == -1) {
|
||||
// Use the first game as default if none selected
|
||||
final selectedGame = games[0];
|
||||
gameToUse = Game(
|
||||
name: selectedGame.$1,
|
||||
description: selectedGame.$2,
|
||||
ruleset: selectedGame.$3,
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
);
|
||||
} else {
|
||||
// Use the selected game from the list
|
||||
final selectedGame = games[selectedGameIndex];
|
||||
gameToUse = Game(
|
||||
name: selectedGame.$1,
|
||||
description: selectedGame.$2,
|
||||
ruleset: selectedGame.$3,
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
);
|
||||
}
|
||||
// Add the game to the database if it doesn't exist
|
||||
await db.gameDao.addGame(game: gameToUse);
|
||||
|
||||
Match match = Match(
|
||||
name: _matchNameController.text.isEmpty
|
||||
? (hintText ?? '')
|
||||
: _matchNameController.text.trim(),
|
||||
createdAt: DateTime.now(),
|
||||
game: gameToUse,
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
notes: '',
|
||||
);
|
||||
await db.matchDao.addMatch(match: match);
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
? () {
|
||||
buttonNavigation(context);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -259,6 +232,155 @@ class _CreateMatchViewState extends State<CreateMatchView> {
|
||||
/// - Either a group is selected OR at least 2 players are selected
|
||||
bool _enableCreateGameButton() {
|
||||
return (selectedGroup != null ||
|
||||
(selectedPlayers.length > 1));
|
||||
(selectedPlayers.length > 1) && selectedGameIndex != -1);
|
||||
}
|
||||
|
||||
// If a match was provided to the view, it updates the match in the database
|
||||
// and navigates back to the previous screen.
|
||||
// If no match was provided, it creates a new match in the database and
|
||||
|
flixcoo marked this conversation as resolved
|
||||
// navigates to the MatchResultView for the newly created match.
|
||||
void buttonNavigation(BuildContext context) async {
|
||||
if (widget.matchToEdit != null) {
|
||||
await updateMatch();
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
} else {
|
||||
final match = await createMatch();
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: widget.onWinnerChanged,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates attributes of the existing match in the database based on the
|
||||
/// changes made in the edit view.
|
||||
Future<void> updateMatch() async {
|
||||
//TODO: Remove when Games implemented
|
||||
final tempGame = await getTemporaryGame();
|
||||
|
||||
final updatedMatch = Match(
|
||||
id: widget.matchToEdit!.id,
|
||||
name: _matchNameController.text.isEmpty
|
||||
? (hintText ?? '')
|
||||
: _matchNameController.text.trim(),
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
game: tempGame,
|
||||
winner: widget.matchToEdit!.winner,
|
||||
createdAt: widget.matchToEdit!.createdAt,
|
||||
endedAt: widget.matchToEdit!.endedAt,
|
||||
notes: widget.matchToEdit!.notes,
|
||||
);
|
||||
|
||||
if (widget.matchToEdit!.name != updatedMatch.name) {
|
||||
await db.matchDao.updateMatchName(
|
||||
matchId: widget.matchToEdit!.id,
|
||||
newName: updatedMatch.name,
|
||||
);
|
||||
}
|
||||
|
||||
if (widget.matchToEdit!.group?.id != updatedMatch.group?.id) {
|
||||
await db.matchDao.updateMatchGroup(
|
||||
matchId: widget.matchToEdit!.id,
|
||||
newGroupId: updatedMatch.group?.id,
|
||||
);
|
||||
}
|
||||
|
||||
// Add players who are in updatedMatch but not in the original match
|
||||
for (var player in updatedMatch.players) {
|
||||
if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) {
|
||||
await db.playerMatchDao.addPlayerToMatch(
|
||||
matchId: widget.matchToEdit!.id,
|
||||
playerId: player.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove players who are in the original match but not in updatedMatch
|
||||
for (var player in widget.matchToEdit!.players) {
|
||||
if (!updatedMatch.players.any((p) => p.id == player.id)) {
|
||||
await db.playerMatchDao.removePlayerFromMatch(
|
||||
matchId: widget.matchToEdit!.id,
|
||||
playerId: player.id,
|
||||
);
|
||||
if (widget.matchToEdit!.winner?.id == player.id) {
|
||||
updatedMatch.winner = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
widget.onMatchUpdated?.call(updatedMatch);
|
||||
}
|
||||
|
||||
// Creates a new match and adds it to the database.
|
||||
// Returns the created match.
|
||||
Future<Match> createMatch() async {
|
||||
final tempGame = await getTemporaryGame();
|
||||
|
||||
Match match = Match(
|
||||
name: _matchNameController.text.isEmpty
|
||||
? (hintText ?? '')
|
||||
: _matchNameController.text.trim(),
|
||||
createdAt: DateTime.now(),
|
||||
group: selectedGroup,
|
||||
players: selectedPlayers,
|
||||
game: tempGame,
|
||||
);
|
||||
await db.matchDao.addMatch(match: match);
|
||||
return match;
|
||||
}
|
||||
|
||||
// TODO: Remove when games fully implemented
|
||||
Future<Game> getTemporaryGame() async {
|
||||
Game? game;
|
||||
|
||||
final selectedGame = games[selectedGameIndex];
|
||||
game = Game(
|
||||
name: selectedGame.$1,
|
||||
description: selectedGame.$2,
|
||||
ruleset: selectedGame.$3,
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
);
|
||||
|
||||
await db.gameDao.addGame(game: game);
|
||||
return game;
|
||||
}
|
||||
|
||||
// If a match was provided to the view, this method prefills the input fields
|
||||
void prefillMatchDetails() {
|
||||
final match = widget.matchToEdit!;
|
||||
_matchNameController.text = match.name;
|
||||
selectedPlayers = match.players;
|
||||
|
||||
if (match.group != null) {
|
||||
selectedGroup = match.group;
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the selected players are from the currently selected group,
|
||||
// the group is also deselected.
|
||||
Future<void> removeGroupWhenNoMemberLeft() async {
|
||||
if (selectedGroup == null) return;
|
||||
|
||||
if (!selectedPlayers.any(
|
||||
(player) =>
|
||||
selectedGroup!.members.any((member) => member.id == player.id),
|
||||
)) {
|
||||
setState(() {
|
||||
selectedGroup = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tallee/core/adaptive_page_route.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/dto/match.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
|
||||
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
class MatchDetailView extends StatefulWidget {
|
||||
/// A view that displays the profile of a match
|
||||
/// - [match]: The match to display
|
||||
/// - [onMatchUpdate]: Callback to refresh the match list
|
||||
const MatchDetailView({
|
||||
super.key,
|
||||
required this.match,
|
||||
required this.onMatchUpdate,
|
||||
});
|
||||
|
||||
/// The match to display
|
||||
final Match match;
|
||||
|
||||
/// Callback to refresh the match list
|
||||
final VoidCallback onMatchUpdate;
|
||||
|
||||
@override
|
||||
State<MatchDetailView> createState() => _MatchDetailViewState();
|
||||
}
|
||||
|
||||
class _MatchDetailViewState extends State<MatchDetailView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
late Match match;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
match = widget.match;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
title: Text(loc.match_profile),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
onPressed: () async {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => CustomAlertDialog(
|
||||
title: '${loc.delete_match}?',
|
||||
content: loc.this_cannot_be_undone,
|
||||
actions: [
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: Text(
|
||||
loc.cancel,
|
||||
style: const TextStyle(color: CustomTheme.textColor),
|
||||
),
|
||||
),
|
||||
AnimatedDialogButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: Text(
|
||||
loc.delete,
|
||||
style: const TextStyle(
|
||||
color: CustomTheme.secondaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) async {
|
||||
if (confirmed! && context.mounted) {
|
||||
await db.matchDao.deleteMatch(matchId: match.id);
|
||||
if (!context.mounted) return;
|
||||
Navigator.pop(context);
|
||||
widget.onMatchUpdate.call();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ListView(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
top: 20,
|
||||
bottom: 100,
|
||||
),
|
||||
children: [
|
||||
const Center(
|
||||
child: ColoredIconContainer(
|
||||
icon: Icons.sports_esports,
|
||||
containerSize: 55,
|
||||
iconSize: 38,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
match.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
Text(
|
||||
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
if (match.group != null) ...[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.group),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${match.group!.name}${getExtraPlayerCount(match)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
InfoTile(
|
||||
title: loc.players,
|
||||
icon: Icons.people,
|
||||
horizontalAlignment: CrossAxisAlignment.start,
|
||||
content: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 12,
|
||||
runSpacing: 8,
|
||||
children: match.players.map((player) {
|
||||
return TextIconTile(
|
||||
text: player.name,
|
||||
iconEnabled: false,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
InfoTile(
|
||||
title: loc.results,
|
||||
icon: Icons.emoji_events,
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
/// TODO: Implement different ruleset results display
|
||||
if (match.winner != null) ...[
|
||||
Text(
|
||||
loc.winner,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
match.winner!.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Text(
|
||||
loc.no_results_entered_yet,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: CustomTheme.textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: Row(
|
||||
children: [
|
||||
MainMenuButton(
|
||||
icon: Icons.edit,
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => CreateMatchView(
|
||||
matchToEdit: match,
|
||||
onMatchUpdated: onMatchUpdated,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 15),
|
||||
MainMenuButton(
|
||||
text: loc.enter_results,
|
||||
icon: Icons.emoji_events,
|
||||
onPressed: () async {
|
||||
match.winner = await Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => MatchResultView(
|
||||
match: match,
|
||||
onWinnerChanged: () {
|
||||
widget.onMatchUpdate.call();
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Callback for when the match is updated in the edit view,
|
||||
/// updates the match in this view
|
||||
void onMatchUpdated(Match editedMatch) {
|
||||
setState(() {
|
||||
match = editedMatch;
|
||||
});
|
||||
widget.onMatchUpdate.call();
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,10 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
@override
|
||||
void initState() {
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
allPlayers = getAllPlayers(widget.match);
|
||||
|
||||
allPlayers = widget.match.players;
|
||||
allPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
|
||||
if (widget.match.winner != null) {
|
||||
_selectedPlayer = allPlayers.firstWhere(
|
||||
(p) => p.id == widget.match.winner!.id,
|
||||
@@ -54,7 +57,7 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () {
|
||||
widget.onWinnerChanged?.call();
|
||||
Navigator.of(context).pop();
|
||||
Navigator.of(context).pop(_selectedPlayer);
|
||||
},
|
||||
),
|
||||
title: Text(widget.match.name),
|
||||
@@ -145,21 +148,4 @@ class _MatchResultViewState extends State<MatchResultView> {
|
||||
}
|
||||
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<Player> getAllPlayers(Match match) {
|
||||
List<Player> players = [];
|
||||
|
||||
if (match.group == null) {
|
||||
players = [...match.players];
|
||||
} else {
|
||||
players = [...match.players, ...match.group!.members];
|
||||
}
|
||||
|
||||
players.sort((a, b) => a.name.compareTo(b.name));
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:core' hide Match;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttericon/rpg_awesome_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -14,7 +12,7 @@ import 'package:tallee/data/dto/match.dart';
|
||||
import 'package:tallee/data/dto/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
|
||||
import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart';
|
||||
import 'package:tallee/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/match_tile.dart';
|
||||
@@ -38,7 +36,13 @@ class _MatchViewState extends State<MatchView> {
|
||||
4,
|
||||
Match(
|
||||
name: 'Skeleton match name',
|
||||
game: Game(name: '', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''),
|
||||
game: Game(
|
||||
name: '',
|
||||
ruleset: Ruleset.singleWinner,
|
||||
description: '',
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
),
|
||||
group: Group(
|
||||
name: 'Group name',
|
||||
description: '',
|
||||
@@ -54,7 +58,7 @@ class _MatchViewState extends State<MatchView> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
loadGames();
|
||||
loadMatches();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -94,10 +98,9 @@ class _MatchViewState extends State<MatchView> {
|
||||
Navigator.push(
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => MatchResultView(
|
||||
builder: (context) => MatchDetailView(
|
||||
match: matches[index],
|
||||
onWinnerChanged: loadGames,
|
||||
onMatchUpdate: loadMatches,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -120,7 +123,7 @@ class _MatchViewState extends State<MatchView> {
|
||||
context,
|
||||
adaptivePageRoute(
|
||||
builder: (context) =>
|
||||
CreateMatchView(onWinnerChanged: loadGames),
|
||||
CreateMatchView(onWinnerChanged: loadMatches),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -131,8 +134,9 @@ class _MatchViewState extends State<MatchView> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Loads the games from the database and sorts them by creation date.
|
||||
void loadGames() {
|
||||
/// Loads the matches from the database and sorts them by creation date.
|
||||
void loadMatches() {
|
||||
isLoading = true;
|
||||
Future.wait([
|
||||
db.matchDao.getAllMatches(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
@@ -140,7 +144,7 @@ class _MatchViewState extends State<MatchView> {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
final loadedMatches = results[0] as List<Match>;
|
||||
matches = loadedMatches
|
||||
matches = [...loadedMatches]
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
isLoading = false;
|
||||
});
|
||||
|
||||
@@ -31,6 +31,7 @@ class _SettingsViewState extends State<SettingsView> {
|
||||
version: 'n.A.',
|
||||
buildNumber: 'n.A.',
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
@@ -47,10 +47,7 @@ class _AppSkeletonState extends State<AppSkeleton> {
|
||||
: (Widget? currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
children: [...previousChildren, ?currentChild],
|
||||
|
flixcoo marked this conversation as resolved
sneeex
commented
was macht das? was macht das?
flixcoo
commented
Das ist ein Null-aware Spread Operator, d.h. das Element Siehe Discord-Nachricht bzw. Dart Documentation Das ist ein Null-aware Spread Operator, d.h. das Element `currentChild` wird nur in die Liste eingefügt, wenn es nicht `null` ist. Der Ausdruck ist Äquivalent zu
```dart
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
```
Siehe [Discord-Nachricht](https://discord.com/channels/921763177245605888/922566373455048875/1479868438812950661) bzw. [Dart Documentation](https://dart.dev/language/collections#null-aware-element)
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,25 +2,25 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class MainMenuButton extends StatefulWidget {
|
||||
/// A button for the main menu with an optional icon and a press animation.
|
||||
/// - [text]: The text of the button.
|
||||
/// - [icon]: The icon of the button.
|
||||
/// - [onPressed]: The callback to be invoked when the button is pressed.
|
||||
/// - [icon]: The icon of the button.
|
||||
/// - [text]: The text of the button.
|
||||
const MainMenuButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
this.icon,
|
||||
required this.onPressed,
|
||||
required this.icon,
|
||||
this.text,
|
||||
});
|
||||
|
||||
/// The text of the button.
|
||||
final String text;
|
||||
|
||||
/// The icon of the button.
|
||||
final IconData? icon;
|
||||
|
||||
/// The callback to be invoked when the button is pressed.
|
||||
final void Function() onPressed;
|
||||
|
||||
/// The icon of the button.
|
||||
final IconData icon;
|
||||
|
||||
/// The text of the button.
|
||||
final String? text;
|
||||
|
||||
@override
|
||||
State<MainMenuButton> createState() => _MainMenuButtonState();
|
||||
}
|
||||
@@ -71,12 +71,11 @@ class _MainMenuButtonState extends State<MainMenuButton>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (widget.icon != null) ...[
|
||||
Icon(widget.icon, size: 26, color: Colors.black),
|
||||
if (widget.text != null) ...[
|
||||
const SizedBox(width: 7),
|
||||
],
|
||||
Text(
|
||||
widget.text,
|
||||
widget.text!,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -84,6 +83,7 @@ class _MainMenuButtonState extends State<MainMenuButton>
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -70,6 +70,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
suggestedPlayers = skeletonData;
|
||||
selectedPlayers = widget.initialSelectedPlayers ?? [];
|
||||
loadPlayerList();
|
||||
}
|
||||
|
||||
@@ -100,7 +101,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
if (value.isEmpty) {
|
||||
// If the search is empty, it shows all unselected players.
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
return !selectedPlayers.contains(player);
|
||||
return !selectedPlayers.any((p) => p.id == player.id);
|
||||
}).toList();
|
||||
} else {
|
||||
// If there is input, it filters by name match (case-insensitive) and ensures
|
||||
@@ -109,8 +110,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
final bool nameMatches = player.name.toLowerCase().contains(
|
||||
value.toLowerCase(),
|
||||
);
|
||||
final bool isNotSelected = !selectedPlayers.contains(
|
||||
player,
|
||||
final bool isNotSelected = !selectedPlayers.any(
|
||||
(p) => p.id == player.id,
|
||||
);
|
||||
return nameMatches && isNotSelected;
|
||||
}).toList();
|
||||
@@ -126,6 +127,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
const SizedBox(height: 10),
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
child: selectedPlayers.isEmpty
|
||||
? Center(child: Text(loc.no_players_selected))
|
||||
: SingleChildScrollView(
|
||||
@@ -167,6 +170,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
loc.all_players,
|
||||
@@ -222,8 +226,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
db.playerDao.getAllPlayers(),
|
||||
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||
]).then((results) => results[0] as List<Player>);
|
||||
if (mounted) {
|
||||
|
||||
_allPlayersFuture.then((loadedPlayers) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
// If a list of available players is provided (even if empty), use that list.
|
||||
if (widget.availablePlayers != null) {
|
||||
@@ -245,13 +250,30 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
// Otherwise, use the loaded players from the database.
|
||||
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
allPlayers = [...loadedPlayers];
|
||||
if (widget.initialSelectedPlayers != null) {
|
||||
// Excludes already selected players from the suggested players list.
|
||||
suggestedPlayers = loadedPlayers
|
||||
.where(
|
||||
(p) => !widget.initialSelectedPlayers!.any(
|
||||
(ip) => ip.id == p.id,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
// Ensures that only players available for selection are pre-selected.
|
||||
selectedPlayers = widget.initialSelectedPlayers!
|
||||
.where(
|
||||
(p) => allPlayers.any((available) => available.id == p.id),
|
||||
|
flixcoo marked this conversation as resolved
sneeex
commented
was geht hier ab? wieso changes? was geht hier ab? wieso changes?
flixcoo
commented
Da kam dieser Error, den du hattest her. Da habe ich ein if statement entfernt, dadurch hat sich alles verschoben und wird als Änderung markiert. Effektiv habe ich nur ein Da kam dieser Error, den du hattest her. Da habe ich ein if statement entfernt, dadurch hat sich alles verschoben und wird als Änderung markiert. Effektiv habe ich nur ein `if(mounted){...}` durch ein `if(!mounted) return;` ersetzt
|
||||
)
|
||||
.toList();
|
||||
} else {
|
||||
// If no initial selection, all loaded players are suggested.
|
||||
suggestedPlayers = [...loadedPlayers];
|
||||
}
|
||||
}
|
||||
isLoading = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a new player to the database from the search bar input.
|
||||
/// Shows a snackbar indicating success or failure.
|
||||
|
||||
@@ -69,7 +69,6 @@ class CustomSearchBar extends StatelessWidget {
|
||||
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(
|
||||
|
||||
@@ -36,11 +36,7 @@ class CustomRadioListTile<T> extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Radio<T>(
|
||||
value: value,
|
||||
activeColor: CustomTheme.primaryColor,
|
||||
toggleable: true,
|
||||
),
|
||||
Radio<T>(value: value, toggleable: true),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:core' hide Match;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:tallee/core/common.dart';
|
||||
import 'package:tallee/core/custom_theme.dart';
|
||||
import 'package:tallee/data/dto/match.dart';
|
||||
import 'package:tallee/data/dto/player.dart';
|
||||
import 'package:tallee/l10n/generated/app_localizations.dart';
|
||||
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
@@ -38,18 +40,13 @@ class MatchTile extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MatchTileState extends State<MatchTile> {
|
||||
late final List<Player> _allPlayers;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_allPlayers = _getCombinedPlayers();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final group = widget.match.group;
|
||||
final winner = widget.match.winner;
|
||||
final match = widget.match;
|
||||
final group = match.group;
|
||||
final winner = match.winner;
|
||||
final players = [...match.players]
|
||||
..sort((a, b) => a.name.compareTo(b.name));
|
||||
final loc = AppLocalizations.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
@@ -67,7 +64,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.match.name,
|
||||
match.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -76,7 +73,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDate(widget.match.createdAt, context),
|
||||
_formatDate(match.createdAt, context),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
@@ -91,7 +88,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${group.name} + ${widget.match.players.length}',
|
||||
'${match.group!.name}${getExtraPlayerCount(match)}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -106,7 +103,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${widget.match.players.length} ${loc.players}',
|
||||
'${match.players.length} ${loc.players}',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -192,7 +189,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
if (_allPlayers.isNotEmpty && widget.compact == false) ...[
|
||||
if (players.isNotEmpty && widget.compact == false) ...[
|
||||
Text(
|
||||
loc.players,
|
||||
style: const TextStyle(
|
||||
@@ -205,7 +202,7 @@ class _MatchTileState extends State<MatchTile> {
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: _allPlayers.map((player) {
|
||||
children: players.map((player) {
|
||||
return TextIconTile(text: player.name, iconEnabled: false);
|
||||
}).toList(),
|
||||
),
|
||||
@@ -233,32 +230,4 @@ class _MatchTileState extends State<MatchTile> {
|
||||
return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieves all unique players associated with the match,
|
||||
/// combining players from both the match and its group.
|
||||
List<Player> _getCombinedPlayers() {
|
||||
final allPlayers = <Player>[];
|
||||
final playerIds = <String>{};
|
||||
|
||||
// Add players from game.players
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ dev_dependencies:
|
||||
build_runner: ^2.5.4
|
||||
dart_pubspec_licenses: ^3.0.14
|
||||
drift_dev: ^2.27.0
|
||||
flutter_lints: ^5.0.0
|
||||
flutter_lints: ^6.0.0
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/drift.dart' hide isNotNull;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
import 'package:tallee/data/db/database.dart';
|
||||
import 'package:tallee/data/dto/game.dart';
|
||||
import 'package:tallee/data/dto/group.dart';
|
||||
import 'package:tallee/data/dto/match.dart';
|
||||
import 'package:tallee/data/dto/player.dart';
|
||||
import 'package:tallee/core/enums.dart';
|
||||
|
||||
void main() {
|
||||
late AppDatabase database;
|
||||
@@ -51,7 +51,13 @@ void main() {
|
||||
description: '',
|
||||
members: [testPlayer4, testPlayer5],
|
||||
);
|
||||
testGame = Game(name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', color: GameColor.blue, icon: '');
|
||||
testGame = Game(
|
||||
name: 'Test Game',
|
||||
ruleset: Ruleset.singleWinner,
|
||||
description: 'A test game',
|
||||
color: GameColor.blue,
|
||||
icon: '',
|
||||
);
|
||||
testMatch1 = Match(
|
||||
name: 'First Test Match',
|
||||
game: testGame,
|
||||
@@ -99,7 +105,6 @@ void main() {
|
||||
});
|
||||
|
||||
group('Match Tests', () {
|
||||
|
||||
// Verifies that a single match can be added and retrieved with all fields, group, and players intact.
|
||||
test('Adding and fetching single match works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
@@ -127,10 +132,7 @@ void main() {
|
||||
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,
|
||||
);
|
||||
expect(result.players[i].createdAt, testMatch1.players[i].createdAt);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -191,10 +193,7 @@ void main() {
|
||||
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,
|
||||
);
|
||||
expect(match.players[i].createdAt, testMatch.players[i].createdAt);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -282,5 +281,31 @@ void main() {
|
||||
);
|
||||
expect(fetchedMatch.name, newName);
|
||||
});
|
||||
|
||||
test('Fetching a winner works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
var fetchedMatch = await database.matchDao.getMatchById(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
|
||||
expect(fetchedMatch.winner, isNotNull);
|
||||
expect(fetchedMatch.winner!.id, testPlayer4.id);
|
||||
});
|
||||
|
||||
test('Setting a winner works correctly', () async {
|
||||
await database.matchDao.addMatch(match: testMatch1);
|
||||
|
||||
await database.matchDao.setWinner(
|
||||
matchId: testMatch1.id,
|
||||
winnerId: testPlayer5.id,
|
||||
);
|
||||
|
||||
final fetchedMatch = await database.matchDao.getMatchById(
|
||||
matchId: testMatch1.id,
|
||||
);
|
||||
expect(fetchedMatch.winner, isNotNull);
|
||||
expect(fetchedMatch.winner!.id, testPlayer5.id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
sollte nicht auch hier als bedingung rein, dass game ausgewählt ist (auch wenns gerade noch keine funktion hat)
Geändert