Beta-Version 0.4.4 (#105)
* Update README.md * Tried new design for im- and export-button * Moved views to presentation folder * Moved widgets to presentation folder * Implemented CustomRowForm Widget * Used new custom form row * Removed double information * Refactored methods to private * Changed label * Modified paddings and text color * Changed string * Updated CustomFormRow padding and pressed handler * Implemented various new forms of CustomFormRow into SettingsView * Implemented VersionService * Updated strings, added wiki button * Corrected replaced string * Added import dialog feedback (got lost in refactoring) * Corrected function duplication * changed suffixWidget assignment and moved stepperKeys * Changed icons * Added rate_my_app package * Renamed folder * Implement native rating dialog * Implemented logic for pre rating and refactored rating dialog * updated launch mode * Small changes * Updated launch mode * Updated linting rules * Renamed folders * Changed l10n files location * Implemented new link constants * Changed privacy policy link * Corrected wiki link * Removed import * Updated links * Updated links to subdomains * Updated file paths * Updated strings * Updated identifiers * Added break in switch case * Updated strings * Implemented new popup * Corrected links * Changed color * Ensured rating dialog wont show in Beta * Refactoring * Adding const * Renamed variables * Corrected links * updated Dialog function * Added version number in about view * Changed order and corrected return * Changed translation * Changed popups because of unmounted context errors * corrected string typo * Replaced int constants with enums * Renamed Stepper to CustomStepper * Changed argument order * Reordered properties * Implemented empty builder for GraphView * Added jitterStip to prevent the graphs overlaying each other * Removed german comments * Added comment to jitter calculation * Overhauled comments in CustomTheme * Updated version * Added Delete all games button to Settings * Updated version * Updated en string * Updated RoundView buttons when game is finished * Changed lock emoji to CuperinoIcons.lock and placed it in trailing of app bar * Simplified comparison * Updated version * Corrected scaling * Updates constant names and lint rule * HOTFIX: Graph showed wrong data * Graph starts at round 0 now where all players have 0 points * Adjusted jitterStep * Removed dead code * Updated Y-Axis and removed values under y = 0 * Changed overflow mode * Replaced string & if statement with visibility widget * updated accessability of graph view * Changed string for GraphView title * Updated comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Updated generated files * Updated version in README --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
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
|
||||
|
||||
@@ -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-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);
|
||||
}
|
||||
@@ -6,6 +6,13 @@ class CustomTheme {
|
||||
static Color backgroundColor = const Color(0xFF101010);
|
||||
static Color backgroundTintColor = CupertinoColors.darkBackgroundGray;
|
||||
|
||||
// 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;
|
||||
|
||||
static TextStyle modeTitle = TextStyle(
|
||||
color: primaryColor,
|
||||
fontSize: 20,
|
||||
@@ -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",
|
||||
@@ -64,7 +74,6 @@
|
||||
"done": "Fertig",
|
||||
"next_round": "Nächste Runde",
|
||||
|
||||
"statistics": "Statistiken",
|
||||
"end_game": "Spiel beenden",
|
||||
"delete_game": "Spiel löschen",
|
||||
"new_game_same_settings": "Neues Spiel mit gleichen Einstellungen",
|
||||
@@ -75,6 +84,7 @@
|
||||
"end_game_message": "Möchtest du das Spiel beenden? Das Spiel wird als beendet markiert und kann nicht fortgeführt werden.",
|
||||
|
||||
"game_process": "Spielverlauf",
|
||||
"empty_graph_text": "Du musst mindestens eine Runde spielen, damit der Graph des Spielverlaufes angezeigt werden kann.",
|
||||
|
||||
"settings": "Einstellungen",
|
||||
"cabo_penalty": "Cabo-Strafe",
|
||||
@@ -83,8 +93,12 @@
|
||||
"point_limit_subtitle": "... hier ist Schluss",
|
||||
"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.",
|
||||
@@ -92,16 +106,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! "
|
||||
}
|
||||
@@ -30,6 +30,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -64,13 +74,17 @@
|
||||
"done": "Done",
|
||||
"next_round": "Next Round",
|
||||
|
||||
"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",
|
||||
"game_process": "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",
|
||||
@@ -81,10 +95,10 @@
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
@@ -97,11 +111,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:
|
||||
@@ -362,12 +416,6 @@ abstract class AppLocalizations {
|
||||
/// **'Nächste Runde'**
|
||||
String get next_round;
|
||||
|
||||
/// No description provided for @statistics.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
/// **'Statistiken'**
|
||||
String get statistics;
|
||||
|
||||
/// No description provided for @end_game.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -422,6 +470,12 @@ abstract class AppLocalizations {
|
||||
/// **'Spielverlauf'**
|
||||
String get game_process;
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// In de, this message translates to:
|
||||
@@ -467,15 +521,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:
|
||||
@@ -548,23 +626,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';
|
||||
|
||||
@@ -149,9 +178,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get next_round => 'Nächste Runde';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistiken';
|
||||
|
||||
@override
|
||||
String get end_game => 'Spiel beenden';
|
||||
|
||||
@@ -181,6 +207,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get game_process => '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';
|
||||
|
||||
@@ -203,10 +233,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';
|
||||
@@ -247,14 +290,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 =>
|
||||
@@ -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';
|
||||
|
||||
@@ -146,9 +175,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get next_round => 'Next Round';
|
||||
|
||||
@override
|
||||
String get statistics => 'Statistics';
|
||||
|
||||
@override
|
||||
String get end_game => 'End Game';
|
||||
|
||||
@@ -176,7 +202,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'Do you want to end the game? The game gets marked as finished and cannot be continued.';
|
||||
|
||||
@override
|
||||
String get game_process => 'Spielverlauf';
|
||||
String get game_process => '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';
|
||||
@@ -205,6 +235,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';
|
||||
|
||||
@@ -244,14 +287,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,8 +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/views/tab_view.dart';
|
||||
import 'package:cabo_counter/services/version_service.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -13,6 +14,7 @@ Future<void> main() async {
|
||||
await ConfigService.initConfig();
|
||||
ConfigService.pointLimit = await ConfigService.getPointLimit();
|
||||
ConfigService.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,11 +1,11 @@
|
||||
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/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/round_view.dart';
|
||||
import 'package:cabo_counter/services/local_storage_service.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:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -121,7 +121,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
children: [
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).statistics,
|
||||
AppLocalizations.of(context).game_process,
|
||||
),
|
||||
backgroundColorActivated:
|
||||
CustomTheme.backgroundColor,
|
||||
@@ -131,8 +131,9 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
builder: (_) => GraphView(
|
||||
gameSession: gameSession,
|
||||
)))),
|
||||
if (!gameSession.isPointsLimitEnabled)
|
||||
CupertinoListTile(
|
||||
Visibility(
|
||||
visible: !gameSession.isPointsLimitEnabled,
|
||||
child: CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: gameSession.roundNumber > 1 &&
|
||||
@@ -148,6 +149,7 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
_showEndGameDialog();
|
||||
}
|
||||
}),
|
||||
),
|
||||
CupertinoListTile(
|
||||
title: Text(
|
||||
AppLocalizations.of(context).delete_game,
|
||||
@@ -235,7 +237,8 @@ class _ActiveGameViewState extends State<ActiveGameView> {
|
||||
child: Text(
|
||||
AppLocalizations.of(context).end_game,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.red),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: CupertinoColors.destructiveRed),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -1,10 +1,10 @@
|
||||
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/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:cabo_counter/utility/custom_theme.dart';
|
||||
import 'package:cabo_counter/views/active_game_view.dart';
|
||||
import 'package:cabo_counter/views/mode_selection_view.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
enum CreateStatus {
|
||||
120
lib/presentation/views/graph_view.dart
Normal file
120
lib/presentation/views/graph_view.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
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).game_process),
|
||||
previousPageTitle: AppLocalizations.of(context).back,
|
||||
),
|
||||
child: widget.gameSession.roundNumber > 1
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 100, 0, 0),
|
||||
child: SfCartesianChart(
|
||||
legend: const Legend(
|
||||
overflowMode: LegendItemOverflowMode.wrap,
|
||||
isVisible: true,
|
||||
position: LegendPosition.bottom),
|
||||
primaryXAxis: const NumericAxis(
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
),
|
||||
primaryYAxis: const NumericAxis(
|
||||
interval: 1,
|
||||
decimalPlaces: 0,
|
||||
),
|
||||
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/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() {
|
||||
@@ -57,14 +74,12 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
icon: const Icon(CupertinoIcons.settings, size: 30)),
|
||||
middle: const Text('Cabo Counter'),
|
||||
trailing: IconButton(
|
||||
onPressed: () => {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const CreateGameView(),
|
||||
),
|
||||
)
|
||||
},
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => const CreateGameView(),
|
||||
),
|
||||
),
|
||||
icon: const Icon(CupertinoIcons.add)),
|
||||
),
|
||||
child: CupertinoPageScaffold(
|
||||
@@ -73,10 +88,9 @@ 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(
|
||||
@@ -92,7 +106,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),
|
||||
@@ -128,7 +142,7 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
final String gameTitle = gameManager
|
||||
.gameList[index].gameTitle;
|
||||
return await _showDeleteGamePopup(
|
||||
gameTitle);
|
||||
context, gameTitle);
|
||||
},
|
||||
onDismissed: (direction) {
|
||||
gameManager
|
||||
@@ -204,40 +218,144 @@ class _MainMenuViewState extends State<MainMenuView> {
|
||||
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,5 +1,5 @@
|
||||
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';
|
||||
|
||||
class ModeSelectionMenu extends StatelessWidget {
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:cabo_counter/core/custom_theme.dart';
|
||||
import 'package:cabo_counter/data/game_session.dart';
|
||||
import 'package:cabo_counter/l10n/app_localizations.dart';
|
||||
import 'package:cabo_counter/l10n/generated/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';
|
||||
@@ -67,19 +67,24 @@ class _RoundViewState extends State<RoundView> {
|
||||
@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)},
|
||||
child: Text(AppLocalizations.of(context).cancel),
|
||||
),
|
||||
middle: Text(AppLocalizations.of(context).results),
|
||||
trailing: widget.gameSession.isGameFinished
|
||||
? const Icon(
|
||||
CupertinoIcons.lock,
|
||||
size: 25,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
@@ -120,9 +125,8 @@ class _RoundViewState extends State<RoundView> {
|
||||
return MapEntry(
|
||||
index,
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 4 +
|
||||
_getSegmentedControlPadding(maxLength),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 6,
|
||||
),
|
||||
child: FittedBox(
|
||||
@@ -131,10 +135,8 @@ class _RoundViewState extends State<RoundView> {
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: _getSegmentedControlFontSize(
|
||||
maxLength),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -293,21 +295,22 @@ class _RoundViewState extends State<RoundView> {
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).done),
|
||||
),
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_finishRound();
|
||||
LocalStorageService.saveGameSessions();
|
||||
if (widget.gameSession.isGameFinished == true) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
Navigator.pop(
|
||||
context, widget.roundNumber + 1);
|
||||
if (!widget.gameSession.isGameFinished)
|
||||
CupertinoButton(
|
||||
onPressed: _areRoundInputsValid()
|
||||
? () {
|
||||
_finishRound();
|
||||
LocalStorageService.saveGameSessions();
|
||||
if (widget.gameSession.isGameFinished) {
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
Navigator.pop(
|
||||
context, widget.roundNumber + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
: null,
|
||||
child: Text(AppLocalizations.of(context).next_round),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -386,32 +389,6 @@ class _RoundViewState extends State<RoundView> {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
269
lib/presentation/views/settings_view.dart
Normal file
269
lib/presentation/views/settings_view.dart
Normal file
@@ -0,0 +1,269 @@
|
||||
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/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();
|
||||
@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.caboPenalty,
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
ConfigService.caboPenalty = newCaboPenalty;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText: AppLocalizations.of(context).point_limit,
|
||||
prefixIcon: FontAwesomeIcons.bullseye,
|
||||
suffixWidget: CustomStepper(
|
||||
key: _stepperKey2,
|
||||
initialValue: ConfigService.pointLimit,
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
ConfigService.pointLimit = newPointLimit;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
CustomFormRow(
|
||||
prefixText:
|
||||
AppLocalizations.of(context).reset_to_default,
|
||||
prefixIcon: CupertinoIcons.arrow_counterclockwise,
|
||||
onPressed: () {
|
||||
ConfigService.resetConfig();
|
||||
setState(() {
|
||||
_stepperKey1 = UniqueKey();
|
||||
_stepperKey2 = UniqueKey();
|
||||
});
|
||||
},
|
||||
)
|
||||
])),
|
||||
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 {
|
||||
@@ -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),
|
||||
),
|
||||
@@ -21,7 +21,7 @@ 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 getGameDataAsJsonFile() {
|
||||
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 = getGameDataAsJsonFile();
|
||||
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, true)) {
|
||||
if (!await _validateJsonSchema(jsonString, true)) {
|
||||
print(
|
||||
'[local_storage_service.dart] Die Datei konnte nicht validiert werden');
|
||||
gameManager.gameList = [];
|
||||
@@ -105,7 +105,7 @@ class LocalStorageService {
|
||||
/// 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(
|
||||
static Future<bool> _exportJsonData(
|
||||
String jsonString,
|
||||
String fileName,
|
||||
) async {
|
||||
@@ -133,16 +133,16 @@ class LocalStorageService {
|
||||
|
||||
/// Opens the file picker to export all game sessions as a JSON file.
|
||||
static Future<bool> exportGameData() async {
|
||||
String jsonString = getGameDataAsJsonFile();
|
||||
String jsonString = _getGameDataAsJsonFile();
|
||||
String fileName = 'cabo_counter-game_data';
|
||||
return exportJsonData(jsonString, fileName);
|
||||
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);
|
||||
return _exportJsonData(jsonString, fileName);
|
||||
}
|
||||
|
||||
/// Opens the file picker to import a JSON file and loads the game data from it.
|
||||
@@ -162,7 +162,7 @@ class LocalStorageService {
|
||||
try {
|
||||
final jsonString = await _readFileContent(path.files.single);
|
||||
|
||||
if (await validateJsonSchema(jsonString, true)) {
|
||||
if (await _validateJsonSchema(jsonString, true)) {
|
||||
// Checks if the JSON String is in the gameList format
|
||||
|
||||
final jsonData = json.decode(jsonString) as List<dynamic>;
|
||||
@@ -172,12 +172,12 @@ class LocalStorageService {
|
||||
.toList();
|
||||
|
||||
for (GameSession s in importedList) {
|
||||
importSession(s);
|
||||
_importSession(s);
|
||||
}
|
||||
} else if (await validateJsonSchema(jsonString, false)) {
|
||||
} 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));
|
||||
_importSession(GameSession.fromJson(jsonData));
|
||||
} else {
|
||||
return ImportStatus.validationError;
|
||||
}
|
||||
@@ -198,7 +198,7 @@ class LocalStorageService {
|
||||
}
|
||||
|
||||
/// Imports a single game session into the gameList.
|
||||
static Future<void> importSession(GameSession session) async {
|
||||
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.');
|
||||
@@ -221,7 +221,7 @@ class LocalStorageService {
|
||||
/// 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(
|
||||
static Future<bool> _validateJsonSchema(
|
||||
String jsonString, bool isGameList) async {
|
||||
final String schemaString;
|
||||
|
||||
|
||||
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,3 +0,0 @@
|
||||
class Globals {
|
||||
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,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: ConfigService.caboPenalty,
|
||||
minValue: 0,
|
||||
maxValue: 50,
|
||||
step: 1,
|
||||
onChanged: (newCaboPenalty) {
|
||||
setState(() {
|
||||
ConfigService.setCaboPenalty(newCaboPenalty);
|
||||
ConfigService.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: ConfigService.pointLimit,
|
||||
minValue: 30,
|
||||
maxValue: 1000,
|
||||
step: 10,
|
||||
onChanged: (newPointLimit) {
|
||||
setState(() {
|
||||
ConfigService.setPointLimit(newPointLimit);
|
||||
ConfigService.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.exportGameData();
|
||||
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.9+331
|
||||
version: 0.4.4+485
|
||||
|
||||
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
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user