Merge branch 'develop' into enhance/34-improvement-for-visual-hierachy
# Conflicts: # lib/presentation/views/create_game_view.dart # pubspec.yaml
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# CABO Counter
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@@ -11,6 +11,3 @@ linter:
|
||||
prefer_const_literals_to_create_immutables: true
|
||||
unnecessary_const: true
|
||||
lines_longer_than_80_chars: false
|
||||
|
||||
# Additional information about this file can be found at
|
||||
# https://dart.dev/guides/language/analysis-options
|
||||
|
||||
291
assets/game-schema.json
Normal file
291
assets/game-schema.json
Normal file
@@ -0,0 +1,291 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"title": "Generated schema for a single cabo counter game",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"gameTitle": {
|
||||
"type": "string"
|
||||
},
|
||||
"players": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPenalty": {
|
||||
"type": "integer"
|
||||
},
|
||||
"isPointsLimitEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isGameFinished": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"winner": {
|
||||
"type": "string"
|
||||
},
|
||||
"roundNumber": {
|
||||
"type": "integer"
|
||||
},
|
||||
"playerScores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"roundList": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roundNum": {
|
||||
"type": "integer"
|
||||
},
|
||||
"caboPlayerIndex": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kamikazePlayerIndex": {
|
||||
"type": "null"
|
||||
},
|
||||
"scores": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scoreUpdates": {
|
||||
"type": "array",
|
||||
"items": [
|
||||
{
|
||||
"type": "integer"
|
||||
},
|
||||
{
|
||||
"type": "integer"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"roundNum",
|
||||
"caboPlayerIndex",
|
||||
"kamikazePlayerIndex",
|
||||
"scores",
|
||||
"scoreUpdates"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"createdAt",
|
||||
"gameTitle",
|
||||
"players",
|
||||
"pointLimit",
|
||||
"caboPenalty",
|
||||
"isPointsLimitEnabled",
|
||||
"isGameFinished",
|
||||
"winner",
|
||||
"roundNumber",
|
||||
"playerScores",
|
||||
"roundList"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Generated schema for cabo game data",
|
||||
"title": "Generated schema for the cabo counter game data",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@@ -1,5 +1,6 @@
|
||||
arb-dir: lib/l10n
|
||||
arb-dir: lib/l10n/arb
|
||||
template-arb-file: app_de.arb
|
||||
untranslated-messages-file: lib/l10n/untranslated_messages.json
|
||||
untranslated-messages-file: lib/l10n/arb/untranslated_messages.json
|
||||
nullable-getter: false
|
||||
output-localization-file: app_localizations.dart
|
||||
output-dir: lib/l10n/generated
|
||||
22
lib/core/constants.dart
Normal file
22
lib/core/constants.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:rate_my_app/rate_my_app.dart';
|
||||
|
||||
class Constants {
|
||||
static const String appDevPhase = 'Beta';
|
||||
|
||||
static const String kInstagramLink = 'https://instagram.felixkirchner.de';
|
||||
static const String kGithubLink = 'https://github.felixkirchner.de';
|
||||
static const String kGithubIssuesLink =
|
||||
'https://cabocounter-issues.felixkirchner.de';
|
||||
static const String kGithubWikiLink =
|
||||
'https://cabocounter-wiki.felixkirchner.de';
|
||||
static const String kEmail = 'cabocounter@felixkirchner.de';
|
||||
static const String kPrivacyPolicyLink =
|
||||
'https://www.privacypolicies.com/live/1b3759d4-b2f1-4511-8e3b-21bb1626be68';
|
||||
|
||||
static RateMyApp rateMyApp = RateMyApp(
|
||||
appStoreIdentifier: '6747105718',
|
||||
minDays: 15,
|
||||
remindDays: 45,
|
||||
minLaunches: 15,
|
||||
remindLaunches: 40);
|
||||
}
|
||||
@@ -4,7 +4,20 @@ class CustomTheme {
|
||||
static Color white = CupertinoColors.white;
|
||||
static Color primaryColor = CupertinoColors.systemGreen;
|
||||
static Color backgroundColor = const Color(0xFF101010);
|
||||
static Color backgroundTintColor = CupertinoColors.darkBackgroundGray;
|
||||
static Color mainElementBackgroundColor = CupertinoColors.darkBackgroundGray;
|
||||
static Color playerTileColor = CupertinoColors.secondaryLabel;
|
||||
static Color buttonBackgroundColor = const Color(0xFF202020);
|
||||
|
||||
// Line Colors for GraphView
|
||||
static const Color graphColor1 = Color(0xFFF44336);
|
||||
static const Color graphColor2 = Color(0xFF2196F3);
|
||||
static const Color graphColor3 = Color(0xFFFFA726);
|
||||
static const Color graphColor4 = Color(0xFF9C27B0);
|
||||
static final Color graphColor5 = primaryColor;
|
||||
|
||||
// Colors for PointsView
|
||||
static Color pointLossColor = primaryColor;
|
||||
static const Color pointGainColor = Color(0xFFF44336);
|
||||
|
||||
static TextStyle modeTitle = TextStyle(
|
||||
color: primaryColor,
|
||||
@@ -42,9 +42,27 @@ class GameManager extends ChangeNotifier {
|
||||
removeGameSessionByIndex(index);
|
||||
}
|
||||
|
||||
/// Retrieves a game session by its ID.
|
||||
/// Takes a String [id] as input. It finds the game session with the matching id
|
||||
bool gameExistsInGameList(String id) {
|
||||
return gameList.any((session) => session.id.toString() == id);
|
||||
}
|
||||
|
||||
/// Ends a game session if its in unlimited mode.
|
||||
/// Takes a String [id] as input. It finds the index of the game
|
||||
/// session with the matching ID marks it as finished,
|
||||
void endGame(String id) {
|
||||
final int index =
|
||||
gameList.indexWhere((session) => session.id.toString() == id);
|
||||
|
||||
// Game session not found or not in unlimited mode
|
||||
if (index == -1 || gameList[index].isPointsLimitEnabled == true) return;
|
||||
|
||||
gameList[index].roundNumber--;
|
||||
gameList[index].isGameFinished = true;
|
||||
notifyListeners();
|
||||
LocalStorageService.saveGameSessions();
|
||||
}
|
||||
}
|
||||
|
||||
final gameManager = GameManager();
|
||||
|
||||
@@ -235,7 +235,7 @@ class GameSession extends ChangeNotifier {
|
||||
|
||||
/// This method updates the points of each player after a round.
|
||||
/// It first uses the _sumPoints() method to calculate the total points of each player.
|
||||
/// Then, it checks if any player has reached 100 points. If so, it marks
|
||||
/// Then, it checks if any player has reached 100 points. If so, saves their indices and marks
|
||||
/// that player as having reached 100 points in that corresponding [Round] object.
|
||||
/// If the game has the point limit activated, it first applies the
|
||||
/// _subtractPointsForReachingHundred() method to subtract 50 points
|
||||
@@ -243,10 +243,13 @@ class GameSession extends ChangeNotifier {
|
||||
/// It then checks if any player has exceeded 100 points. If so, it sets
|
||||
/// isGameFinished to true and calls the _setWinner() method to determine
|
||||
/// the winner.
|
||||
Future<void> updatePoints() async {
|
||||
/// It returns a list of players indices who reached 100 points in the current
|
||||
/// round for the [RoundView] to show a popup
|
||||
List<int> updatePoints() {
|
||||
List<int> bonusPlayers = [];
|
||||
_sumPoints();
|
||||
if (isPointsLimitEnabled) {
|
||||
_checkHundredPointsReached();
|
||||
bonusPlayers = _checkHundredPointsReached();
|
||||
|
||||
for (int i = 0; i < playerScores.length; i++) {
|
||||
if (playerScores[i] > pointLimit) {
|
||||
@@ -258,6 +261,7 @@ class GameSession extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
notifyListeners();
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
@@ -278,15 +282,18 @@ class GameSession extends ChangeNotifier {
|
||||
/// Checks if a player has reached 100 points in the current round.
|
||||
/// If so, it updates the [scoreUpdate] List by subtracting 50 points from
|
||||
/// the corresponding round update.
|
||||
void _checkHundredPointsReached() {
|
||||
List<int> _checkHundredPointsReached() {
|
||||
List<int> bonusPlayers = [];
|
||||
for (int i = 0; i < players.length; i++) {
|
||||
if (playerScores[i] == pointLimit) {
|
||||
bonusPlayers.add(i);
|
||||
print('${players[i]} hat genau 100 Punkte erreicht und bekommt '
|
||||
'deswegen 50 Punkte abgezogen');
|
||||
roundList[roundNumber - 1].scoreUpdates[i] -= 50;
|
||||
'deswegen ${(pointLimit / 2).round()} Punkte abgezogen');
|
||||
roundList[roundNumber - 1].scoreUpdates[i] -= (pointLimit / 2).round();
|
||||
}
|
||||
}
|
||||
_sumPoints();
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
/// Determines the winner of the game session.
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pre_rating_title": "Gefällt dir die App?",
|
||||
"pre_rating_message": "Feedback hilft mir, die App zu verbessern. Vielen Dank!",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"bad_rating_title": "Unzufrieden mit der App?",
|
||||
"bad_rating_message": "Schreib mir gerne direkt eine E-Mail, damit wir dein Problem lösen können!",
|
||||
"contact_email": "E-Mail schreiben",
|
||||
"email_subject": "Feedback: Cabo Counter App",
|
||||
"email_body": "Ich habe folgendes Feedback...",
|
||||
|
||||
"overview": "Übersicht",
|
||||
"new_game": "Neues Spiel",
|
||||
"game_title": "Titel des Spiels",
|
||||
@@ -48,6 +58,9 @@
|
||||
"no_name_message": "Jeder Spieler muss einen Namen haben.",
|
||||
|
||||
"select_game_mode": "Spielmodus auswählen",
|
||||
"no_mode_selected": "Wähle einen Spielmodus",
|
||||
"no_default_mode": "Kein Modus",
|
||||
"no_default_description": "Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.",
|
||||
"point_limit_description": "Es wird so lange gespielt, bis ein:e Spieler:in mehr als {pointLimit} Punkte erreicht",
|
||||
"@point_limit_description": {
|
||||
"placeholders": {
|
||||
@@ -61,28 +74,54 @@
|
||||
"results": "Ergebnisse",
|
||||
"who_said_cabo": "Wer hat CABO gesagt?",
|
||||
"kamikaze": "Kamikaze",
|
||||
"who_has_kamikaze": "Wer hat Kamikaze?",
|
||||
"done": "Fertig",
|
||||
"next_round": "Nächste Runde",
|
||||
"bonus_points_title": "Bonus-Punkte!",
|
||||
"bonus_points_message": "{playerCount, plural, =1{{names} hat exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommt deshalb {bonusPoints} Punkte abgezogen!} other{{names} haben exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommen deshalb jeweils {bonusPoints} Punkte abgezogen!}}",
|
||||
"@bonus_points_message": {
|
||||
"placeholders": {
|
||||
"playerCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"names": {
|
||||
"type": "String"
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
},
|
||||
"bonusPoints": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"statistics": "Statistiken",
|
||||
"end_game": "Spiel beenden",
|
||||
"delete_game": "Spiel löschen",
|
||||
"new_game_same_settings": "Neues Spiel mit gleichen Einstellungen",
|
||||
"export_game": "Spiel exportieren",
|
||||
"id_error_title": "ID Fehler",
|
||||
"id_error_message": "Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.",
|
||||
"end_game_title": "Spiel beenden?",
|
||||
"end_game_message": "Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.",
|
||||
|
||||
|
||||
"game_process": "Spielverlauf",
|
||||
"statistics": "Statistiken",
|
||||
"point_overview": "Punkteübersicht",
|
||||
"scoring_history": "Spielverlauf",
|
||||
"empty_graph_text": "Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.",
|
||||
|
||||
"settings": "Einstellungen",
|
||||
"cabo_penalty": "Cabo-Strafe",
|
||||
"cabo_penalty_subtitle": "... für falsches Cabo sagen",
|
||||
"point_limit": "Punkte-Limit",
|
||||
"point_limit_subtitle": "... hier ist Schluss",
|
||||
"standard_mode": "Standard-Modus",
|
||||
"reset_to_default": "Auf Standard zurücksetzen",
|
||||
"game_data": "Spieldaten",
|
||||
"import_data": "Daten importieren",
|
||||
"export_data": "Daten exportieren",
|
||||
"import_data": "Spieldaten importieren",
|
||||
"export_data": "Spieldaten exportieren",
|
||||
"delete_data": "Alle Spieldaten löschen",
|
||||
"delete_data_title": "Spieldaten löschen?",
|
||||
"delete_data_message": "Bist du sicher, dass du alle Spieldaten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"app": "App",
|
||||
|
||||
"import_success_title": "Import erfolgreich",
|
||||
"import_success_message":"Die Spieldaten wurden erfolgreich importiert.",
|
||||
@@ -90,16 +129,19 @@
|
||||
"import_validation_error_message": "Es wurden keine Cabo-Counter Spieldaten gefunden. Bitte stellen Sie sicher, dass es sich um eine gültige Cabo-Counter Exportdatei handelt.",
|
||||
"import_format_error_title": "Falsches Format",
|
||||
"import_format_error_message": "Die Datei ist kein gültiges JSON-Format oder enthält ungültige Daten.",
|
||||
"import_generic_error_title": "Import fehlgeschlagen",
|
||||
"import_generic_error_title": "Import fehlgeschlagen",
|
||||
"import_generic_error_message": "Der Import ist fehlgeschlagen.",
|
||||
|
||||
"export_error_title": "Fehler",
|
||||
"export_error_message": "Datei konnte nicht exportiert werden",
|
||||
|
||||
"error_found": "Fehler gefunden?",
|
||||
"create_issue": "Issue erstellen",
|
||||
"wiki": "Wiki",
|
||||
"app_version": "App-Version",
|
||||
"build": "Build",
|
||||
"load_version": "Lade Version...",
|
||||
"privacy_policy": "Datenschutzerklärung",
|
||||
"build": "Build-Nr.",
|
||||
"loading": "Lädt...",
|
||||
|
||||
"about_text": "Hey :) Danke, dass du als eine:r der ersten User meiner ersten eigenen App dabei bist! Ich hab sehr viel Arbeit in dieses Projekt gesteckt und auch, wenn ich (hoffentlich) an vieles Gedacht hab, wird auf jeden Fall noch nicht alles 100% funktionieren. Solltest du also irgendwelche Fehler entdecken oder Feedback zum Design oder der Benutzerfreundlichkeit haben, teile Sie mir gern über die Testflight App oder auf den dir bekannten Wegen mit. Danke! "
|
||||
}
|
||||
@@ -14,13 +14,13 @@
|
||||
"player": "Player",
|
||||
"players": "Players",
|
||||
"name": "Name",
|
||||
"back": "Back",
|
||||
"back": "Back",
|
||||
|
||||
"home": "Home",
|
||||
"about": "About",
|
||||
|
||||
"empty_text_1": "Pretty empty here...",
|
||||
"empty_text_2": "Add a new round using the button in the top right corner.",
|
||||
"empty_text_2": "Create a new game using the button in the top right.",
|
||||
"delete_game_title": "Delete game?",
|
||||
"delete_game_message": "Are you sure you want to delete the game \"{gameTitle}\"? This action cannot be undone.",
|
||||
"@delete_game_message": {
|
||||
@@ -30,25 +30,38 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"pre_rating_title": "Do you like the app?",
|
||||
"pre_rating_message": "Feedback helps me to continuously improve the app. Thank you!",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"bad_rating_title": "Not satisfied?",
|
||||
"bad_rating_message": "If you are not satisfied with the app, please let me know before leaving a bad rating. I will try to fix the issue as soon as possible.",
|
||||
"contact_email": "Contact via E-Mail",
|
||||
"email_subject": "Feedback: Cabo Counter App",
|
||||
"email_body": "I have the following feedback...",
|
||||
|
||||
"overview": "Overview",
|
||||
"new_game": "New Game",
|
||||
"game_title": "Game Title",
|
||||
"select_mode": "Select a mode",
|
||||
"add_player": "Add Player",
|
||||
"create_game": "Create Game",
|
||||
"max_players_title": "Maximum reached",
|
||||
"max_players_message": "A maximum of 5 players can be added.",
|
||||
"no_gameTitle_title": "No Title",
|
||||
"no_gameTitle_message": "You must enter a title for the game.",
|
||||
"no_mode_title": "No Mode",
|
||||
"no_mode_message": "You must select a game mode.",
|
||||
"min_players_title": "Too few players",
|
||||
"min_players_message": "At least 2 players must be added.",
|
||||
"no_name_title": "No Name",
|
||||
"max_players_title": "Player Limit Reached",
|
||||
"max_players_message": "You can add a maximum of 5 players.",
|
||||
"no_gameTitle_title": "Missing Game Title",
|
||||
"no_gameTitle_message": "Please enter a title for your game.",
|
||||
"no_mode_title": "Game Mode Required",
|
||||
"no_mode_message": "Please select a game mode to continue",
|
||||
"min_players_title": "Too Few Players",
|
||||
"min_players_message": "At least 2 players are required to start the game.",
|
||||
"no_name_title": "Missing Player Names",
|
||||
"no_name_message": "Each player must have a name.",
|
||||
|
||||
"select_game_mode": "Select game mode",
|
||||
"point_limit_description": "The game ends when a player reaches more than {pointLimit} points.",
|
||||
"no_mode_selected": "No mode selected",
|
||||
"no_default_mode": "No default mode",
|
||||
"no_default_description": "The default mode gets reset.",
|
||||
"point_limit_description": "The game ends when a player scores more than {pointLimit} points.",
|
||||
"@point_limit_description": {
|
||||
"placeholders": {
|
||||
"pointLimit": {
|
||||
@@ -56,32 +69,59 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"unlimited_description": "There is no limit. The game continues until you decide to stop.",
|
||||
"unlimited_description": "The game continues until you decide to stop playing",
|
||||
|
||||
"results": "Results",
|
||||
"who_said_cabo": "Who said CABO?",
|
||||
"who_said_cabo": "Who called Cabo?",
|
||||
"kamikaze": "Kamikaze",
|
||||
"who_has_kamikaze": "Who has Kamikaze?",
|
||||
"done": "Done",
|
||||
"next_round": "Next Round",
|
||||
"bonus_points_title": "Bonus-Points!",
|
||||
"bonus_points_message": "{playerCount, plural, =1{{names} has reached exactly the point limit of {pointLimit} points and therefore gets {bonusPoints} points deducted!} other{{names} have reached exactly the point limit of {pointLimit} points and therefore get {bonusPoints} points deducted!}}",
|
||||
"@bonus_points_message": {
|
||||
"placeholders": {
|
||||
"playerCount": {
|
||||
"type": "int"
|
||||
},
|
||||
"names": {
|
||||
"type": "String"
|
||||
},
|
||||
"pointLimit": {
|
||||
"type": "int"
|
||||
},
|
||||
"bonusPoints": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"statistics": "Statistics",
|
||||
"end_game": "End Game",
|
||||
"delete_game": "Delete Game",
|
||||
"new_game_same_settings": "New Game with same Settings",
|
||||
"export_game": "Export Game",
|
||||
"id_error_title": "ID Error",
|
||||
"id_error_message": "The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.",
|
||||
"end_game_title": "End the game?",
|
||||
"end_game_message": "Do you want to end the game? The game gets marked as finished and cannot be continued.",
|
||||
|
||||
"game_process": "Spielverlauf",
|
||||
"statistics": "Statistics",
|
||||
"point_overview": "Point Overview",
|
||||
"scoring_history": "Scoring History",
|
||||
"empty_graph_text": "You must play at least one round for the game progress graph to be displayed.",
|
||||
|
||||
"settings": "Settings",
|
||||
"cabo_penalty": "Cabo Penalty",
|
||||
"cabo_penalty_subtitle": "... for falsely calling Cabo.",
|
||||
"point_limit": "Point Limit",
|
||||
"point_limit_subtitle": "... the game ends here.",
|
||||
"standard_mode": "Default Mode",
|
||||
"reset_to_default": "Reset to Default",
|
||||
"game_data": "Game Data",
|
||||
"import_data": "Import Data",
|
||||
"export_data": "Export Data",
|
||||
"id_error_title": "ID Error",
|
||||
"id_error_message": "The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.",
|
||||
"delete_data": "Delete all Game Data",
|
||||
"delete_data_title": "Delete game data?",
|
||||
"delete_data_message": "Are you sure you want to delete all game data? This action cannot be undone.",
|
||||
"app": "App",
|
||||
|
||||
"import_success_title": "Import successful",
|
||||
"import_success_message":"The game data has been successfully imported.",
|
||||
@@ -94,11 +134,14 @@
|
||||
|
||||
"export_error_title": "Export failed",
|
||||
"export_error_message": "Could not export file",
|
||||
|
||||
"error_found": "Found a bug?",
|
||||
"create_issue": "Create Issue",
|
||||
"wiki": "Wiki",
|
||||
"app_version": "App Version",
|
||||
"load_version": "Loading version...",
|
||||
"build": "Build",
|
||||
"privacy_policy": "Privacy Policy",
|
||||
"loading": "Loading...",
|
||||
"build": "Build No.",
|
||||
|
||||
"about_text": "Hey :) Thanks for being one of the first users of my app! I’ve put a lot of work into this project, and even though I tried to think of everything, it might not work perfectly just yet. So if you discover any bugs or have feedback on the design or usability, please let me know via the TestFlight app or by sending me a message or email. Thank you very much!"
|
||||
}
|
||||
@@ -18,7 +18,7 @@ import 'app_localizations_en.dart';
|
||||
/// `supportedLocales` list. For example:
|
||||
///
|
||||
/// ```dart
|
||||
/// import 'l10n/app_localizations.dart';
|
||||
/// import 'generated/app_localizations.dart';
|
||||
///
|
||||
/// return MaterialApp(
|
||||
/// localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
@@ -218,6 +218,60 @@ abstract class AppLocalizations {
|
||||
/// **'Bist du sicher, dass du das Spiel \"{gameTitle}\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'**
|
||||
String delete_game_message(String gameTitle);
|
||||
|
||||
/// No description provided for @pre_rating_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Gefällt dir die App?'**
|
||||
String get pre_rating_title;
|
||||
|
||||
/// No description provided for @pre_rating_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Feedback hilft mir, die App zu verbessern. Vielen Dank!'**
|
||||
String get pre_rating_message;
|
||||
|
||||
/// No description provided for @yes.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Ja'**
|
||||
String get yes;
|
||||
|
||||
/// No description provided for @no.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Nein'**
|
||||
String get no;
|
||||
|
||||
/// No description provided for @bad_rating_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Unzufrieden mit der App?'**
|
||||
String get bad_rating_title;
|
||||
|
||||
/// No description provided for @bad_rating_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Schreib mir gerne direkt eine E-Mail, damit wir dein Problem lösen können!'**
|
||||
String get bad_rating_message;
|
||||
|
||||
/// No description provided for @contact_email.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'E-Mail schreiben'**
|
||||
String get contact_email;
|
||||
|
||||
/// No description provided for @email_subject.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Feedback: Cabo Counter App'**
|
||||
String get email_subject;
|
||||
|
||||
/// No description provided for @email_body.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Ich habe folgendes Feedback...'**
|
||||
String get email_body;
|
||||
|
||||
/// No description provided for @overview.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -320,6 +374,24 @@ abstract class AppLocalizations {
|
||||
/// **'Spielmodus auswählen'**
|
||||
String get select_game_mode;
|
||||
|
||||
/// No description provided for @no_mode_selected.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Wähle einen Spielmodus'**
|
||||
String get no_mode_selected;
|
||||
|
||||
/// No description provided for @no_default_mode.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Kein Modus'**
|
||||
String get no_default_mode;
|
||||
|
||||
/// No description provided for @no_default_description.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.'**
|
||||
String get no_default_description;
|
||||
|
||||
/// No description provided for @point_limit_description.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -350,6 +422,12 @@ abstract class AppLocalizations {
|
||||
/// **'Kamikaze'**
|
||||
String get kamikaze;
|
||||
|
||||
/// No description provided for @who_has_kamikaze.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Wer hat Kamikaze?'**
|
||||
String get who_has_kamikaze;
|
||||
|
||||
/// No description provided for @done.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -362,11 +440,24 @@ abstract class AppLocalizations {
|
||||
/// **'Nächste Runde'**
|
||||
String get next_round;
|
||||
|
||||
/// No description provided for @statistics.
|
||||
/// No description provided for @bonus_points_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Statistiken'**
|
||||
String get statistics;
|
||||
/// **'Bonus-Punkte!'**
|
||||
String get bonus_points_title;
|
||||
|
||||
/// No description provided for @bonus_points_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'{playerCount, plural, =1{{names} hat exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommt deshalb {bonusPoints} Punkte abgezogen!} other{{names} haben exakt das Punktelimit von {pointLimit} Punkten erreicht und bekommen deshalb jeweils {bonusPoints} Punkte abgezogen!}}'**
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints);
|
||||
|
||||
/// No description provided for @end_game.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spiel beenden'**
|
||||
String get end_game;
|
||||
|
||||
/// No description provided for @delete_game.
|
||||
///
|
||||
@@ -398,11 +489,41 @@ abstract class AppLocalizations {
|
||||
/// **'Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.'**
|
||||
String get id_error_message;
|
||||
|
||||
/// No description provided for @game_process.
|
||||
/// No description provided for @end_game_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spiel beenden?'**
|
||||
String get end_game_title;
|
||||
|
||||
/// No description provided for @end_game_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.'**
|
||||
String get end_game_message;
|
||||
|
||||
/// No description provided for @statistics.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Statistiken'**
|
||||
String get statistics;
|
||||
|
||||
/// No description provided for @point_overview.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Punkteübersicht'**
|
||||
String get point_overview;
|
||||
|
||||
/// No description provided for @scoring_history.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spielverlauf'**
|
||||
String get game_process;
|
||||
String get scoring_history;
|
||||
|
||||
/// No description provided for @empty_graph_text.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.'**
|
||||
String get empty_graph_text;
|
||||
|
||||
/// No description provided for @settings.
|
||||
///
|
||||
@@ -416,23 +537,17 @@ abstract class AppLocalizations {
|
||||
/// **'Cabo-Strafe'**
|
||||
String get cabo_penalty;
|
||||
|
||||
/// No description provided for @cabo_penalty_subtitle.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'... für falsches Cabo sagen'**
|
||||
String get cabo_penalty_subtitle;
|
||||
|
||||
/// No description provided for @point_limit.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Punkte-Limit'**
|
||||
String get point_limit;
|
||||
|
||||
/// No description provided for @point_limit_subtitle.
|
||||
/// No description provided for @standard_mode.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'... hier ist Schluss'**
|
||||
String get point_limit_subtitle;
|
||||
/// **'Standard-Modus'**
|
||||
String get standard_mode;
|
||||
|
||||
/// No description provided for @reset_to_default.
|
||||
///
|
||||
@@ -449,15 +564,39 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @import_data.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Daten importieren'**
|
||||
/// **'Spieldaten importieren'**
|
||||
String get import_data;
|
||||
|
||||
/// No description provided for @export_data.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Daten exportieren'**
|
||||
/// **'Spieldaten exportieren'**
|
||||
String get export_data;
|
||||
|
||||
/// No description provided for @delete_data.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Alle Spieldaten löschen'**
|
||||
String get delete_data;
|
||||
|
||||
/// No description provided for @delete_data_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Spieldaten löschen?'**
|
||||
String get delete_data_title;
|
||||
|
||||
/// No description provided for @delete_data_message.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Bist du sicher, dass du alle Spieldaten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.'**
|
||||
String get delete_data_message;
|
||||
|
||||
/// No description provided for @app.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'App'**
|
||||
String get app;
|
||||
|
||||
/// No description provided for @import_success_title.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -530,23 +669,35 @@ abstract class AppLocalizations {
|
||||
/// **'Issue erstellen'**
|
||||
String get create_issue;
|
||||
|
||||
/// No description provided for @wiki.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Wiki'**
|
||||
String get wiki;
|
||||
|
||||
/// No description provided for @app_version.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'App-Version'**
|
||||
String get app_version;
|
||||
|
||||
/// No description provided for @privacy_policy.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Datenschutzerklärung'**
|
||||
String get privacy_policy;
|
||||
|
||||
/// No description provided for @build.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Build'**
|
||||
/// **'Build-Nr.'**
|
||||
String get build;
|
||||
|
||||
/// No description provided for @load_version.
|
||||
/// No description provided for @loading.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Lade Version...'**
|
||||
String get load_version;
|
||||
/// **'Lädt...'**
|
||||
String get loading;
|
||||
|
||||
/// No description provided for @about_text.
|
||||
///
|
||||
@@ -71,6 +71,35 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Bist du sicher, dass du das Spiel \"$gameTitle\" löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pre_rating_title => 'Gefällt dir die App?';
|
||||
|
||||
@override
|
||||
String get pre_rating_message =>
|
||||
'Feedback hilft mir, die App zu verbessern. Vielen Dank!';
|
||||
|
||||
@override
|
||||
String get yes => 'Ja';
|
||||
|
||||
@override
|
||||
String get no => 'Nein';
|
||||
|
||||
@override
|
||||
String get bad_rating_title => 'Unzufrieden mit der App?';
|
||||
|
||||
@override
|
||||
String get bad_rating_message =>
|
||||
'Schreib mir gerne direkt eine E-Mail, damit wir dein Problem lösen können!';
|
||||
|
||||
@override
|
||||
String get contact_email => 'E-Mail schreiben';
|
||||
|
||||
@override
|
||||
String get email_subject => 'Feedback: Cabo Counter App';
|
||||
|
||||
@override
|
||||
String get email_body => 'Ich habe folgendes Feedback...';
|
||||
|
||||
@override
|
||||
String get overview => 'Übersicht';
|
||||
|
||||
@@ -125,6 +154,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get select_game_mode => 'Spielmodus auswählen';
|
||||
|
||||
@override
|
||||
String get no_mode_selected => 'Wähle einen Spielmodus';
|
||||
|
||||
@override
|
||||
String get no_default_mode => 'Kein Modus';
|
||||
|
||||
@override
|
||||
String get no_default_description =>
|
||||
'Entscheide bei jedem Spiel selber, welchen Modus du spielen möchtest.';
|
||||
|
||||
@override
|
||||
String point_limit_description(int pointLimit) {
|
||||
return 'Es wird so lange gespielt, bis ein:e Spieler:in mehr als $pointLimit Punkte erreicht';
|
||||
@@ -143,6 +182,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get kamikaze => 'Kamikaze';
|
||||
|
||||
@override
|
||||
String get who_has_kamikaze => 'Wer hat Kamikaze?';
|
||||
|
||||
@override
|
||||
String get done => 'Fertig';
|
||||
|
||||
@@ -150,7 +192,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get next_round => 'Nächste Runde';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistiken';
|
||||
String get bonus_points_title => 'Bonus-Punkte!';
|
||||
|
||||
@override
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
other:
|
||||
'$names haben exakt das Punktelimit von $pointLimit Punkten erreicht und bekommen deshalb jeweils $bonusPoints Punkte abgezogen!',
|
||||
one:
|
||||
'$names hat exakt das Punktelimit von $pointLimit Punkten erreicht und bekommt deshalb $bonusPoints Punkte abgezogen!',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get end_game => 'Spiel beenden';
|
||||
|
||||
@override
|
||||
String get delete_game => 'Spiel löschen';
|
||||
@@ -169,7 +228,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
'Das Spiel hat bisher noch keine ID zugewiesen bekommen. Falls du das Spiel löschen möchtest, mache das bitte über das Hauptmenü. Alle neu erstellten Spiele haben eine ID.';
|
||||
|
||||
@override
|
||||
String get game_process => 'Spielverlauf';
|
||||
String get end_game_title => 'Spiel beenden?';
|
||||
|
||||
@override
|
||||
String get end_game_message =>
|
||||
'Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistiken';
|
||||
|
||||
@override
|
||||
String get point_overview => 'Punkteübersicht';
|
||||
|
||||
@override
|
||||
String get scoring_history => 'Spielverlauf';
|
||||
|
||||
@override
|
||||
String get empty_graph_text =>
|
||||
'Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.';
|
||||
|
||||
@override
|
||||
String get settings => 'Einstellungen';
|
||||
@@ -177,14 +253,11 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get cabo_penalty => 'Cabo-Strafe';
|
||||
|
||||
@override
|
||||
String get cabo_penalty_subtitle => '... für falsches Cabo sagen';
|
||||
|
||||
@override
|
||||
String get point_limit => 'Punkte-Limit';
|
||||
|
||||
@override
|
||||
String get point_limit_subtitle => '... hier ist Schluss';
|
||||
String get standard_mode => 'Standard-Modus';
|
||||
|
||||
@override
|
||||
String get reset_to_default => 'Auf Standard zurücksetzen';
|
||||
@@ -193,10 +266,23 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get game_data => 'Spieldaten';
|
||||
|
||||
@override
|
||||
String get import_data => 'Daten importieren';
|
||||
String get import_data => 'Spieldaten importieren';
|
||||
|
||||
@override
|
||||
String get export_data => 'Daten exportieren';
|
||||
String get export_data => 'Spieldaten exportieren';
|
||||
|
||||
@override
|
||||
String get delete_data => 'Alle Spieldaten löschen';
|
||||
|
||||
@override
|
||||
String get delete_data_title => 'Spieldaten löschen?';
|
||||
|
||||
@override
|
||||
String get delete_data_message =>
|
||||
'Bist du sicher, dass du alle Spieldaten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.';
|
||||
|
||||
@override
|
||||
String get app => 'App';
|
||||
|
||||
@override
|
||||
String get import_success_title => 'Import erfolgreich';
|
||||
@@ -237,14 +323,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get create_issue => 'Issue erstellen';
|
||||
|
||||
@override
|
||||
String get wiki => 'Wiki';
|
||||
|
||||
@override
|
||||
String get app_version => 'App-Version';
|
||||
|
||||
@override
|
||||
String get build => 'Build';
|
||||
String get privacy_policy => 'Datenschutzerklärung';
|
||||
|
||||
@override
|
||||
String get load_version => 'Lade Version...';
|
||||
String get build => 'Build-Nr.';
|
||||
|
||||
@override
|
||||
String get loading => 'Lädt...';
|
||||
|
||||
@override
|
||||
String get about_text =>
|
||||
@@ -61,7 +61,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get empty_text_2 =>
|
||||
'Add a new round using the button in the top right corner.';
|
||||
'Create a new game using the button in the top right.';
|
||||
|
||||
@override
|
||||
String get delete_game_title => 'Delete game?';
|
||||
@@ -71,6 +71,35 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Are you sure you want to delete the game \"$gameTitle\"? This action cannot be undone.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pre_rating_title => 'Do you like the app?';
|
||||
|
||||
@override
|
||||
String get pre_rating_message =>
|
||||
'Feedback helps me to continuously improve the app. Thank you!';
|
||||
|
||||
@override
|
||||
String get yes => 'Yes';
|
||||
|
||||
@override
|
||||
String get no => 'No';
|
||||
|
||||
@override
|
||||
String get bad_rating_title => 'Not satisfied?';
|
||||
|
||||
@override
|
||||
String get bad_rating_message =>
|
||||
'If you are not satisfied with the app, please let me know before leaving a bad rating. I will try to fix the issue as soon as possible.';
|
||||
|
||||
@override
|
||||
String get contact_email => 'Contact via E-Mail';
|
||||
|
||||
@override
|
||||
String get email_subject => 'Feedback: Cabo Counter App';
|
||||
|
||||
@override
|
||||
String get email_body => 'I have the following feedback...';
|
||||
|
||||
@override
|
||||
String get overview => 'Overview';
|
||||
|
||||
@@ -90,31 +119,32 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get create_game => 'Create Game';
|
||||
|
||||
@override
|
||||
String get max_players_title => 'Maximum reached';
|
||||
String get max_players_title => 'Player Limit Reached';
|
||||
|
||||
@override
|
||||
String get max_players_message => 'A maximum of 5 players can be added.';
|
||||
String get max_players_message => 'You can add a maximum of 5 players.';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_title => 'No Title';
|
||||
String get no_gameTitle_title => 'Missing Game Title';
|
||||
|
||||
@override
|
||||
String get no_gameTitle_message => 'You must enter a title for the game.';
|
||||
String get no_gameTitle_message => 'Please enter a title for your game.';
|
||||
|
||||
@override
|
||||
String get no_mode_title => 'No Mode';
|
||||
String get no_mode_title => 'Game Mode Required';
|
||||
|
||||
@override
|
||||
String get no_mode_message => 'You must select a game mode.';
|
||||
String get no_mode_message => 'Please select a game mode to continue';
|
||||
|
||||
@override
|
||||
String get min_players_title => 'Too few players';
|
||||
String get min_players_title => 'Too Few Players';
|
||||
|
||||
@override
|
||||
String get min_players_message => 'At least 2 players must be added.';
|
||||
String get min_players_message =>
|
||||
'At least 2 players are required to start the game.';
|
||||
|
||||
@override
|
||||
String get no_name_title => 'No Name';
|
||||
String get no_name_title => 'Missing Player Names';
|
||||
|
||||
@override
|
||||
String get no_name_message => 'Each player must have a name.';
|
||||
@@ -122,24 +152,36 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get select_game_mode => 'Select game mode';
|
||||
|
||||
@override
|
||||
String get no_mode_selected => 'No mode selected';
|
||||
|
||||
@override
|
||||
String get no_default_mode => 'No default mode';
|
||||
|
||||
@override
|
||||
String get no_default_description => 'The default mode gets reset.';
|
||||
|
||||
@override
|
||||
String point_limit_description(int pointLimit) {
|
||||
return 'The game ends when a player reaches more than $pointLimit points.';
|
||||
return 'The game ends when a player scores more than $pointLimit points.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get unlimited_description =>
|
||||
'There is no limit. The game continues until you decide to stop.';
|
||||
'The game continues until you decide to stop playing';
|
||||
|
||||
@override
|
||||
String get results => 'Results';
|
||||
|
||||
@override
|
||||
String get who_said_cabo => 'Who said CABO?';
|
||||
String get who_said_cabo => 'Who called Cabo?';
|
||||
|
||||
@override
|
||||
String get kamikaze => 'Kamikaze';
|
||||
|
||||
@override
|
||||
String get who_has_kamikaze => 'Who has Kamikaze?';
|
||||
|
||||
@override
|
||||
String get done => 'Done';
|
||||
|
||||
@@ -147,7 +189,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get next_round => 'Next Round';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistics';
|
||||
String get bonus_points_title => 'Bonus-Points!';
|
||||
|
||||
@override
|
||||
String bonus_points_message(
|
||||
int playerCount, String names, int pointLimit, int bonusPoints) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
playerCount,
|
||||
locale: localeName,
|
||||
other:
|
||||
'$names have reached exactly the point limit of $pointLimit points and therefore get $bonusPoints points deducted!',
|
||||
one:
|
||||
'$names has reached exactly the point limit of $pointLimit points and therefore gets $bonusPoints points deducted!',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get end_game => 'End Game';
|
||||
|
||||
@override
|
||||
String get delete_game => 'Delete Game';
|
||||
@@ -166,7 +225,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'The game has not yet been assigned an ID. If you want to delete the game, please do so via the main menu. All newly created games have an ID.';
|
||||
|
||||
@override
|
||||
String get game_process => 'Spielverlauf';
|
||||
String get end_game_title => 'End the game?';
|
||||
|
||||
@override
|
||||
String get end_game_message =>
|
||||
'Do you want to end the game? The game gets marked as finished and cannot be continued.';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistics';
|
||||
|
||||
@override
|
||||
String get point_overview => 'Point Overview';
|
||||
|
||||
@override
|
||||
String get scoring_history => 'Scoring History';
|
||||
|
||||
@override
|
||||
String get empty_graph_text =>
|
||||
'You must play at least one round for the game progress graph to be displayed.';
|
||||
|
||||
@override
|
||||
String get settings => 'Settings';
|
||||
@@ -174,14 +250,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get cabo_penalty => 'Cabo Penalty';
|
||||
|
||||
@override
|
||||
String get cabo_penalty_subtitle => '... for falsely calling Cabo.';
|
||||
|
||||
@override
|
||||
String get point_limit => 'Point Limit';
|
||||
|
||||
@override
|
||||
String get point_limit_subtitle => '... the game ends here.';
|
||||
String get standard_mode => 'Default Mode';
|
||||
|
||||
@override
|
||||
String get reset_to_default => 'Reset to Default';
|
||||
@@ -195,6 +268,19 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get export_data => 'Export Data';
|
||||
|
||||
@override
|
||||
String get delete_data => 'Delete all Game Data';
|
||||
|
||||
@override
|
||||
String get delete_data_title => 'Delete game data?';
|
||||
|
||||
@override
|
||||
String get delete_data_message =>
|
||||
'Are you sure you want to delete all game data? This action cannot be undone.';
|
||||
|
||||
@override
|
||||
String get app => 'App';
|
||||
|
||||
@override
|
||||
String get import_success_title => 'Import successful';
|
||||
|
||||
@@ -234,14 +320,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get create_issue => 'Create Issue';
|
||||
|
||||
@override
|
||||
String get wiki => 'Wiki';
|
||||
|
||||
@override
|
||||
String get app_version => 'App Version';
|
||||
|
||||
@override
|
||||
String get build => 'Build';
|
||||
String get privacy_policy => 'Privacy Policy';
|
||||
|
||||
@override
|
||||
String get load_version => 'Loading version...';
|
||||
String get build => 'Build No.';
|
||||
|
||||
@override
|
||||
String get loading => 'Loading...';
|
||||
|
||||
@override
|
||||
String get about_text =>
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/tab_view.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/views/tab_view.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -12,8 +12,7 @@ Future<void> main() async {
|
||||
await SystemChrome.setPreferredOrientations(
|
||||
[DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
|
||||
await ConfigService.initConfig();
|
||||
Globals.pointLimit = await ConfigService.getPointLimit();
|
||||
Globals.caboPenalty = await ConfigService.getCaboPenalty();
|
||||
await VersionService.init();
|
||||
runApp(const App());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class InformationView extends StatelessWidget {
|
||||
const InformationView({super.key});
|
||||
class AboutView extends StatelessWidget {
|
||||
const AboutView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -28,9 +30,13 @@ class InformationView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).app_version} ${VersionService.getVersionWithBuild()}',
|
||||
style: TextStyle(fontSize: 15, color: Colors.grey[300]),
|
||||
),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 30),
|
||||
const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
||||
child: SizedBox(
|
||||
height: 200,
|
||||
child: Image.asset('assets/cabo_counter-logo_rounded.png'),
|
||||
@@ -54,15 +60,15 @@ class InformationView extends StatelessWidget {
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('https://www.instagram.com/fx.kr')),
|
||||
launchUrl(Uri.parse(Constants.kInstagramLink)),
|
||||
icon: const Icon(FontAwesomeIcons.instagram)),
|
||||
IconButton(
|
||||
onPressed: () => launchUrl(
|
||||
Uri.parse('mailto:felix.kirchner.fk@gmail.com')),
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('mailto:${Constants.kEmail}')),
|
||||
icon: const Icon(CupertinoIcons.envelope)),
|
||||
IconButton(
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse('https://www.github.com/flixcoo')),
|
||||
launchUrl(Uri.parse(Constants.kGithubLink)),
|
||||
icon: const Icon(FontAwesomeIcons.github)),
|
||||
],
|
||||
),
|
||||
@@ -1,10 +1,13 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_manager.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/views/create_game_view.dart';
|
||||
import 'package:cabo_counter/views/graph_view.dart';
|
||||
import 'package:cabo_counter/views/round_view.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/create_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/graph_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/points_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/round_view.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -19,6 +22,8 @@ class ActiveGameView extends StatefulWidget {
|
||||
|
||||
class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
late final GameSession gameSession;
|
||||
late List<int> denseRanks;
|
||||
late List<int> sortedPlayerIndices;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -31,7 +36,9 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
return ListenableBuilder(
|
||||
listenable: gameSession,
|
||||
builder: (context, _) {
|
||||
List<int> sortedPlayerIndices = _getSortedPlayerIndices();
|
||||
sortedPlayerIndices = _getSortedPlayerIndices();
|
||||
denseRanks = _calculateDenseRank(
|
||||
gameSession.playerScores, sortedPlayerIndices);
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(gameSession.gameTitle),
|
||||
@@ -57,7 +64,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
return CupertinoListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
_getPlacementPrefix(index),
|
||||
_getPlacementTextWidget(index),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
gameSession.players[playerIndex],
|
||||
@@ -104,21 +111,46 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
: const Text('\u{23F3}',
|
||||
style: TextStyle(fontSize: 22)),
|
||||
onTap: () async {
|
||||
// ignore: unused_local_variable
|
||||
final val = await Navigator.of(context,
|
||||
rootNavigator: true)
|
||||
.push(
|
||||
CupertinoPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => RoundView(
|
||||
gameSession: gameSession,
|
||||
roundNumber: index + 1),
|
||||
),
|
||||
);
|
||||
_openRoundView(index + 1);
|
||||
},
|
||||
));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).statistics,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).scoring_history,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => GraphView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).point_overview,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => PointsView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
@@ -128,22 +160,31 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
),
|
||||
Column(
|
||||
children: [
|
||||
CupertinoListTile(
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
title: Text(
|
||||
AppLocalizations.of(context).statistics,
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => GraphView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
Visibility(
|
||||
visible: !gameSession.isPointsLimitEnabled,
|
||||
child: CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: gameSession.roundNumber > 1 &&
|
||||
!gameSession.isGameFinished
|
||||
? const TextStyle(color: Colors.white)
|
||||
: const TextStyle(color: Colors.white30),
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
if (gameSession.roundNumber > 1 &&
|
||||
!gameSession.isGameFinished) {
|
||||
_showEndGameDialog();
|
||||
}
|
||||
}),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).delete_game,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
_showDeleteGameDialog().then((value) {
|
||||
if (value) {
|
||||
@@ -157,26 +198,53 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
AppLocalizations.of(context)
|
||||
.new_game_same_settings,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => CreateGameView(
|
||||
gameTitle: gameSession.gameTitle,
|
||||
isPointsLimitEnabled: widget
|
||||
.gameSession
|
||||
.isPointsLimitEnabled,
|
||||
gameMode: widget.gameSession
|
||||
.isPointsLimitEnabled ==
|
||||
true
|
||||
? GameMode.pointLimit
|
||||
: GameMode.unlimited,
|
||||
players: gameSession.players,
|
||||
)));
|
||||
},
|
||||
),
|
||||
CupertinoListTile(
|
||||
title:
|
||||
Text(AppLocalizations.of(context).export_game,
|
||||
style: const TextStyle(
|
||||
color: Colors.white30,
|
||||
)),
|
||||
),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).export_game,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
onTap: () async {
|
||||
final success = await LocalStorageService
|
||||
.exportSingleGameSession(
|
||||
widget.gameSession);
|
||||
if (!success && context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context)
|
||||
.export_error_title),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.export_error_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).ok),
|
||||
onPressed: () =>
|
||||
Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}),
|
||||
],
|
||||
)
|
||||
],
|
||||
@@ -186,6 +254,40 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
});
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm ending the game.
|
||||
/// If the user confirms, it calls the `endGame` method on the game manager
|
||||
void _showEndGameDialog() {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).end_game_title),
|
||||
content: Text(AppLocalizations.of(context).end_game_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.destructiveRed),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
gameManager.endGame(gameSession.id);
|
||||
});
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of player indices sorted by their scores in
|
||||
/// ascending order.
|
||||
List<int> _getSortedPlayerIndices() {
|
||||
@@ -195,39 +297,50 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
playerIndices.sort((a, b) {
|
||||
int scoreA = gameSession.playerScores[a];
|
||||
int scoreB = gameSession.playerScores[b];
|
||||
return scoreA.compareTo(scoreB);
|
||||
if (scoreA != scoreB) {
|
||||
return scoreA.compareTo(scoreB);
|
||||
}
|
||||
return a.compareTo(b);
|
||||
});
|
||||
return playerIndices;
|
||||
}
|
||||
|
||||
/// Returns a widget that displays the placement prefix based on the index.
|
||||
/// First three places are represented by medals, and the rest are numbered.
|
||||
/// [index] is the index of the player in the descending sorted list.
|
||||
Widget _getPlacementPrefix(int index) {
|
||||
switch (index) {
|
||||
case 0:
|
||||
return const Text(
|
||||
'\u{1F947}',
|
||||
style: TextStyle(fontSize: 22),
|
||||
);
|
||||
/// Calculates the dense rank for a player based on their index in the sorted list of players.
|
||||
List<int> _calculateDenseRank(
|
||||
List<int> playerScores, List<int> sortedIndices) {
|
||||
List<int> denseRanks = [];
|
||||
int rank = 1;
|
||||
for (int i = 0; i < sortedIndices.length; i++) {
|
||||
if (i > 0) {
|
||||
int prevScore = playerScores[sortedIndices[i - 1]];
|
||||
int currScore = playerScores[sortedIndices[i]];
|
||||
if (currScore != prevScore) {
|
||||
rank++;
|
||||
}
|
||||
}
|
||||
denseRanks.add(rank);
|
||||
}
|
||||
return denseRanks;
|
||||
}
|
||||
|
||||
/// Returns a text widget representing the placement text based on the given placement number.
|
||||
/// [index] is the index of the player in [players] list,
|
||||
Text _getPlacementTextWidget(int index) {
|
||||
int placement = denseRanks[index];
|
||||
switch (placement) {
|
||||
case 1:
|
||||
return const Text(
|
||||
'\u{1F948}',
|
||||
style: TextStyle(fontSize: 22),
|
||||
);
|
||||
return const Text('\u{1F947}', style: TextStyle(fontSize: 22)); // 🥇
|
||||
case 2:
|
||||
return const Text(
|
||||
'\u{1F949}',
|
||||
style: TextStyle(fontSize: 22),
|
||||
);
|
||||
return const Text('\u{1F948}', style: TextStyle(fontSize: 22)); // 🥈
|
||||
case 3:
|
||||
return const Text('\u{1F949}', style: TextStyle(fontSize: 22)); // 🥉
|
||||
default:
|
||||
return Text(
|
||||
' ${index + 1}.',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
);
|
||||
return Text(' $placement.',
|
||||
style: const TextStyle(fontWeight: FontWeight.bold));
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm deleting the game session.
|
||||
Future<bool> _showDeleteGameDialog() async {
|
||||
return await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
@@ -260,6 +373,8 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
false;
|
||||
}
|
||||
|
||||
/// Removes the game session in the game manager and navigates back to the previous screen.
|
||||
/// If the game session does not exist in the game list, it shows an error dialog.
|
||||
Future<void> _removeGameSession(GameSession gameSession) async {
|
||||
if (gameManager.gameExistsInGameList(gameSession.id)) {
|
||||
Navigator.pop(context);
|
||||
@@ -284,4 +399,25 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively opens the RoundView for the specified round number.
|
||||
/// It starts with the given [roundNumber] and continues to open the next round
|
||||
/// until the user navigates back or the round number is invalid.
|
||||
void _openRoundView(int roundNumber) async {
|
||||
final val = await Navigator.of(context, rootNavigator: true).push(
|
||||
CupertinoPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => RoundView(
|
||||
gameSession: gameSession,
|
||||
roundNumber: roundNumber,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (val != null && val >= 0) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
_openRoundView(val);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,31 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_manager.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/views/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum CreateStatus {
|
||||
noGameTitle,
|
||||
noModeSelected,
|
||||
minPlayers,
|
||||
maxPlayers,
|
||||
noPlayerName,
|
||||
}
|
||||
|
||||
class CreateGameView extends StatefulWidget {
|
||||
final GameMode gameMode;
|
||||
final String? gameTitle;
|
||||
final bool? isPointsLimitEnabled;
|
||||
final List<String>? players;
|
||||
|
||||
const CreateGameView({
|
||||
super.key,
|
||||
this.gameTitle,
|
||||
this.isPointsLimitEnabled,
|
||||
this.players,
|
||||
required this.gameMode,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -35,14 +43,15 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
/// Maximum number of players allowed in the game.
|
||||
final int maxPlayers = 5;
|
||||
|
||||
/// Variable to store whether the points limit feature is enabled.
|
||||
bool? _isPointsLimitEnabled;
|
||||
/// Variable to hold the selected game mode.
|
||||
late GameMode gameMode;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_isPointsLimitEnabled = widget.isPointsLimitEnabled;
|
||||
gameMode = widget.gameMode;
|
||||
|
||||
_gameTitleTextController.text = widget.gameTitle ?? '';
|
||||
|
||||
if (widget.players != null) {
|
||||
@@ -84,7 +93,6 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
controller: _gameTitleTextController,
|
||||
),
|
||||
),
|
||||
// Spielmodus-Auswahl mit Chevron
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoTextField(
|
||||
@@ -94,10 +102,10 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
suffix: Row(
|
||||
children: [
|
||||
Text(
|
||||
_isPointsLimitEnabled == null
|
||||
? AppLocalizations.of(context).select_mode
|
||||
: (_isPointsLimitEnabled!
|
||||
? '${Globals.pointLimit} ${AppLocalizations.of(context).points}'
|
||||
gameMode == GameMode.none
|
||||
? AppLocalizations.of(context).no_mode_selected
|
||||
: (gameMode == GameMode.pointLimit
|
||||
? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
@@ -109,16 +117,15 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: Globals.pointLimit,
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
showDeselection: false,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (selectedMode != null) {
|
||||
setState(() {
|
||||
_isPointsLimitEnabled = selectedMode;
|
||||
});
|
||||
}
|
||||
setState(() {
|
||||
gameMode = selectedMode ?? gameMode;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -198,64 +205,51 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
style:
|
||||
TextStyle(color: CustomTheme.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (_playerNameTextControllers.length <
|
||||
maxPlayers) {
|
||||
setState(() {
|
||||
_playerNameTextControllers
|
||||
.add(TextEditingController());
|
||||
});
|
||||
} else {
|
||||
_showDialog((
|
||||
AppLocalizations.of(context).max_players_title,
|
||||
AppLocalizations.of(context).max_players_message
|
||||
));
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
if (_playerNameTextControllers.length < maxPlayers) {
|
||||
setState(() {
|
||||
_playerNameTextControllers
|
||||
.add(TextEditingController());
|
||||
});
|
||||
} else {
|
||||
showFeedbackDialog(CreateStatus.maxPlayers);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
// Player name input field
|
||||
return Padding(
|
||||
key: ValueKey('player_${index + 1}'),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4.0, horizontal: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circle_fill,
|
||||
color: CupertinoColors.destructiveRed,
|
||||
size: 25,
|
||||
} else {
|
||||
// Spieler-Einträge
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0, horizontal: 5),
|
||||
child: Row(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: const Icon(
|
||||
CupertinoIcons.minus_circle_fill,
|
||||
color: CupertinoColors.destructiveRed,
|
||||
size: 25,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_playerNameTextControllers[index].dispose();
|
||||
_playerNameTextControllers.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_playerNameTextControllers[index].dispose();
|
||||
_playerNameTextControllers.removeAt(index);
|
||||
});
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: _playerNameTextControllers[index],
|
||||
maxLength: 12,
|
||||
placeholder:
|
||||
'${AppLocalizations.of(context).player} ${index + 1}',
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: const Icon(
|
||||
CupertinoIcons.line_horizontal_3,
|
||||
size: 20,
|
||||
color: CupertinoColors.systemGrey,
|
||||
Expanded(
|
||||
child: CupertinoTextField(
|
||||
controller: _playerNameTextControllers[index],
|
||||
maxLength: 12,
|
||||
placeholder:
|
||||
'${AppLocalizations.of(context).player} ${index + 1}',
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -264,67 +258,67 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
},
|
||||
),
|
||||
),
|
||||
Center(
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).create_game,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.activeGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
if (_gameTitleTextController.text == '') {
|
||||
showFeedbackDialog(CreateStatus.noGameTitle);
|
||||
return;
|
||||
}
|
||||
if (gameMode == GameMode.none) {
|
||||
showFeedbackDialog(CreateStatus.noModeSelected);
|
||||
return;
|
||||
}
|
||||
if (_playerNameTextControllers.length < 2) {
|
||||
showFeedbackDialog(CreateStatus.minPlayers);
|
||||
return;
|
||||
}
|
||||
if (!everyPlayerHasAName()) {
|
||||
showFeedbackDialog(CreateStatus.noPlayerName);
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> players = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
players.add(controller.text);
|
||||
}
|
||||
|
||||
bool isPointsLimitEnabled = gameMode == GameMode.pointLimit;
|
||||
|
||||
GameSession gameSession = GameSession(
|
||||
createdAt: DateTime.now(),
|
||||
gameTitle: _gameTitleTextController.text,
|
||||
players: players,
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
caboPenalty: ConfigService.getCaboPenalty(),
|
||||
isPointsLimitEnabled: isPointsLimitEnabled,
|
||||
);
|
||||
final index = await gameManager.addGameSession(gameSession);
|
||||
final session = gameManager.gameList[index];
|
||||
if (context.mounted) {
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
ActiveGameView(gameSession: session)));
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
))));
|
||||
}
|
||||
|
||||
void _checkAllGameAttributes() {
|
||||
if (_gameTitleTextController.text == '') {
|
||||
_showDialog((
|
||||
AppLocalizations.of(context).no_gameTitle_title,
|
||||
AppLocalizations.of(context).no_gameTitle_message
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isPointsLimitEnabled == null) {
|
||||
_showDialog(
|
||||
(
|
||||
AppLocalizations.of(context).no_mode_title,
|
||||
AppLocalizations.of(context).no_mode_message
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_playerNameTextControllers.length < 2) {
|
||||
_showDialog(
|
||||
(
|
||||
AppLocalizations.of(context).min_players_title,
|
||||
AppLocalizations.of(context).min_players_message
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!everyPlayerHasAName()) {
|
||||
_showDialog((
|
||||
AppLocalizations.of(context).no_name_title,
|
||||
AppLocalizations.of(context).no_name_message
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
_createGame();
|
||||
}
|
||||
|
||||
void _showDialog((String, String) content) {
|
||||
final (title, message) = content;
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createGame() async {
|
||||
List<String> players = [];
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
@@ -347,6 +341,60 @@ class _CreateGameViewState extends State<CreateGameView> {
|
||||
builder: (context) => ActiveGameView(gameSession: session)));
|
||||
}
|
||||
|
||||
/// Displays a feedback dialog based on the [CreateStatus].
|
||||
void showFeedbackDialog(CreateStatus status) {
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the title and message for the dialog based on the [CreateStatus].
|
||||
(String, String) _getDialogContent(CreateStatus status) {
|
||||
switch (status) {
|
||||
case CreateStatus.noGameTitle:
|
||||
return (
|
||||
AppLocalizations.of(context).no_gameTitle_title,
|
||||
AppLocalizations.of(context).no_gameTitle_message
|
||||
);
|
||||
case CreateStatus.noModeSelected:
|
||||
return (
|
||||
AppLocalizations.of(context).no_mode_title,
|
||||
AppLocalizations.of(context).no_mode_message
|
||||
);
|
||||
|
||||
case CreateStatus.minPlayers:
|
||||
return (
|
||||
AppLocalizations.of(context).min_players_title,
|
||||
AppLocalizations.of(context).min_players_message
|
||||
);
|
||||
case CreateStatus.maxPlayers:
|
||||
return (
|
||||
AppLocalizations.of(context).max_players_title,
|
||||
AppLocalizations.of(context).max_players_message
|
||||
);
|
||||
case CreateStatus.noPlayerName:
|
||||
return (
|
||||
AppLocalizations.of(context).no_name_title,
|
||||
AppLocalizations.of(context).no_name_message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if every player has a name.
|
||||
/// Returns true if all players have a name, false otherwise.
|
||||
bool everyPlayerHasAName() {
|
||||
for (var controller in _playerNameTextControllers) {
|
||||
if (controller.text == '') {
|
||||
132
lib/presentation/views/graph_view.dart
Normal file
132
lib/presentation/views/graph_view.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class GraphView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
const GraphView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
/// List of colors for the graph lines.
|
||||
final List<Color> lineColors = [
|
||||
CustomTheme.graphColor1,
|
||||
CustomTheme.graphColor2,
|
||||
CustomTheme.graphColor3,
|
||||
CustomTheme.graphColor4,
|
||||
CustomTheme.graphColor5
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).scoring_history),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: widget.gameSession.roundNumber > 1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: SfCartesianChart(
|
||||
enableAxisAnimation: true,
|
||||
legend: const Legend(
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
isVisible: true,
|
||||
position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(
|
||||
labelStyle: TextStyle(fontWeight: FontWeight.bold),
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
),
|
||||
primaryYAxis: NumericAxis(
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.bold),
|
||||
labelAlignment: LabelAlignment.center,
|
||||
labelPosition: ChartDataLabelPosition.inside,
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
axisLabelFormatter: (AxisLabelRenderDetails details) {
|
||||
if (details.value == 0) {
|
||||
return ChartAxisLabel('', const TextStyle());
|
||||
}
|
||||
return ChartAxisLabel(
|
||||
'${details.value.toInt()}', const TextStyle());
|
||||
},
|
||||
),
|
||||
series: getCumulativeScores(),
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Center(
|
||||
child: Icon(CupertinoIcons.chart_bar_alt_fill, size: 60),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).empty_graph_text,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
|
||||
/// Returns a list of LineSeries representing the cumulative scores of each player.
|
||||
/// Each series contains data points for each round, showing the cumulative score up to that round.
|
||||
/// The x-axis represents the round number, and the y-axis represents the cumulative score.
|
||||
List<LineSeries<(int, num), int>> getCumulativeScores() {
|
||||
final rounds = widget.gameSession.roundList;
|
||||
final playerCount = widget.gameSession.players.length;
|
||||
final playerNames = widget.gameSession.players;
|
||||
|
||||
List<List<int>> cumulativeScores = List.generate(playerCount, (_) => []);
|
||||
List<int> runningTotals = List.filled(playerCount, 0);
|
||||
|
||||
for (var round in rounds) {
|
||||
for (int i = 0; i < playerCount; i++) {
|
||||
runningTotals[i] += round.scoreUpdates[i];
|
||||
cumulativeScores[i].add(runningTotals[i]);
|
||||
}
|
||||
}
|
||||
|
||||
const double jitterStep = 0.03;
|
||||
|
||||
/// Create a list of LineSeries for each player
|
||||
/// Each series contains data points for each round
|
||||
return List.generate(playerCount, (i) {
|
||||
final data = List.generate(
|
||||
cumulativeScores[i].length + 1,
|
||||
(j) => (
|
||||
j,
|
||||
j == 0 || cumulativeScores[i][j - 1] == 0
|
||||
? 0 // 0 points at the start of the game or when the value is 0 (don't subtract jitter step)
|
||||
|
||||
// Adds a small jitter to the cumulative scores to prevent overlapping data points in the graph.
|
||||
// The jitter is centered around zero by subtracting playerCount ~/ 2 from the player index i.
|
||||
: cumulativeScores[i][j - 1] + (i - playerCount ~/ 2) * jitterStep
|
||||
),
|
||||
);
|
||||
|
||||
/// Create a LineSeries for the player
|
||||
/// The xValueMapper maps the round number, and the yValueMapper maps the cumulative score.
|
||||
return LineSeries<(int, num), int>(
|
||||
name: playerNames[i],
|
||||
dataSource: data,
|
||||
xValueMapper: (record, _) => record.$1,
|
||||
yValueMapper: (record, _) => record.$2,
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
color: lineColors[i],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_manager.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/create_game_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/settings_view.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/views/create_game_view.dart';
|
||||
import 'package:cabo_counter/views/settings_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
enum PreRatingDialogDecision { yes, no, cancel }
|
||||
|
||||
enum BadRatingDialogDecision { email, cancel }
|
||||
|
||||
class MainMenuView extends StatefulWidget {
|
||||
const MainMenuView({super.key});
|
||||
@@ -29,6 +35,17 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
});
|
||||
});
|
||||
gameManager.addListener(_updateView);
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await Constants.rateMyApp.init();
|
||||
|
||||
if (Constants.rateMyApp.shouldOpenDialog &&
|
||||
Constants.appDevPhase != 'Beta') {
|
||||
await Future.delayed(const Duration(milliseconds: 600));
|
||||
if (!mounted) return;
|
||||
_handleFeedbackDialog(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _updateView() {
|
||||
@@ -55,16 +72,15 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
});
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.settings, size: 30)),
|
||||
middle: const Text('Cabo Counter'),
|
||||
middle: Text(AppLocalizations.of(context).app_name),
|
||||
trailing: IconButton(
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const CreateGameView(),
|
||||
),
|
||||
)
|
||||
},
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => CreateGameView(
|
||||
gameMode: ConfigService.getGameMode()),
|
||||
),
|
||||
),
|
||||
icon: const Icon(CupertinoIcons.add)),
|
||||
),
|
||||
child: CupertinoPageScaffold(
|
||||
@@ -73,17 +89,16 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
? const Center(child: CupertinoActivityIndicator())
|
||||
: gameManager.gameList.isEmpty
|
||||
? Column(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center, // Oben ausrichten
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 30), // Abstand von oben
|
||||
const SizedBox(height: 30),
|
||||
Center(
|
||||
child: GestureDetector(
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const CreateGameView(),
|
||||
builder: (context) => CreateGameView(
|
||||
gameMode: ConfigService.getGameMode()),
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
@@ -92,7 +107,7 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 10), // Abstand von oben
|
||||
const SizedBox(height: 10),
|
||||
Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 70),
|
||||
@@ -104,15 +119,22 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
),
|
||||
],
|
||||
)
|
||||
: ListView.builder(
|
||||
: ListView.separated(
|
||||
itemCount: gameManager.gameList.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: CustomTheme.white.withAlpha(50),
|
||||
indent: 50,
|
||||
endIndent: 50,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final session = gameManager.gameList[index];
|
||||
return ListenableBuilder(
|
||||
listenable: session,
|
||||
builder: (context, _) {
|
||||
return Dismissible(
|
||||
key: Key(session.gameTitle),
|
||||
key: Key(session.id),
|
||||
background: Container(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
alignment: Alignment.centerRight,
|
||||
@@ -125,14 +147,12 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
),
|
||||
direction: DismissDirection.endToStart,
|
||||
confirmDismiss: (direction) async {
|
||||
final String gameTitle = gameManager
|
||||
.gameList[index].gameTitle;
|
||||
return await _showDeleteGamePopup(
|
||||
gameTitle);
|
||||
context, session.gameTitle);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
gameManager
|
||||
.removeGameSessionByIndex(index);
|
||||
.removeGameSessionById(session.id);
|
||||
},
|
||||
dismissThresholds: const {
|
||||
DismissDirection.startToEnd: 0.6
|
||||
@@ -197,47 +217,151 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
|
||||
/// Translates the game mode boolean into the corresponding String.
|
||||
/// If [pointLimit] is true, it returns '101 Punkte', otherwise it returns 'Unbegrenzt'.
|
||||
String _translateGameMode(bool pointLimit) {
|
||||
if (pointLimit) {
|
||||
return '${Globals.pointLimit} ${AppLocalizations.of(context).points}';
|
||||
String _translateGameMode(bool isPointLimitEnabled) {
|
||||
if (isPointLimitEnabled) {
|
||||
return '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}';
|
||||
}
|
||||
return AppLocalizations.of(context).unlimited;
|
||||
}
|
||||
|
||||
/// Handles the feedback dialog when the conditions for rating are met.
|
||||
/// It shows a dialog asking the user if they like the app,
|
||||
/// and based on their response, it either opens the rating dialog or an email client for feedback.
|
||||
Future<void> _handleFeedbackDialog(BuildContext context) async {
|
||||
final String emailSubject = AppLocalizations.of(context).email_subject;
|
||||
final String emailBody = AppLocalizations.of(context).email_body;
|
||||
|
||||
final Uri emailUri = Uri(
|
||||
scheme: 'mailto',
|
||||
path: Constants.kEmail,
|
||||
query: 'subject=$emailSubject'
|
||||
'&body=$emailBody',
|
||||
);
|
||||
|
||||
PreRatingDialogDecision preRatingDecision =
|
||||
await _showPreRatingDialog(context);
|
||||
BadRatingDialogDecision badRatingDecision = BadRatingDialogDecision.cancel;
|
||||
|
||||
// so that the bad rating dialog is not shown immediately
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
switch (preRatingDecision) {
|
||||
case PreRatingDialogDecision.yes:
|
||||
if (context.mounted) Constants.rateMyApp.showStarRateDialog(context);
|
||||
break;
|
||||
case PreRatingDialogDecision.no:
|
||||
if (context.mounted) {
|
||||
badRatingDecision = await _showBadRatingDialog(context);
|
||||
}
|
||||
if (badRatingDecision == BadRatingDialogDecision.email) {
|
||||
if (context.mounted) {
|
||||
launchUrl(emailUri);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PreRatingDialogDecision.cancel:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Shows a confirmation dialog to delete all game sessions.
|
||||
/// Returns true if the user confirms the deletion, false otherwise.
|
||||
/// [gameTitle] is the title of the game session to be deleted.
|
||||
Future<bool> _showDeleteGamePopup(String gameTitle) async {
|
||||
bool? shouldDelete = await showCupertinoDialog<bool>(
|
||||
Future<bool> _showDeleteGamePopup(
|
||||
BuildContext context, String gameTitle) async {
|
||||
return await showCupertinoDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete_game_title),
|
||||
content: Text(
|
||||
AppLocalizations.of(context).delete_game_message(gameTitle)),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
title: Text(
|
||||
AppLocalizations.of(context).delete_game_title,
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).delete,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.red),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.delete_game_message(gameTitle)),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
isDefaultAction: true,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).delete,
|
||||
),
|
||||
)
|
||||
]);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
return shouldDelete;
|
||||
}
|
||||
|
||||
/// Shows a dialog asking the user if they like the app.
|
||||
/// Returns the user's decision as an integer.
|
||||
/// - PRE_RATING_DIALOG_YES: User likes the app and wants to rate it.
|
||||
/// - PRE_RATING_DIALOG_NO: User does not like the app and wants to provide feedback.
|
||||
/// - PRE_RATING_DIALOG_CANCEL: User cancels the dialog.
|
||||
Future<PreRatingDialogDecision> _showPreRatingDialog(
|
||||
BuildContext context) async {
|
||||
return await showCupertinoDialog<PreRatingDialogDecision>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).pre_rating_title),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).pre_rating_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
onPressed: () => Navigator.of(context)
|
||||
.pop(PreRatingDialogDecision.yes),
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).yes),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () =>
|
||||
Navigator.of(context).pop(PreRatingDialogDecision.no),
|
||||
child: Text(AppLocalizations.of(context).no),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
isDestructiveAction: true,
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
)
|
||||
],
|
||||
)) ??
|
||||
PreRatingDialogDecision.cancel;
|
||||
}
|
||||
|
||||
/// Shows a dialog asking the user for feedback if they do not like the app.
|
||||
/// Returns the user's decision as an integer.
|
||||
/// - BAD_RATING_DIALOG_EMAIL: User wants to send an email with feedback.
|
||||
/// - BAD_RATING_DIALOG_CANCEL: User cancels the dialog.
|
||||
Future<BadRatingDialogDecision> _showBadRatingDialog(
|
||||
BuildContext context) async {
|
||||
return await showCupertinoDialog<BadRatingDialogDecision>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).bad_rating_title),
|
||||
content:
|
||||
Text(AppLocalizations.of(context).bad_rating_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
isDefaultAction: true,
|
||||
onPressed: () => Navigator.of(context)
|
||||
.pop(BadRatingDialogDecision.email),
|
||||
child: Text(AppLocalizations.of(context).contact_email),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(AppLocalizations.of(context).cancel))
|
||||
],
|
||||
)) ??
|
||||
BadRatingDialogDecision.cancel;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,10 +1,18 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
enum GameMode {
|
||||
none,
|
||||
pointLimit,
|
||||
unlimited,
|
||||
}
|
||||
|
||||
class ModeSelectionMenu extends StatelessWidget {
|
||||
final int pointLimit;
|
||||
const ModeSelectionMenu({super.key, required this.pointLimit});
|
||||
final bool showDeselection;
|
||||
const ModeSelectionMenu(
|
||||
{super.key, required this.pointLimit, required this.showDeselection});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -26,12 +34,12 @@ class ModeSelectionMenu extends StatelessWidget {
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, true);
|
||||
Navigator.pop(context, GameMode.pointLimit);
|
||||
},
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).unlimited,
|
||||
style: CustomTheme.modeTitle),
|
||||
@@ -41,10 +49,27 @@ class ModeSelectionMenu extends StatelessWidget {
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, false);
|
||||
Navigator.pop(context, GameMode.unlimited);
|
||||
},
|
||||
),
|
||||
),
|
||||
Visibility(
|
||||
visible: showDeselection,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 16, 0, 0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).no_default_mode,
|
||||
style: CustomTheme.modeTitle),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).no_default_description,
|
||||
style: CustomTheme.modeDescription,
|
||||
maxLines: 3,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.pop(context, GameMode.none);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
141
lib/presentation/views/points_view.dart
Normal file
141
lib/presentation/views/points_view.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PointsView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
const PointsView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<PointsView> createState() => _PointsViewState();
|
||||
}
|
||||
|
||||
class _PointsViewState extends State<PointsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).point_overview),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: DataTable(
|
||||
dataRowMinHeight: 60,
|
||||
dataRowMaxHeight: 60,
|
||||
dividerThickness: 0.5,
|
||||
columnSpacing: 20,
|
||||
columns: [
|
||||
const DataColumn(
|
||||
numeric: true,
|
||||
headingRowAlignment: MainAxisAlignment.center,
|
||||
label: Text(
|
||||
'#',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
columnWidth: IntrinsicColumnWidth(flex: 0.5)),
|
||||
...widget.gameSession.players.map(
|
||||
(player) => DataColumn(
|
||||
label: FittedBox(
|
||||
fit: BoxFit.fill,
|
||||
child: Text(
|
||||
player,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
)),
|
||||
headingRowAlignment: MainAxisAlignment.center,
|
||||
columnWidth: const IntrinsicColumnWidth(flex: 1)),
|
||||
),
|
||||
],
|
||||
rows: [
|
||||
...List<DataRow>.generate(
|
||||
widget.gameSession.roundList.length,
|
||||
(roundIndex) {
|
||||
final round = widget.gameSession.roundList[roundIndex];
|
||||
return DataRow(
|
||||
cells: [
|
||||
DataCell(Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'${roundIndex + 1}',
|
||||
style: const TextStyle(fontSize: 20),
|
||||
),
|
||||
)),
|
||||
...List.generate(widget.gameSession.players.length,
|
||||
(playerIndex) {
|
||||
final int score = round.scores[playerIndex];
|
||||
final int update = round.scoreUpdates[playerIndex];
|
||||
final bool saidCabo =
|
||||
round.caboPlayerIndex == playerIndex;
|
||||
return DataCell(
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: update <= 0
|
||||
? CustomTheme.pointLossColor
|
||||
: CustomTheme.pointGainColor,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'${update >= 0 ? '+' : ''}$update',
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('$score',
|
||||
style: TextStyle(
|
||||
fontWeight: saidCabo
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
DataRow(
|
||||
cells: [
|
||||
const DataCell(Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'Σ',
|
||||
style:
|
||||
TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
),
|
||||
)),
|
||||
...widget.gameSession.playerScores.map(
|
||||
(score) => DataCell(
|
||||
Center(
|
||||
child: Text(
|
||||
'$score',
|
||||
style: const TextStyle(
|
||||
fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
534
lib/presentation/views/round_view.dart
Normal file
534
lib/presentation/views/round_view.dart
Normal file
@@ -0,0 +1,534 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
|
||||
class RoundView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
final int roundNumber;
|
||||
const RoundView(
|
||||
{super.key, required this.roundNumber, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_RoundViewState createState() => _RoundViewState();
|
||||
}
|
||||
|
||||
class _RoundViewState extends State<RoundView> {
|
||||
/// The current game session.
|
||||
late GameSession gameSession = widget.gameSession;
|
||||
|
||||
/// Index of the player who said CABO.
|
||||
int _caboPlayerIndex = 0;
|
||||
|
||||
/// Index of the player who has Kamikaze.
|
||||
/// Default is null (no Kamikaze player).
|
||||
int? _kamikazePlayerIndex;
|
||||
|
||||
/// List of text controllers for the score text fields.
|
||||
late final List<TextEditingController> _scoreControllerList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => TextEditingController(),
|
||||
);
|
||||
|
||||
/// List of focus nodes for the score text fields.
|
||||
late final List<FocusNode> _focusNodeList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => FocusNode(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
print('=== Runde ${widget.roundNumber} geöffnet ===');
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber ||
|
||||
widget.gameSession.isGameFinished == true) {
|
||||
print(
|
||||
'Diese wurde bereits gespielt, deshalb werden die alten Punktestaende angezeigt');
|
||||
|
||||
// If the current round has already been played, the text fields
|
||||
// are filled with the scores from this round
|
||||
for (int i = 0; i < _scoreControllerList.length; i++) {
|
||||
_scoreControllerList[i].text =
|
||||
gameSession.roundList[widget.roundNumber - 1].scores[i].toString();
|
||||
}
|
||||
_caboPlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].caboPlayerIndex;
|
||||
_kamikazePlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].kamikazePlayerIndex;
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final rotatedPlayers = _getRotatedPlayers();
|
||||
final originalIndices = _getOriginalIndices();
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
transitionBetweenRoutes: true,
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => {
|
||||
LocalStorageService.saveGameSessions(),
|
||||
Navigator.pop(context)
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
middle: Text(AppLocalizations.of(context).results),
|
||||
trailing: Visibility(
|
||||
visible: widget.gameSession.isGameFinished,
|
||||
child: const Icon(
|
||||
CupertinoIcons.lock,
|
||||
size: 25,
|
||||
))),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 100 + bottomInset),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).round} ${widget.roundNumber}',
|
||||
style: CustomTheme.roundTitle),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
AppLocalizations.of(context).who_said_cabo,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.gameSession.players.length > 3 ? 5 : 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 60,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
unselectedColor:
|
||||
CustomTheme.mainElementBackgroundColor,
|
||||
selectedColor: CustomTheme.primaryColor,
|
||||
groupValue: _caboPlayerIndex,
|
||||
children: Map.fromEntries(widget.gameSession.players
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 8,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_caboPlayerIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: rotatedPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
final originalIndex = originalIndices[index];
|
||||
final name = rotatedPlayers[index];
|
||||
bool shouldShowMedal =
|
||||
index == 0 && widget.roundNumber > 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 20),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CupertinoListTile(
|
||||
backgroundColor: CustomTheme.playerTileColor,
|
||||
title: Row(children: [
|
||||
Expanded(
|
||||
child: Row(children: [
|
||||
Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Visibility(
|
||||
visible: shouldShowMedal,
|
||||
child: const SizedBox(width: 10),
|
||||
),
|
||||
Visibility(
|
||||
visible: shouldShowMedal,
|
||||
child: const Icon(FontAwesomeIcons.crown,
|
||||
size: 15))
|
||||
]))
|
||||
]),
|
||||
subtitle: Text(
|
||||
'${widget.gameSession.playerScores[originalIndex]}'
|
||||
' ${AppLocalizations.of(context).points}'),
|
||||
trailing: SizedBox(
|
||||
width: 100,
|
||||
child: CupertinoTextField(
|
||||
maxLength: 3,
|
||||
focusNode: _focusNodeList[originalIndex],
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
signed: true,
|
||||
decimal: false,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textInputAction: index ==
|
||||
widget.gameSession.players.length - 1
|
||||
? TextInputAction.done
|
||||
: TextInputAction.next,
|
||||
controller:
|
||||
_scoreControllerList[originalIndex],
|
||||
placeholder:
|
||||
AppLocalizations.of(context).points,
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (_) =>
|
||||
_focusNextTextfield(originalIndex),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Center(
|
||||
heightFactor: 1,
|
||||
child: CupertinoButton(
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CustomTheme.buttonBackgroundColor,
|
||||
onPressed: () async {
|
||||
if (await _showKamikazeSheet(context)) {
|
||||
if (!context.mounted) return;
|
||||
_endOfRoundNavigation(context, true);
|
||||
}
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context).kamikaze,
|
||||
style: const TextStyle(
|
||||
color: CupertinoColors.destructiveRed,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottomInset,
|
||||
child: KeyboardVisibilityBuilder(builder: (context, visible) {
|
||||
if (!visible) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
color: CustomTheme.mainElementBackgroundColor,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_endOfRoundNavigation(context, false);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).done),
|
||||
),
|
||||
if (!widget.gameSession.isGameFinished)
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_endOfRoundNavigation(context, true);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Gets the index of the player who won the previous round.
|
||||
int _getPreviousRoundWinnerIndex() {
|
||||
if (widget.roundNumber == 1) {
|
||||
return 0; // If it's the first round, there's no previous round, so return 0.
|
||||
}
|
||||
|
||||
final previousRound = widget.gameSession.roundList[widget.roundNumber - 2];
|
||||
final scores = previousRound.scoreUpdates;
|
||||
|
||||
// Find the index of the player with the minimum score
|
||||
int minScore = scores[0];
|
||||
int winnerIndex = 0;
|
||||
|
||||
// Iterate through the scores to find the player with the minimum score
|
||||
for (int i = 1; i < scores.length; i++) {
|
||||
if (scores[i] < minScore) {
|
||||
minScore = scores[i];
|
||||
winnerIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return winnerIndex;
|
||||
}
|
||||
|
||||
/// Rotates the players list based on the previous round's winner.
|
||||
List<String> _getRotatedPlayers() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
return [
|
||||
widget.gameSession.players[winnerIndex],
|
||||
...widget.gameSession.players.sublist(winnerIndex + 1),
|
||||
...widget.gameSession.players.sublist(0, winnerIndex)
|
||||
];
|
||||
}
|
||||
|
||||
/// Gets the original indices of the players by recalculating it from the rotated list.
|
||||
List<int> _getOriginalIndices() {
|
||||
final winnerIndex = _getPreviousRoundWinnerIndex();
|
||||
return [
|
||||
winnerIndex,
|
||||
...List.generate(widget.gameSession.players.length - winnerIndex - 1,
|
||||
(i) => winnerIndex + i + 1),
|
||||
...List.generate(winnerIndex, (i) => i)
|
||||
];
|
||||
}
|
||||
|
||||
/// Shows a Cupertino action sheet to select the player who has Kamikaze.
|
||||
/// It returns true if a player was selected, false if the action was cancelled.
|
||||
Future<bool> _showKamikazeSheet(BuildContext context) async {
|
||||
return await showCupertinoModalPopup<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return CupertinoActionSheet(
|
||||
title: Text(AppLocalizations.of(context).kamikaze),
|
||||
message: Text(AppLocalizations.of(context).who_has_kamikaze),
|
||||
actions: widget.gameSession.players.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return CupertinoActionSheetAction(
|
||||
onPressed: () {
|
||||
_kamikazePlayerIndex = index;
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: Text(name),
|
||||
);
|
||||
}).toList(),
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
isDestructiveAction: true,
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
);
|
||||
},
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
|
||||
/// Focuses the next text field in the list of text fields.
|
||||
/// [index] is the index of the current text field.
|
||||
void _focusNextTextfield(int index) {
|
||||
final originalIndices = _getOriginalIndices();
|
||||
final currentPos = originalIndices.indexOf(index);
|
||||
|
||||
if (currentPos < originalIndices.length - 1) {
|
||||
FocusScope.of(context)
|
||||
.requestFocus(_focusNodeList[originalIndices[currentPos + 1]]);
|
||||
} else {
|
||||
_focusNodeList[index].unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the inputs for the round are valid.
|
||||
/// Returns true if the inputs are valid, false otherwise.
|
||||
/// Round Inputs are valid if every player has a score or
|
||||
/// kamikaze is selected for a player
|
||||
bool _areRoundInputsValid() {
|
||||
if (_areTextFieldsEmpty() && _kamikazePlayerIndex == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Checks if any of the text fields for the players points are empty.
|
||||
/// Returns true if any of the text fields is empty, false otherwise.
|
||||
bool _areTextFieldsEmpty() {
|
||||
for (TextEditingController t in _scoreControllerList) {
|
||||
if (t.text.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Finishes the current round.
|
||||
/// It first determines, ifCalls the [_calculateScoredPoints()] method to calculate the points for
|
||||
/// every player. If the round is the highest round played in this game,
|
||||
/// it expands the player score lists. At the end it updates the score
|
||||
/// array for the game.
|
||||
List<int> _finishRound() {
|
||||
print('====================================');
|
||||
print('Runde ${widget.roundNumber} beendet');
|
||||
// The shown round is smaller than the newest round
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber) {
|
||||
print('Da diese Runde bereits gespielt wurde, werden die alten '
|
||||
'Punktestaende ueberschrieben');
|
||||
}
|
||||
if (_kamikazePlayerIndex != null) {
|
||||
print('${widget.gameSession.players[_kamikazePlayerIndex!]} hat Kamikaze '
|
||||
'und bekommt 0 Punkte');
|
||||
print('Alle anderen Spieler bekommen 50 Punkte');
|
||||
widget.gameSession
|
||||
.applyKamikaze(widget.roundNumber, _kamikazePlayerIndex!);
|
||||
} else {
|
||||
List<int> roundScores = [];
|
||||
for (TextEditingController c in _scoreControllerList) {
|
||||
if (c.text.isNotEmpty) roundScores.add(int.parse(c.text));
|
||||
}
|
||||
widget.gameSession.calculateScoredPoints(
|
||||
widget.roundNumber, roundScores, _caboPlayerIndex);
|
||||
}
|
||||
List<int> bonusPlayers = widget.gameSession.updatePoints();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
print('Das Spiel ist beendet');
|
||||
} else if (widget.roundNumber == widget.gameSession.roundNumber) {
|
||||
widget.gameSession.increaseRound();
|
||||
}
|
||||
return bonusPlayers;
|
||||
}
|
||||
|
||||
/// Shows a popup dialog with the information which player received the bonus points.
|
||||
Future<void> _showBonusPopup(
|
||||
BuildContext context, List<int> bonusPlayers) async {
|
||||
int pointLimit = widget.gameSession.pointLimit;
|
||||
int bonusPoints = (pointLimit / 2).round();
|
||||
|
||||
String resultText =
|
||||
_getBonusPopupMessageString(pointLimit, bonusPoints, bonusPlayers);
|
||||
|
||||
await showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).bonus_points_title),
|
||||
content: Text(resultText),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Generates the message string for the bonus popup.
|
||||
/// It takes the [pointLimit], [bonusPoints] and the list of [bonusPlayers]
|
||||
/// and returns a formatted string.
|
||||
String _getBonusPopupMessageString(
|
||||
int pointLimit, int bonusPoints, List<int> bonusPlayers) {
|
||||
List<String> nameList =
|
||||
bonusPlayers.map((i) => widget.gameSession.players[i]).toList();
|
||||
String resultText = '';
|
||||
if (nameList.length == 1) {
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
nameList.length, nameList.first, pointLimit, bonusPoints);
|
||||
} else {
|
||||
resultText = nameList.length == 2
|
||||
? '${nameList[0]} & ${nameList[1]}'
|
||||
: '${nameList.sublist(0, nameList.length - 1).join(', ')} & ${nameList.last}';
|
||||
resultText = AppLocalizations.of(context).bonus_points_message(
|
||||
nameList.length,
|
||||
resultText,
|
||||
pointLimit,
|
||||
bonusPoints,
|
||||
);
|
||||
}
|
||||
return resultText;
|
||||
}
|
||||
|
||||
/// Handles the navigation for the end of the round.
|
||||
/// It checks for bonus players and shows a popup, saves the game session,
|
||||
/// and navigates to the next round or back to the previous screen.
|
||||
/// It takes the BuildContext [context] and a boolean [navigateToNextRound] to determine
|
||||
/// if it should navigate to the next round or not.
|
||||
Future<void> _endOfRoundNavigation(
|
||||
BuildContext context, bool navigateToNextRound) async {
|
||||
List<int> bonusPlayersIndices = _finishRound();
|
||||
if (bonusPlayersIndices.isNotEmpty) {
|
||||
await _showBonusPopup(context, bonusPlayersIndices);
|
||||
}
|
||||
|
||||
LocalStorageService.saveGameSessions();
|
||||
|
||||
if (context.mounted) {
|
||||
// If the game is finished, pop the context and return to the previous screen.
|
||||
if (widget.gameSession.isGameFinished) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
// If navigateToNextRound is false, pop the context and return to the previous screen.
|
||||
if (!navigateToNextRound) {
|
||||
Navigator.pop(context);
|
||||
return;
|
||||
}
|
||||
// If navigateToNextRound is true and the game isn't finished yet,
|
||||
// pop the context and navigate to the next round.
|
||||
Navigator.pop(context, widget.roundNumber + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _scoreControllerList) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final focusNode in _focusNodeList) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
304
lib/presentation/views/settings_view.dart
Normal file
304
lib/presentation/views/settings_view.dart
Normal file
@@ -0,0 +1,304 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/mode_selection_view.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_form_row.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_stepper.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
UniqueKey _stepperKey1 = UniqueKey();
|
||||
UniqueKey _stepperKey2 = UniqueKey();
|
||||
GameMode defaultMode = ConfigService.getGameMode();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).settings),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 10),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).cabo_penalty,
|
||||
prefixIcon: CupertinoIcons.bolt_fill,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey1,
|
||||
initialValue: ConfigService.getCaboPenalty(),
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).point_limit,
|
||||
prefixIcon: FontAwesomeIcons.bullseye,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey2,
|
||||
initialValue: ConfigService.getPointLimit(),
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).standard_mode,
|
||||
prefixIcon: CupertinoIcons.square_stack,
|
||||
suffixWidget: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
defaultMode == GameMode.none
|
||||
? AppLocalizations.of(context).no_default_mode
|
||||
: (defaultMode == GameMode.pointLimit
|
||||
? '${ConfigService.getPointLimit()} ${AppLocalizations.of(context).points}'
|
||||
: AppLocalizations.of(context).unlimited),
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
const CupertinoListTileChevron()
|
||||
],
|
||||
),
|
||||
onPressed: () async {
|
||||
final selectedMode = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => ModeSelectionMenu(
|
||||
pointLimit: ConfigService.getPointLimit(),
|
||||
showDeselection: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
defaultMode = selectedMode ?? GameMode.none;
|
||||
});
|
||||
ConfigService.setGameMode(defaultMode);
|
||||
},
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText:
|
||||
AppLocalizations.of(context).reset_to_default,
|
||||
prefixIcon: CupertinoIcons.arrow_counterclockwise,
|
||||
onPressed: () {
|
||||
ConfigService.resetConfig();
|
||||
setState(() {
|
||||
_stepperKey1 = UniqueKey();
|
||||
_stepperKey2 = UniqueKey();
|
||||
defaultMode = ConfigService.getGameMode();
|
||||
});
|
||||
},
|
||||
)
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game_data,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 10),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).import_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_down,
|
||||
onPressed: () async {
|
||||
final status =
|
||||
await LocalStorageService.importJsonFile();
|
||||
showFeedbackDialog(status);
|
||||
},
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).export_data,
|
||||
prefixIcon: CupertinoIcons.square_arrow_up,
|
||||
onPressed: () => LocalStorageService.exportGameData(),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).delete_data,
|
||||
prefixIcon: CupertinoIcons.trash,
|
||||
onPressed: () => _deleteAllGames(),
|
||||
),
|
||||
])),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).app,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 0),
|
||||
child: CupertinoFormSection.insetGrouped(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
margin: EdgeInsets.zero,
|
||||
children: [
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).wiki,
|
||||
prefixIcon: CupertinoIcons.book,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubWikiLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).privacy_policy,
|
||||
prefixIcon: CupertinoIcons.doc_append,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kPrivacyPolicyLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).error_found,
|
||||
prefixIcon: FontAwesomeIcons.github,
|
||||
onPressed: () =>
|
||||
launchUrl(Uri.parse(Constants.kGithubIssuesLink)),
|
||||
suffixWidget: const CupertinoListTileChevron(),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).app_version,
|
||||
prefixIcon: CupertinoIcons.tag,
|
||||
onPressed: null,
|
||||
suffixWidget: Text(VersionService.getVersion(),
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
))),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).build,
|
||||
prefixIcon: CupertinoIcons.number,
|
||||
onPressed: null,
|
||||
suffixWidget: Text(VersionService.getBuildNumber(),
|
||||
style: TextStyle(
|
||||
color: CustomTheme.primaryColor,
|
||||
))),
|
||||
])),
|
||||
const SizedBox(height: 50)
|
||||
],
|
||||
),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Shows a dialog to confirm the deletion of all game data.
|
||||
/// When confirmed, it deletes all game data from local storage.
|
||||
void _deleteAllGames() {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context).delete_data_title),
|
||||
content: Text(AppLocalizations.of(context).delete_data_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
CupertinoDialogAction(
|
||||
isDestructiveAction: true,
|
||||
isDefaultAction: true,
|
||||
child: Text(AppLocalizations.of(context).delete),
|
||||
onPressed: () {
|
||||
LocalStorageService.deleteAllGames();
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void showFeedbackDialog(ImportStatus status) {
|
||||
if (status == ImportStatus.canceled) return;
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
(String, String) _getDialogContent(ImportStatus status) {
|
||||
switch (status) {
|
||||
case ImportStatus.success:
|
||||
return (
|
||||
AppLocalizations.of(context).import_success_title,
|
||||
AppLocalizations.of(context).import_success_message
|
||||
);
|
||||
case ImportStatus.validationError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_validation_error_title,
|
||||
AppLocalizations.of(context).import_validation_error_message
|
||||
);
|
||||
|
||||
case ImportStatus.formatError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_format_error_title,
|
||||
AppLocalizations.of(context).import_format_error_message
|
||||
);
|
||||
case ImportStatus.genericError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_generic_error_title,
|
||||
AppLocalizations.of(context).import_generic_error_message
|
||||
);
|
||||
case ImportStatus.canceled:
|
||||
return ('', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/views/information_view.dart';
|
||||
import 'package:cabo_counter/views/main_menu_view.dart';
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/l10n/generated/app_localizations.dart';
|
||||
import 'package:cabo_counter/presentation/views/about_view.dart';
|
||||
import 'package:cabo_counter/presentation/views/main_menu_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class TabView extends StatefulWidget {
|
||||
@@ -17,7 +17,7 @@ class _TabViewState extends State<TabView> {
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoTabScaffold(
|
||||
tabBar: CupertinoTabBar(
|
||||
backgroundColor: CustomTheme.backgroundTintColor,
|
||||
backgroundColor: CustomTheme.mainElementBackgroundColor,
|
||||
iconSize: 27,
|
||||
height: 55,
|
||||
items: <BottomNavigationBarItem>[
|
||||
@@ -39,7 +39,7 @@ class _TabViewState extends State<TabView> {
|
||||
if (index == 0) {
|
||||
return const MainMenuView();
|
||||
} else {
|
||||
return const InformationView();
|
||||
return const AboutView();
|
||||
}
|
||||
});
|
||||
},
|
||||
53
lib/presentation/widgets/custom_form_row.dart
Normal file
53
lib/presentation/widgets/custom_form_row.dart
Normal file
@@ -0,0 +1,53 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/presentation/widgets/custom_stepper.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class CustomFormRow extends StatefulWidget {
|
||||
final String prefixText;
|
||||
final IconData prefixIcon;
|
||||
final Widget? suffixWidget;
|
||||
final void Function()? onPressed;
|
||||
const CustomFormRow({
|
||||
super.key,
|
||||
required this.prefixText,
|
||||
required this.prefixIcon,
|
||||
this.onPressed,
|
||||
this.suffixWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomFormRow> createState() => _CustomFormRowState();
|
||||
}
|
||||
|
||||
class _CustomFormRowState extends State<CustomFormRow> {
|
||||
late Widget suffixWidget;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
suffixWidget = widget.suffixWidget ?? const SizedBox.shrink();
|
||||
return CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: widget.onPressed,
|
||||
child: CupertinoFormRow(
|
||||
prefix: Row(
|
||||
children: [
|
||||
Icon(
|
||||
widget.prefixIcon,
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(widget.prefixText),
|
||||
],
|
||||
),
|
||||
padding: suffixWidget is CustomStepper
|
||||
? const EdgeInsets.fromLTRB(15, 0, 0, 0)
|
||||
: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||
child: suffixWidget,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart'; // Für iOS-Style
|
||||
|
||||
class Stepper extends StatefulWidget {
|
||||
class CustomStepper extends StatefulWidget {
|
||||
final int minValue;
|
||||
final int maxValue;
|
||||
final int? initialValue;
|
||||
final int step;
|
||||
final ValueChanged<int> onChanged;
|
||||
const Stepper({
|
||||
const CustomStepper({
|
||||
super.key,
|
||||
required this.minValue,
|
||||
required this.maxValue,
|
||||
@@ -17,10 +18,10 @@ class Stepper extends StatefulWidget {
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_StepperState createState() => _StepperState();
|
||||
_CustomStepperState createState() => _CustomStepperState();
|
||||
}
|
||||
|
||||
class _StepperState extends State<Stepper> {
|
||||
class _CustomStepperState extends State<CustomStepper> {
|
||||
late int _value;
|
||||
|
||||
@override
|
||||
@@ -34,18 +35,20 @@ class _StepperState extends State<Stepper> {
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _decrement,
|
||||
child: const Icon(CupertinoIcons.minus),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Text('$_value', style: const TextStyle(fontSize: 18)),
|
||||
child: Text('$_value',
|
||||
style: TextStyle(fontSize: 18, color: CustomTheme.white)),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _increment,
|
||||
child: const Icon(CupertinoIcons.add),
|
||||
),
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/presentation/views/mode_selection_view.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// This class handles the configuration settings for the app.
|
||||
@@ -7,49 +7,101 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
class ConfigService {
|
||||
static const String _keyPointLimit = 'pointLimit';
|
||||
static const String _keyCaboPenalty = 'caboPenalty';
|
||||
static const int _defaultPointLimit = 100; // Default Value
|
||||
static const int _defaultCaboPenalty = 5; // Default Value
|
||||
static const String _keyGameMode = 'gameMode';
|
||||
// Actual values used in the app
|
||||
static int _pointLimit = 100;
|
||||
static int _caboPenalty = 5;
|
||||
static int _gameMode = -1;
|
||||
// Default values
|
||||
static const int _defaultPointLimit = 100;
|
||||
static const int _defaultCaboPenalty = 5;
|
||||
static const int _defaultGameMode = -1;
|
||||
|
||||
static Future<void> initConfig() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Default values only set if they are not already set
|
||||
prefs.setInt(
|
||||
_keyPointLimit, prefs.getInt(_keyPointLimit) ?? _defaultPointLimit);
|
||||
prefs.setInt(
|
||||
_keyCaboPenalty, prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty);
|
||||
// Initialize pointLimit, caboPenalty, and gameMode from SharedPreferences
|
||||
// If they are not set, use the default values
|
||||
_pointLimit = prefs.getInt(_keyPointLimit) ?? _defaultPointLimit;
|
||||
_caboPenalty = prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty;
|
||||
_gameMode = prefs.getInt(_keyGameMode) ?? _defaultGameMode;
|
||||
|
||||
// Save the initial values to SharedPreferences
|
||||
prefs.setInt(_keyPointLimit, _pointLimit);
|
||||
prefs.setInt(_keyCaboPenalty, _caboPenalty);
|
||||
prefs.setInt(_keyGameMode, _gameMode);
|
||||
}
|
||||
|
||||
/// Getter for the point limit.
|
||||
static Future<int> getPointLimit() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_keyPointLimit) ?? _defaultPointLimit;
|
||||
/// Retrieves the current game mode.
|
||||
///
|
||||
/// The game mode is determined based on the stored integer value:
|
||||
/// - `0`: [GameMode.pointLimit]
|
||||
/// - `1`: [GameMode.unlimited]
|
||||
/// - Any other value: [GameMode.none] (-1 is used as a default for no mode)
|
||||
///
|
||||
/// Returns the corresponding [GameMode] enum value.
|
||||
static GameMode getGameMode() {
|
||||
switch (_gameMode) {
|
||||
case 0:
|
||||
return GameMode.pointLimit;
|
||||
case 1:
|
||||
return GameMode.unlimited;
|
||||
default:
|
||||
return GameMode.none;
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the game mode for the application.
|
||||
///
|
||||
/// [newGameMode] is the new game mode to be set. It can be one of the following:
|
||||
/// - `GameMode.pointLimit`: The game ends when a pleayer reaches the point limit.
|
||||
/// - `GameMode.unlimited`: Every game goes for infinity until you end it.
|
||||
/// - `GameMode.none`: No default mode set.
|
||||
///
|
||||
/// This method updates the `_gameMode` field and persists the value in `SharedPreferences`.
|
||||
static Future<void> setGameMode(GameMode newGameMode) async {
|
||||
int gameMode;
|
||||
switch (newGameMode) {
|
||||
case GameMode.pointLimit:
|
||||
gameMode = 0;
|
||||
break;
|
||||
case GameMode.unlimited:
|
||||
gameMode = 1;
|
||||
break;
|
||||
default:
|
||||
gameMode = -1;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyGameMode, gameMode);
|
||||
_gameMode = gameMode;
|
||||
}
|
||||
|
||||
static int getPointLimit() => _pointLimit;
|
||||
|
||||
/// Setter for the point limit.
|
||||
/// [newPointLimit] is the new point limit to be set.
|
||||
static Future<void> setPointLimit(int newPointLimit) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyPointLimit, newPointLimit);
|
||||
_pointLimit = newPointLimit;
|
||||
}
|
||||
|
||||
/// Getter for the cabo penalty.
|
||||
static Future<int> getCaboPenalty() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getInt(_keyCaboPenalty) ?? _defaultCaboPenalty;
|
||||
}
|
||||
static int getCaboPenalty() => _caboPenalty;
|
||||
|
||||
/// Setter for the cabo penalty.
|
||||
/// [newCaboPenalty] is the new cabo penalty to be set.
|
||||
static Future<void> setCaboPenalty(int newCaboPenalty) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyCaboPenalty, newCaboPenalty);
|
||||
_caboPenalty = newCaboPenalty;
|
||||
}
|
||||
|
||||
/// Resets the configuration to default values.
|
||||
static Future<void> resetConfig() async {
|
||||
Globals.pointLimit = _defaultPointLimit;
|
||||
Globals.caboPenalty = _defaultCaboPenalty;
|
||||
ConfigService._pointLimit = _defaultPointLimit;
|
||||
ConfigService._caboPenalty = _defaultCaboPenalty;
|
||||
ConfigService._gameMode = _defaultGameMode;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_keyPointLimit, _defaultPointLimit);
|
||||
await prefs.setInt(_keyCaboPenalty, _defaultCaboPenalty);
|
||||
|
||||
@@ -20,8 +20,8 @@ enum ImportStatus {
|
||||
class LocalStorageService {
|
||||
static const String _fileName = 'game_data.json';
|
||||
|
||||
/// Writes the game session list to a JSON file and returns it as string.
|
||||
static String getJsonFile() {
|
||||
/// Writes the game session list to a JSON file and returns it as string.
|
||||
static String _getGameDataAsJsonFile() {
|
||||
final jsonFile =
|
||||
gameManager.gameList.map((session) => session.toJson()).toList();
|
||||
return json.encode(jsonFile);
|
||||
@@ -39,7 +39,7 @@ class LocalStorageService {
|
||||
print('[local_storage_service.dart] Versuche, Daten zu speichern...');
|
||||
try {
|
||||
final file = await _getFilePath();
|
||||
final jsonFile = getJsonFile();
|
||||
final jsonFile = _getGameDataAsJsonFile();
|
||||
await file.writeAsString(jsonFile);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden zwischengespeichert.');
|
||||
@@ -70,7 +70,7 @@ class LocalStorageService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!await validateJsonSchema(jsonString)) {
|
||||
if (!await _validateJsonSchema(jsonString, true)) {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei konnte nicht validiert werden');
|
||||
gameManager.gameList = [];
|
||||
@@ -102,19 +102,27 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the file picker to save a JSON file with the current game data.
|
||||
static Future<bool> exportJsonFile() async {
|
||||
final jsonString = getJsonFile();
|
||||
/// Opens the file picker to export game data as a JSON file.
|
||||
/// This method will export the given [jsonString] as a JSON file. It opens
|
||||
/// the file picker with the choosen [fileName].
|
||||
static Future<bool> _exportJsonData(
|
||||
String jsonString,
|
||||
String fileName,
|
||||
) async {
|
||||
try {
|
||||
final bytes = Uint8List.fromList(utf8.encode(jsonString));
|
||||
final result = await FileSaver.instance.saveAs(
|
||||
name: 'cabo_counter_data',
|
||||
final path = await FileSaver.instance.saveAs(
|
||||
name: fileName,
|
||||
bytes: bytes,
|
||||
ext: 'json',
|
||||
mimeType: MimeType.json,
|
||||
);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden exportiert. Dateipfad: $result');
|
||||
if (path == null) {
|
||||
print('[local_storage_service.dart]: Export abgebrochen');
|
||||
} else {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Spieldaten wurden exportiert. Dateipfad: $path');
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
print(
|
||||
@@ -123,33 +131,59 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the file picker to export all game sessions as a JSON file.
|
||||
static Future<bool> exportGameData() async {
|
||||
String jsonString = _getGameDataAsJsonFile();
|
||||
String fileName = 'cabo_counter-game_data';
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to save a single game session as a JSON file.
|
||||
static Future<bool> exportSingleGameSession(GameSession session) async {
|
||||
String jsonString = json.encode(session.toJson());
|
||||
String fileName = 'cabo_counter-game_${session.id.substring(0, 7)}';
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to import a JSON file and loads the game data from it.
|
||||
static Future<ImportStatus> importJsonFile() async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
final path = await FilePicker.platform.pickFiles(
|
||||
dialogTitle: 'Wähle eine Datei mit Spieldaten aus',
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['json'],
|
||||
);
|
||||
|
||||
if (result == null) {
|
||||
if (path == null) {
|
||||
print(
|
||||
'[local_storage_service.dart] Der Filepicker-Dialog wurde abgebrochen');
|
||||
return ImportStatus.canceled;
|
||||
}
|
||||
|
||||
try {
|
||||
final jsonString = await _readFileContent(result.files.single);
|
||||
final jsonString = await _readFileContent(path.files.single);
|
||||
|
||||
if (!await validateJsonSchema(jsonString)) {
|
||||
if (await _validateJsonSchema(jsonString, true)) {
|
||||
// Checks if the JSON String is in the gameList format
|
||||
|
||||
final jsonData = json.decode(jsonString) as List<dynamic>;
|
||||
List<GameSession> importedList = jsonData
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
for (GameSession s in importedList) {
|
||||
_importSession(s);
|
||||
}
|
||||
} else if (await _validateJsonSchema(jsonString, false)) {
|
||||
// Checks if the JSON String is in the single game format
|
||||
final jsonData = json.decode(jsonString) as Map<String, dynamic>;
|
||||
_importSession(GameSession.fromJson(jsonData));
|
||||
} else {
|
||||
return ImportStatus.validationError;
|
||||
}
|
||||
final jsonData = json.decode(jsonString) as List<dynamic>;
|
||||
gameManager.gameList = jsonData
|
||||
.map((jsonItem) =>
|
||||
GameSession.fromJson(jsonItem as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei wurde erfolgreich Importiertn');
|
||||
'[local_storage_service.dart] Die Datei wurde erfolgreich Importiert');
|
||||
await saveGameSessions();
|
||||
return ImportStatus.success;
|
||||
} on FormatException catch (e) {
|
||||
@@ -163,6 +197,18 @@ class LocalStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Imports a single game session into the gameList.
|
||||
static Future<void> _importSession(GameSession session) async {
|
||||
if (gameManager.gameExistsInGameList(session.id)) {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Session mit der ID ${session.id} existiert bereits. Sie wird überschrieben.');
|
||||
gameManager.removeGameSessionById(session.id);
|
||||
}
|
||||
gameManager.addGameSession(session);
|
||||
print(
|
||||
'[local_storage_service.dart] Die Session mit der ID ${session.id} wurde erfolgreich importiert.');
|
||||
}
|
||||
|
||||
/// Helper method to read file content from either bytes or path
|
||||
static Future<String> _readFileContent(PlatformFile file) async {
|
||||
if (file.bytes != null) return utf8.decode(file.bytes!);
|
||||
@@ -172,15 +218,28 @@ class LocalStorageService {
|
||||
}
|
||||
|
||||
/// Validates the JSON data against the schema.
|
||||
static Future<bool> validateJsonSchema(String jsonString) async {
|
||||
/// This method checks if the provided [jsonString] is valid against the
|
||||
/// JSON schema. It takes a boolean [isGameList] to determine
|
||||
/// which schema to use (game list or single game).
|
||||
static Future<bool> _validateJsonSchema(
|
||||
String jsonString, bool isGameList) async {
|
||||
final String schemaString;
|
||||
|
||||
if (isGameList) {
|
||||
schemaString =
|
||||
await rootBundle.loadString('assets/game_list-schema.json');
|
||||
} else {
|
||||
schemaString = await rootBundle.loadString('assets/game-schema.json');
|
||||
}
|
||||
|
||||
try {
|
||||
final schemaString = await rootBundle.loadString('assets/schema.json');
|
||||
final schema = JsonSchema.create(json.decode(schemaString));
|
||||
final jsonData = json.decode(jsonString);
|
||||
final result = schema.validate(jsonData);
|
||||
|
||||
if (result.isValid) {
|
||||
print('[local_storage_service.dart] JSON ist erfolgreich validiert.');
|
||||
print(
|
||||
'[local_storage_service.dart] JSON ist erfolgreich validiert. Typ: ${isGameList ? 'Game List' : 'Single Game'}');
|
||||
return true;
|
||||
}
|
||||
print(
|
||||
|
||||
32
lib/services/version_service.dart
Normal file
32
lib/services/version_service.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:cabo_counter/core/constants.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
|
||||
class VersionService {
|
||||
static String _version = '-.-.-';
|
||||
static String _buildNumber = '-';
|
||||
|
||||
static Future<void> init() async {
|
||||
var packageInfo = await PackageInfo.fromPlatform();
|
||||
_version = packageInfo.version;
|
||||
_buildNumber = packageInfo.buildNumber;
|
||||
}
|
||||
|
||||
static String getVersionNumber() {
|
||||
return _version;
|
||||
}
|
||||
|
||||
static String getVersion() {
|
||||
if (_version == '-.-.-') {
|
||||
return getVersionNumber();
|
||||
}
|
||||
return '${Constants.appDevPhase} $_version';
|
||||
}
|
||||
|
||||
static String getBuildNumber() {
|
||||
return _buildNumber;
|
||||
}
|
||||
|
||||
static String getVersionWithBuild() {
|
||||
return '$_version ($_buildNumber)';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
class Globals {
|
||||
static int pointLimit = 100;
|
||||
static int caboPenalty = 5;
|
||||
static String appDevPhase = 'Beta';
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:syncfusion_flutter_charts/charts.dart';
|
||||
|
||||
class GraphView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
|
||||
const GraphView({super.key, required this.gameSession});
|
||||
|
||||
@override
|
||||
State<GraphView> createState() => _GraphViewState();
|
||||
}
|
||||
|
||||
class _GraphViewState extends State<GraphView> {
|
||||
/// List of colors for the graph lines.
|
||||
List<Color> lineColors = [
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
Colors.orange.shade400,
|
||||
Colors.purple,
|
||||
Colors.green,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).game_process),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: SfCartesianChart(
|
||||
legend:
|
||||
const Legend(isVisible: true, position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(),
|
||||
primaryYAxis: const NumericAxis(),
|
||||
series: getCumulativeScores(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns a list of LineSeries representing the cumulative scores of each player.
|
||||
/// Each series contains data points for each round, showing the cumulative score up to that round.
|
||||
/// The x-axis represents the round number, and the y-axis represents the cumulative score.
|
||||
List<LineSeries<(int, int), int>> getCumulativeScores() {
|
||||
final rounds = widget.gameSession.roundList;
|
||||
final playerCount = widget.gameSession.players.length;
|
||||
final playerNames = widget.gameSession.players;
|
||||
|
||||
List<List<int>> cumulativeScores = List.generate(playerCount, (_) => []);
|
||||
List<int> runningTotals = List.filled(playerCount, 0);
|
||||
|
||||
for (var round in rounds) {
|
||||
for (int i = 0; i < playerCount; i++) {
|
||||
runningTotals[i] += round.scores[i];
|
||||
cumulativeScores[i].add(runningTotals[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a list of LineSeries for each player
|
||||
/// Each series contains data points for each round
|
||||
return List.generate(playerCount, (i) {
|
||||
final data = List.generate(
|
||||
cumulativeScores[i].length,
|
||||
(j) => (j + 1, cumulativeScores[i][j]), // (round, score)
|
||||
);
|
||||
|
||||
/// Create a LineSeries for the player
|
||||
/// The xValueMapper maps the round number, and the yValueMapper maps the cumulative score.
|
||||
return LineSeries<(int, int), int>(
|
||||
name: playerNames[i],
|
||||
dataSource: data,
|
||||
xValueMapper: (record, _) => record.$1, // Runde
|
||||
yValueMapper: (record, _) => record.$2, // Punktestand
|
||||
markerSettings: const MarkerSettings(isVisible: true),
|
||||
color: lineColors[i],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,434 +0,0 @@
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
|
||||
class RoundView extends StatefulWidget {
|
||||
final GameSession gameSession;
|
||||
final int roundNumber;
|
||||
const RoundView(
|
||||
{super.key, required this.roundNumber, required this.gameSession});
|
||||
|
||||
@override
|
||||
// ignore: library_private_types_in_public_api
|
||||
_RoundViewState createState() => _RoundViewState();
|
||||
}
|
||||
|
||||
class _RoundViewState extends State<RoundView> {
|
||||
/// The current game session.
|
||||
late GameSession gameSession = widget.gameSession;
|
||||
|
||||
/// Index of the player who said CABO.
|
||||
int _caboPlayerIndex = 0;
|
||||
|
||||
/// Index of the player who has Kamikaze.
|
||||
/// Default is null (no Kamikaze player).
|
||||
int? _kamikazePlayerIndex;
|
||||
|
||||
/// List of text controllers for the score text fields.
|
||||
late final List<TextEditingController> _scoreControllerList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => TextEditingController(),
|
||||
);
|
||||
|
||||
/// List of focus nodes for the score text fields.
|
||||
late final List<FocusNode> _focusNodeList = List.generate(
|
||||
widget.gameSession.players.length,
|
||||
(index) => FocusNode(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
print('=== Runde ${widget.roundNumber} geöffnet ===');
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber ||
|
||||
widget.gameSession.isGameFinished == true) {
|
||||
print(
|
||||
'Diese wurde bereits gespielt, deshalb werden die alten Punktestaende angezeigt');
|
||||
|
||||
// If the current round has already been played, the text fields
|
||||
// are filled with the scores from this round
|
||||
for (int i = 0; i < _scoreControllerList.length; i++) {
|
||||
_scoreControllerList[i].text =
|
||||
gameSession.roundList[widget.roundNumber - 1].scores[i].toString();
|
||||
}
|
||||
_caboPlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].caboPlayerIndex;
|
||||
_kamikazePlayerIndex =
|
||||
gameSession.roundList[widget.roundNumber - 1].kamikazePlayerIndex;
|
||||
}
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bottomInset = MediaQuery.of(context).viewInsets.bottom;
|
||||
final maxLength = widget.gameSession.getMaxLengthOfPlayerNames();
|
||||
|
||||
return CupertinoPageScaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
transitionBetweenRoutes: true,
|
||||
middle: Text(AppLocalizations.of(context).results),
|
||||
leading: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => {
|
||||
LocalStorageService.saveGameSessions(),
|
||||
Navigator.pop(context, widget.gameSession)
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(bottom: 100 + bottomInset),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Text(
|
||||
'${AppLocalizations.of(context).round} ${widget.roundNumber}',
|
||||
style: CustomTheme.roundTitle),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
AppLocalizations.of(context).who_said_cabo,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal:
|
||||
widget.gameSession.players.length > 3 ? 5 : 20,
|
||||
vertical: 10,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: CupertinoSegmentedControl<int>(
|
||||
unselectedColor: CustomTheme.backgroundTintColor,
|
||||
selectedColor: CustomTheme.primaryColor,
|
||||
groupValue: _caboPlayerIndex,
|
||||
children: Map.fromEntries(widget.gameSession.players
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) {
|
||||
final index = entry.key;
|
||||
final name = entry.value;
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4 +
|
||||
_getSegmentedControlPadding(maxLength),
|
||||
vertical: 6,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: _getSegmentedControlFontSize(
|
||||
maxLength),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
})),
|
||||
onValueChanged: (value) {
|
||||
setState(() {
|
||||
_caboPlayerIndex = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: CupertinoListTile(
|
||||
title: Text(AppLocalizations.of(context).player),
|
||||
trailing: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points))),
|
||||
const SizedBox(width: 20),
|
||||
SizedBox(
|
||||
width: 80,
|
||||
child: Center(
|
||||
child: Text(AppLocalizations.of(context)
|
||||
.kamikaze))),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: widget.gameSession.players.length,
|
||||
itemBuilder: (context, index) {
|
||||
final name = widget.gameSession.players[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10, horizontal: 20),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: CupertinoListTile(
|
||||
backgroundColor: CupertinoColors.secondaryLabel,
|
||||
title: Row(children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
))
|
||||
]),
|
||||
subtitle: Text(
|
||||
'${widget.gameSession.playerScores[index]}'
|
||||
' ${AppLocalizations.of(context).points}'),
|
||||
trailing: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: CupertinoTextField(
|
||||
maxLength: 3,
|
||||
focusNode: _focusNodeList[index],
|
||||
keyboardType:
|
||||
const TextInputType.numberWithOptions(
|
||||
signed: true,
|
||||
decimal: false,
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
textInputAction: index ==
|
||||
widget.gameSession.players
|
||||
.length -
|
||||
1
|
||||
? TextInputAction.done
|
||||
: TextInputAction.next,
|
||||
controller: _scoreControllerList[index],
|
||||
placeholder:
|
||||
AppLocalizations.of(context).points,
|
||||
textAlign: TextAlign.center,
|
||||
onSubmitted: (_) =>
|
||||
_focusNextTextfield(index),
|
||||
onChanged: (_) => setState(() {}),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 50),
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_kamikazePlayerIndex =
|
||||
(_kamikazePlayerIndex == index)
|
||||
? null
|
||||
: index;
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
width: 24,
|
||||
height: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: _kamikazePlayerIndex == index
|
||||
? CupertinoColors.systemRed
|
||||
: CupertinoColors
|
||||
.tertiarySystemFill,
|
||||
border: Border.all(
|
||||
color: _kamikazePlayerIndex == index
|
||||
? CupertinoColors.systemRed
|
||||
: CupertinoColors.systemGrey,
|
||||
),
|
||||
),
|
||||
child: _kamikazePlayerIndex == index
|
||||
? const Icon(
|
||||
CupertinoIcons.exclamationmark,
|
||||
size: 16,
|
||||
color: CupertinoColors.white,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 22),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: bottomInset,
|
||||
child: KeyboardVisibilityBuilder(builder: (context, visible) {
|
||||
if (!visible) {
|
||||
return Container(
|
||||
height: 80,
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
color: CustomTheme.backgroundTintColor,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_finishRound();
|
||||
LocalStorageService.saveGameSessions();
|
||||
Navigator.pop(context, widget.gameSession);
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).done),
|
||||
),
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_finishRound();
|
||||
LocalStorageService.saveGameSessions();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
Navigator.pop(context, widget.gameSession);
|
||||
} else {
|
||||
Navigator.of(context, rootNavigator: true)
|
||||
.pushReplacement(
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => RoundView(
|
||||
gameSession: widget.gameSession,
|
||||
roundNumber: widget.roundNumber + 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Focuses the next text field in the list of text fields.
|
||||
/// [index] is the index of the current text field.
|
||||
void _focusNextTextfield(int index) {
|
||||
if (index < widget.gameSession.players.length - 1) {
|
||||
FocusScope.of(context).requestFocus(_focusNodeList[index + 1]);
|
||||
} else {
|
||||
_focusNodeList[index].unfocus();
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the inputs for the round are valid.
|
||||
/// Returns true if the inputs are valid, false otherwise.
|
||||
/// Round Inputs are valid if every player has a score or
|
||||
/// kamikaze is selected for a player
|
||||
bool _areRoundInputsValid() {
|
||||
if (_areTextFieldsEmpty() && _kamikazePlayerIndex == null) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Checks if any of the text fields for the players points are empty.
|
||||
/// Returns true if any of the text fields is empty, false otherwise.
|
||||
bool _areTextFieldsEmpty() {
|
||||
for (TextEditingController t in _scoreControllerList) {
|
||||
if (t.text.isEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Finishes the current round.
|
||||
/// It first determines, ifCalls the [_calculateScoredPoints()] method to calculate the points for
|
||||
/// every player. If the round is the highest round played in this game,
|
||||
/// it expands the player score lists. At the end it updates the score
|
||||
/// array for the game.
|
||||
void _finishRound() {
|
||||
print('====================================');
|
||||
print('Runde ${widget.roundNumber} beendet');
|
||||
// The shown round is smaller than the newest round
|
||||
if (widget.roundNumber < widget.gameSession.roundNumber) {
|
||||
print('Da diese Runde bereits gespielt wurde, werden die alten '
|
||||
'Punktestaende ueberschrieben');
|
||||
}
|
||||
if (_kamikazePlayerIndex != null) {
|
||||
print('${widget.gameSession.players[_kamikazePlayerIndex!]} hat Kamikaze '
|
||||
'und bekommt 0 Punkte');
|
||||
print('Alle anderen Spieler bekommen 50 Punkte');
|
||||
widget.gameSession
|
||||
.applyKamikaze(widget.roundNumber, _kamikazePlayerIndex!);
|
||||
} else {
|
||||
List<int> roundScores = [];
|
||||
for (TextEditingController c in _scoreControllerList) {
|
||||
if (c.text.isNotEmpty) roundScores.add(int.parse(c.text));
|
||||
}
|
||||
widget.gameSession.calculateScoredPoints(
|
||||
widget.roundNumber, roundScores, _caboPlayerIndex);
|
||||
}
|
||||
widget.gameSession.updatePoints();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
print('Das Spiel ist beendet');
|
||||
} else if (widget.roundNumber == widget.gameSession.roundNumber) {
|
||||
widget.gameSession.increaseRound();
|
||||
}
|
||||
}
|
||||
|
||||
double _getSegmentedControlFontSize(int maxLength) {
|
||||
if (maxLength > 8) {
|
||||
// 9 - 12 characters
|
||||
return 9.0;
|
||||
} else if (maxLength > 4) {
|
||||
// 5 - 8 characters
|
||||
return 15.0;
|
||||
} else {
|
||||
// 0 - 4 characters
|
||||
return 18.0;
|
||||
}
|
||||
}
|
||||
|
||||
double _getSegmentedControlPadding(int maxLength) {
|
||||
if (maxLength > 8) {
|
||||
// 9 - 12 characters
|
||||
return 0.0;
|
||||
} else if (maxLength > 4) {
|
||||
// 5 - 8 characters
|
||||
return 5.0;
|
||||
} else {
|
||||
// 0 - 4 characters
|
||||
return 8.0;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _scoreControllerList) {
|
||||
controller.dispose();
|
||||
}
|
||||
for (final focusNode in _focusNodeList) {
|
||||
focusNode.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -1,267 +0,0 @@
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/services/config_service.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.dart';
|
||||
import 'package:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/utility/globals.dart';
|
||||
import 'package:cabo_counter/widgets/stepper.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
UniqueKey _stepperKey1 = UniqueKey();
|
||||
UniqueKey _stepperKey2 = UniqueKey();
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
navigationBar: CupertinoNavigationBar(
|
||||
middle: Text(AppLocalizations.of(context).settings),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Stack(
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).points,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoListTile(
|
||||
padding: EdgeInsets.zero,
|
||||
title: Text(AppLocalizations.of(context).cabo_penalty),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context).cabo_penalty_subtitle),
|
||||
trailing: Stepper(
|
||||
key: _stepperKey1,
|
||||
initialValue: Globals.caboPenalty,
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
Globals.caboPenalty = newCaboPenalty;
|
||||
});
|
||||
},
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 10, 0),
|
||||
child: CupertinoListTile(
|
||||
padding: EdgeInsets.zero,
|
||||
title: Text(AppLocalizations.of(context).point_limit),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context).point_limit_subtitle),
|
||||
trailing: Stepper(
|
||||
key: _stepperKey2,
|
||||
initialValue: Globals.pointLimit,
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
Globals.pointLimit = newPointLimit;
|
||||
});
|
||||
},
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 10, 0, 0),
|
||||
child: Center(
|
||||
heightFactor: 0.9,
|
||||
child: CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: () => setState(() {
|
||||
ConfigService.resetConfig();
|
||||
_stepperKey1 = UniqueKey();
|
||||
_stepperKey2 = UniqueKey();
|
||||
}),
|
||||
child:
|
||||
Text(AppLocalizations.of(context).reset_to_default),
|
||||
),
|
||||
)),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 10, 0, 0),
|
||||
child: Text(
|
||||
AppLocalizations.of(context).game_data,
|
||||
style: CustomTheme.rowTitle,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 30),
|
||||
child: Center(
|
||||
heightFactor: 1,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CupertinoButton(
|
||||
color: CustomTheme.primaryColor,
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).import_data,
|
||||
style:
|
||||
TextStyle(color: CustomTheme.backgroundColor),
|
||||
),
|
||||
onPressed: () async {
|
||||
final success =
|
||||
await LocalStorageService.importJsonFile();
|
||||
showFeedbackDialog(success);
|
||||
}),
|
||||
const SizedBox(
|
||||
width: 20,
|
||||
),
|
||||
CupertinoButton(
|
||||
color: CustomTheme.primaryColor,
|
||||
sizeStyle: CupertinoButtonSize.medium,
|
||||
child: Text(
|
||||
AppLocalizations.of(context).export_data,
|
||||
style:
|
||||
TextStyle(color: CustomTheme.backgroundColor),
|
||||
),
|
||||
onPressed: () async {
|
||||
final success =
|
||||
await LocalStorageService.exportJsonFile();
|
||||
if (!success && context.mounted) {
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) => CupertinoAlertDialog(
|
||||
title: Text(AppLocalizations.of(context)
|
||||
.export_error_title),
|
||||
content: Text(AppLocalizations.of(context)
|
||||
.export_error_message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child:
|
||||
Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
)),
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 30,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Column(
|
||||
children: [
|
||||
Center(
|
||||
child: Text(AppLocalizations.of(context).error_found),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 30),
|
||||
child: Center(
|
||||
child: CupertinoButton(
|
||||
onPressed: () => launchUrl(Uri.parse(
|
||||
'https://github.com/flixcoo/Cabo-Counter/issues')),
|
||||
child: Text(AppLocalizations.of(context).create_issue),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder<PackageInfo>(
|
||||
future: _getPackageInfo(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Text(
|
||||
'${Globals.appDevPhase} ${snapshot.data!.version} '
|
||||
'(${AppLocalizations.of(context).build} ${snapshot.data!.buildNumber})',
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
} else if (snapshot.hasError) {
|
||||
return Text(
|
||||
'${AppLocalizations.of(context).app_version} -.-.- (${AppLocalizations.of(context).build} -)',
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
}
|
||||
return Text(
|
||||
AppLocalizations.of(context).load_version,
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PackageInfo> _getPackageInfo() async {
|
||||
return await PackageInfo.fromPlatform();
|
||||
}
|
||||
|
||||
void showFeedbackDialog(ImportStatus status) {
|
||||
if (status == ImportStatus.canceled) return;
|
||||
final (title, message) = _getDialogContent(status);
|
||||
|
||||
showCupertinoDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return CupertinoAlertDialog(
|
||||
title: Text(title),
|
||||
content: Text(message),
|
||||
actions: [
|
||||
CupertinoDialogAction(
|
||||
child: Text(AppLocalizations.of(context).ok),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
(String, String) _getDialogContent(ImportStatus status) {
|
||||
switch (status) {
|
||||
case ImportStatus.success:
|
||||
return (
|
||||
AppLocalizations.of(context).import_success_title,
|
||||
AppLocalizations.of(context).import_success_message
|
||||
);
|
||||
case ImportStatus.validationError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_validation_error_title,
|
||||
AppLocalizations.of(context).import_validation_error_message
|
||||
);
|
||||
|
||||
case ImportStatus.formatError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_format_error_title,
|
||||
AppLocalizations.of(context).import_format_error_message
|
||||
);
|
||||
case ImportStatus.genericError:
|
||||
return (
|
||||
AppLocalizations.of(context).import_generic_error_title,
|
||||
AppLocalizations.of(context).import_generic_error_message
|
||||
);
|
||||
case ImportStatus.canceled:
|
||||
return ('', '');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: cabo_counter
|
||||
description: "Mobile app for the card game Cabo"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 0.3.6+328
|
||||
version: 0.5.0+554
|
||||
|
||||
environment:
|
||||
sdk: ^3.5.4
|
||||
@@ -27,6 +27,7 @@ dependencies:
|
||||
intl: any
|
||||
syncfusion_flutter_charts: ^30.1.37
|
||||
uuid: ^4.5.1
|
||||
rate_my_app: ^2.3.2
|
||||
reorderables: ^0.4.2
|
||||
|
||||
dev_dependencies:
|
||||
@@ -40,4 +41,5 @@ flutter:
|
||||
uses-material-design: false
|
||||
assets:
|
||||
- assets/cabo_counter-logo_rounded.png
|
||||
- assets/schema.json
|
||||
- assets/game_list-schema.json
|
||||
- assets/game-schema.json
|
||||
|
||||
@@ -114,15 +114,15 @@ void main() {
|
||||
expect(session.roundList[0].caboPlayerIndex, 0);
|
||||
});
|
||||
|
||||
test('updatePoints - game not finished', () async {
|
||||
test('updatePoints - game not finished', () {
|
||||
session.addRoundScoresToList(1, [10, 20, 30], [10, 20, 30], 0);
|
||||
await session.updatePoints();
|
||||
session.updatePoints();
|
||||
expect(session.isGameFinished, isFalse);
|
||||
});
|
||||
|
||||
test('updatePoints - game finished', () async {
|
||||
test('updatePoints - game finished', () {
|
||||
session.addRoundScoresToList(1, [101, 20, 30], [101, 20, 30], 0);
|
||||
await session.updatePoints();
|
||||
session.updatePoints();
|
||||
expect(session.isGameFinished, isTrue);
|
||||
});
|
||||
|
||||
@@ -154,9 +154,9 @@ void main() {
|
||||
expect(session.playerScores, equals([50, 0, 30]));
|
||||
});
|
||||
|
||||
test('_setWinner via updatePoints', () async {
|
||||
test('_setWinner via updatePoints', () {
|
||||
session.addRoundScoresToList(1, [101, 20, 30], [101, 0, 30], 1);
|
||||
await session.updatePoints();
|
||||
session.updatePoints();
|
||||
expect(session.winner, 'Bob'); // Bob has lowest score (20)
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user