From 8565382fab793c55cac5981a34826601a4293bd0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 13 Jul 2025 12:48:24 +0200 Subject: [PATCH] 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> --- README.md | 2 +- analysis_options.yaml | 3 - l10n.yaml | 7 +- lib/core/constants.dart | 22 ++ lib/{utility => core}/custom_theme.dart | 7 + lib/l10n/{ => arb}/app_de.arb | 29 +- lib/l10n/{ => arb}/app_en.arb | 33 ++- lib/l10n/{ => arb}/untranslated_messages.json | 0 .../{ => generated}/app_localizations.dart | 116 +++++++- .../{ => generated}/app_localizations_de.dart | 63 +++- .../{ => generated}/app_localizations_en.dart | 61 +++- lib/main.dart | 8 +- .../views/about_view.dart} | 22 +- .../views/active_game_view.dart | 21 +- .../views/create_game_view.dart | 8 +- lib/presentation/views/graph_view.dart | 120 ++++++++ .../views/main_menu_view.dart | 202 ++++++++++--- .../views/mode_selection_view.dart | 4 +- lib/{ => presentation}/views/round_view.dart | 77 ++--- lib/presentation/views/settings_view.dart | 269 ++++++++++++++++++ lib/{ => presentation}/views/tab_view.dart | 10 +- lib/presentation/widgets/custom_form_row.dart | 53 ++++ .../widgets/custom_stepper.dart} | 17 +- lib/services/local_storage_service.dart | 26 +- lib/services/version_service.dart | 32 +++ lib/utility/globals.dart | 3 - lib/views/graph_view.dart | 84 ------ lib/views/settings_view.dart | 267 ----------------- pubspec.yaml | 3 +- 29 files changed, 1024 insertions(+), 545 deletions(-) create mode 100644 lib/core/constants.dart rename lib/{utility => core}/custom_theme.dart (71%) rename lib/l10n/{ => arb}/app_de.arb (80%) rename lib/l10n/{ => arb}/app_en.arb (80%) rename lib/l10n/{ => arb}/untranslated_messages.json (100%) rename lib/l10n/{ => generated}/app_localizations.dart (85%) rename lib/l10n/{ => generated}/app_localizations_de.dart (81%) rename lib/l10n/{ => generated}/app_localizations_en.dart (80%) rename lib/{views/information_view.dart => presentation/views/about_view.dart} (74%) rename lib/{ => presentation}/views/active_game_view.dart (95%) rename lib/{ => presentation}/views/create_game_view.dart (97%) create mode 100644 lib/presentation/views/graph_view.dart rename lib/{ => presentation}/views/main_menu_view.dart (57%) rename lib/{ => presentation}/views/mode_selection_view.dart (92%) rename lib/{ => presentation}/views/round_view.dart (89%) create mode 100644 lib/presentation/views/settings_view.dart rename lib/{ => presentation}/views/tab_view.dart (80%) create mode 100644 lib/presentation/widgets/custom_form_row.dart rename lib/{widgets/stepper.dart => presentation/widgets/custom_stepper.dart} (75%) create mode 100644 lib/services/version_service.dart delete mode 100644 lib/utility/globals.dart delete mode 100644 lib/views/graph_view.dart delete mode 100644 lib/views/settings_view.dart diff --git a/README.md b/README.md index 20b0cae..23ec7d3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # CABO Counter -![Version](https://img.shields.io/badge/Version-0.3.8-orange) +![Version](https://img.shields.io/badge/Version-0.4.4-orange) ![Flutter](https://img.shields.io/badge/Flutter-3.32.1-blue?logo=flutter) ![Dart](https://img.shields.io/badge/Dart-3.8.1-blue?logo=dart) ![iOS](https://img.shields.io/badge/iOS-18.5-white?logo=apple) diff --git a/analysis_options.yaml b/analysis_options.yaml index 2ce6b52..b3623a1 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -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 diff --git a/l10n.yaml b/l10n.yaml index 239fdc6..f69305d 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -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 \ No newline at end of file +output-localization-file: app_localizations.dart +output-dir: lib/l10n/generated \ No newline at end of file diff --git a/lib/core/constants.dart b/lib/core/constants.dart new file mode 100644 index 0000000..e716464 --- /dev/null +++ b/lib/core/constants.dart @@ -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); +} diff --git a/lib/utility/custom_theme.dart b/lib/core/custom_theme.dart similarity index 71% rename from lib/utility/custom_theme.dart rename to lib/core/custom_theme.dart index 77a2f5b..a00340b 100644 --- a/lib/utility/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -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, diff --git a/lib/l10n/app_de.arb b/lib/l10n/arb/app_de.arb similarity index 80% rename from lib/l10n/app_de.arb rename to lib/l10n/arb/app_de.arb index 9281ac1..b072d63 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -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! " } \ No newline at end of file diff --git a/lib/l10n/app_en.arb b/lib/l10n/arb/app_en.arb similarity index 80% rename from lib/l10n/app_en.arb rename to lib/l10n/arb/app_en.arb index 8b328ba..a649362 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -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!" } diff --git a/lib/l10n/untranslated_messages.json b/lib/l10n/arb/untranslated_messages.json similarity index 100% rename from lib/l10n/untranslated_messages.json rename to lib/l10n/arb/untranslated_messages.json diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/generated/app_localizations.dart similarity index 85% rename from lib/l10n/app_localizations.dart rename to lib/l10n/generated/app_localizations.dart index c836194..2059f1b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -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. /// diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart similarity index 81% rename from lib/l10n/app_localizations_de.dart rename to lib/l10n/generated/app_localizations_de.dart index 5b9d841..068711f 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -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 => diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart similarity index 80% rename from lib/l10n/app_localizations_en.dart rename to lib/l10n/generated/app_localizations_en.dart index c98dddd..06b5c03 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -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 => diff --git a/lib/main.dart b/lib/main.dart index cd3d05f..9279426 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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 main() async { await ConfigService.initConfig(); ConfigService.pointLimit = await ConfigService.getPointLimit(); ConfigService.caboPenalty = await ConfigService.getCaboPenalty(); + await VersionService.init(); runApp(const App()); } diff --git a/lib/views/information_view.dart b/lib/presentation/views/about_view.dart similarity index 74% rename from lib/views/information_view.dart rename to lib/presentation/views/about_view.dart index 1d0918b..481e294 100644 --- a/lib/views/information_view.dart +++ b/lib/presentation/views/about_view.dart @@ -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)), ], ), diff --git a/lib/views/active_game_view.dart b/lib/presentation/views/active_game_view.dart similarity index 95% rename from lib/views/active_game_view.dart rename to lib/presentation/views/active_game_view.dart index 5945a3e..704952a 100644 --- a/lib/views/active_game_view.dart +++ b/lib/presentation/views/active_game_view.dart @@ -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 { children: [ CupertinoListTile( title: Text( - AppLocalizations.of(context).statistics, + AppLocalizations.of(context).game_process, ), backgroundColorActivated: CustomTheme.backgroundColor, @@ -131,8 +131,9 @@ class _ActiveGameViewState extends State { 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 { _showEndGameDialog(); } }), + ), CupertinoListTile( title: Text( AppLocalizations.of(context).delete_game, @@ -235,7 +237,8 @@ class _ActiveGameViewState extends State { child: Text( AppLocalizations.of(context).end_game, style: const TextStyle( - fontWeight: FontWeight.bold, color: Colors.red), + fontWeight: FontWeight.bold, + color: CupertinoColors.destructiveRed), ), onPressed: () { setState(() { diff --git a/lib/views/create_game_view.dart b/lib/presentation/views/create_game_view.dart similarity index 97% rename from lib/views/create_game_view.dart rename to lib/presentation/views/create_game_view.dart index f6099f4..0d23494 100644 --- a/lib/views/create_game_view.dart +++ b/lib/presentation/views/create_game_view.dart @@ -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 { diff --git a/lib/presentation/views/graph_view.dart b/lib/presentation/views/graph_view.dart new file mode 100644 index 0000000..d322bd0 --- /dev/null +++ b/lib/presentation/views/graph_view.dart @@ -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 createState() => _GraphViewState(); +} + +class _GraphViewState extends State { + /// List of colors for the graph lines. + final List 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> getCumulativeScores() { + final rounds = widget.gameSession.roundList; + final playerCount = widget.gameSession.players.length; + final playerNames = widget.gameSession.players; + + List> cumulativeScores = List.generate(playerCount, (_) => []); + List 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], + ); + }); + } +} diff --git a/lib/views/main_menu_view.dart b/lib/presentation/views/main_menu_view.dart similarity index 57% rename from lib/views/main_menu_view.dart rename to lib/presentation/views/main_menu_view.dart index e087cfc..86ff208 100644 --- a/lib/views/main_menu_view.dart +++ b/lib/presentation/views/main_menu_view.dart @@ -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 { }); }); 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 { 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 { ? 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 { 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 { final String gameTitle = gameManager .gameList[index].gameTitle; return await _showDeleteGamePopup( - gameTitle); + context, gameTitle); }, onDismissed: (direction) { gameManager @@ -204,40 +218,144 @@ class _MainMenuViewState extends State { 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 _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 _showDeleteGamePopup(String gameTitle) async { - bool? shouldDelete = await showCupertinoDialog( + Future _showDeleteGamePopup( + BuildContext context, String gameTitle) async { + return await showCupertinoDialog( 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 _showPreRatingDialog( + BuildContext context) async { + return await showCupertinoDialog( + 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 _showBadRatingDialog( + BuildContext context) async { + return await showCupertinoDialog( + 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 diff --git a/lib/views/mode_selection_view.dart b/lib/presentation/views/mode_selection_view.dart similarity index 92% rename from lib/views/mode_selection_view.dart rename to lib/presentation/views/mode_selection_view.dart index 93cdc7a..a7d3ce7 100644 --- a/lib/views/mode_selection_view.dart +++ b/lib/presentation/views/mode_selection_view.dart @@ -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 { diff --git a/lib/views/round_view.dart b/lib/presentation/views/round_view.dart similarity index 89% rename from lib/views/round_view.dart rename to lib/presentation/views/round_view.dart index 4e114fe..39e5cc8 100644 --- a/lib/views/round_view.dart +++ b/lib/presentation/views/round_view.dart @@ -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 { @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 { 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 { 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 { : 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 { } } - 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) { diff --git a/lib/presentation/views/settings_view.dart b/lib/presentation/views/settings_view.dart new file mode 100644 index 0000000..d6f0833 --- /dev/null +++ b/lib/presentation/views/settings_view.dart @@ -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 createState() => _SettingsViewState(); +} + +class _SettingsViewState extends State { + 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 ('', ''); + } + } +} diff --git a/lib/views/tab_view.dart b/lib/presentation/views/tab_view.dart similarity index 80% rename from lib/views/tab_view.dart rename to lib/presentation/views/tab_view.dart index 4abd411..0c98cc7 100644 --- a/lib/views/tab_view.dart +++ b/lib/presentation/views/tab_view.dart @@ -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 { if (index == 0) { return const MainMenuView(); } else { - return const InformationView(); + return const AboutView(); } }); }, diff --git a/lib/presentation/widgets/custom_form_row.dart b/lib/presentation/widgets/custom_form_row.dart new file mode 100644 index 0000000..7a2526b --- /dev/null +++ b/lib/presentation/widgets/custom_form_row.dart @@ -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 createState() => _CustomFormRowState(); +} + +class _CustomFormRowState extends State { + 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, + ), + ); + } +} diff --git a/lib/widgets/stepper.dart b/lib/presentation/widgets/custom_stepper.dart similarity index 75% rename from lib/widgets/stepper.dart rename to lib/presentation/widgets/custom_stepper.dart index 879235e..a05a4cb 100644 --- a/lib/widgets/stepper.dart +++ b/lib/presentation/widgets/custom_stepper.dart @@ -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 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 { +class _CustomStepperState extends State { late int _value; @override @@ -34,18 +35,20 @@ class _StepperState extends State { 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), ), diff --git a/lib/services/local_storage_service.dart b/lib/services/local_storage_service.dart index 12130a1..29ac58b 100644 --- a/lib/services/local_storage_service.dart +++ b/lib/services/local_storage_service.dart @@ -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 exportJsonData( + static Future _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 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 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; @@ -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; - 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 importSession(GameSession session) async { + static Future _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 validateJsonSchema( + static Future _validateJsonSchema( String jsonString, bool isGameList) async { final String schemaString; diff --git a/lib/services/version_service.dart b/lib/services/version_service.dart new file mode 100644 index 0000000..6511c69 --- /dev/null +++ b/lib/services/version_service.dart @@ -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 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)'; + } +} diff --git a/lib/utility/globals.dart b/lib/utility/globals.dart deleted file mode 100644 index e11a118..0000000 --- a/lib/utility/globals.dart +++ /dev/null @@ -1,3 +0,0 @@ -class Globals { - static String appDevPhase = 'Beta'; -} diff --git a/lib/views/graph_view.dart b/lib/views/graph_view.dart deleted file mode 100644 index 345c670..0000000 --- a/lib/views/graph_view.dart +++ /dev/null @@ -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 createState() => _GraphViewState(); -} - -class _GraphViewState extends State { - /// List of colors for the graph lines. - List 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> getCumulativeScores() { - final rounds = widget.gameSession.roundList; - final playerCount = widget.gameSession.players.length; - final playerNames = widget.gameSession.players; - - List> cumulativeScores = List.generate(playerCount, (_) => []); - List 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], - ); - }); - } -} diff --git a/lib/views/settings_view.dart b/lib/views/settings_view.dart deleted file mode 100644 index 2e86122..0000000 --- a/lib/views/settings_view.dart +++ /dev/null @@ -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 createState() => _SettingsViewState(); -} - -class _SettingsViewState extends State { - 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( - 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 _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 ('', ''); - } - } -} diff --git a/pubspec.yaml b/pubspec.yaml index 562b189..da2c087 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: