diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 9844ba7..5492bab 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -56,6 +56,7 @@ "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", + "exit_view": "Ansicht verlassen", "export_canceled": "Export abgebrochen", "export_data": "Daten exportieren", "format_exception": "Formatfehler (siehe Konsole)", @@ -74,6 +75,7 @@ "legal": "Rechtliches", "legal_notice": "Impressum", "licenses": "Lizenzen", + "live_edit_mode": "Live-Bearbeitungsmodus", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "match_profile": "Spielprofil", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index a873419..7fb944b 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -1,352 +1,6 @@ { "@@locale": "en", - "@all_players": { - "description": "Label for all players list" - }, - "@all_players_selected": { - "description": "Message when all players are added to selection" - }, - "@amount_of_matches": { - "description": "Label for amount of matches statistic" - }, - "@app_name": { - "description": "The name of the App" - }, - "@best_player": { - "description": "Label for best player statistic" - }, - "@cancel": { - "description": "Cancel button text" - }, - "@choose_game": { - "description": "Label for choosing a game" - }, - "@choose_group": { - "description": "Label for choosing a group" - }, - "@choose_ruleset": { - "description": "Label for choosing a ruleset" - }, - "@could_not_add_player": { - "description": "Error message when adding a player fails" - }, - "@create_group": { - "description": "Button text to create a group" - }, - "@create_match": { - "description": "Button text to create a match" - }, - "@create_new_group": { - "description": "Appbar text to create a new group" - }, - "@create_new_match": { - "description": "Appbar text to create a new match" - }, - "@created_on": { - "description": "Label for creation date" - }, - "@data": { - "description": "Data label" - }, - "@data_successfully_deleted": { - "description": "Success message after deleting data" - }, - "@data_successfully_exported": { - "description": "Success message after exporting data" - }, - "@data_successfully_imported": { - "description": "Success message after importing data" - }, - "@days_ago": { - "description": "Date format for days ago", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@delete": { - "description": "Delete button text" - }, - "@delete_all_data": { - "description": "Confirmation dialog for deleting all data" - }, - "@delete_group": { - "description": "Confirmation dialog for deleting a group" - }, - "@delete_match": { - "description": "Button text to delete a match" - }, - "@drag_to_set_placement": { - "description": "Label for dragging to set placement" - }, - "@edit_group": { - "description": "Button & Appbar label for editing a group" - }, - "@edit_match": { - "description": "Button & Appbar label for editing a match" - }, - "@enter_points": { - "description": "Label to enter players points" - }, - "@enter_results": { - "description": "Button text to enter match results" - }, - "@error_creating_group": { - "description": "Error message when group creation fails" - }, - "@error_deleting_group": { - "description": "Error message when group deletion fails" - }, - "@error_editing_group": { - "description": "Error message when group editing fails" - }, - "@error_reading_file": { - "description": "Error message when file cannot be read" - }, - "@export_canceled": { - "description": "Message when export is canceled" - }, - "@export_data": { - "description": "Export data menu item" - }, - "@format_exception": { - "description": "Error message for format exceptions" - }, - "@game": { - "description": "Game label" - }, - "@game_name": { - "description": "Placeholder for game name search" - }, - "@group": { - "description": "Group label" - }, - "@group_name": { - "description": "Placeholder for group name input" - }, - "@group_profile": { - "description": "Title for group profile view" - }, - "@groups": { - "description": "Label for groups" - }, - "@home": { - "description": "Home tab label" - }, - "@import_canceled": { - "description": "Message when import is canceled" - }, - "@import_data": { - "description": "Import data menu item" - }, - "@info": { - "description": "Info label" - }, - "@invalid_schema": { - "description": "Error message for invalid schema" - }, - "@least_points": { - "description": "Title for least points ruleset" - }, - "@legal": { - "description": "Legal section header" - }, - "@legal_notice": { - "description": "Legal notice menu item" - }, - "@licenses": { - "description": "Licenses menu item" - }, - "@match_in_progress": { - "description": "Message when match is in progress" - }, - "@match_name": { - "description": "Placeholder for match name input" - }, - "@match_profile": { - "description": "Title for match profile view" - }, - "@matches": { - "description": "Label for matches" - }, - "@members": { - "description": "Label for group members" - }, - "@most_points": { - "description": "Title for most points ruleset" - }, - "@no_data_available": { - "description": "Message when no data in the statistic tiles is given" - }, - "@no_groups_created_yet": { - "description": "Message when no groups exist" - }, - "@no_licenses_found": { - "description": "Message when no licenses are found" - }, - "@no_license_text_available": { - "description": "Message when no license text is available" - }, - "@no_matches_created_yet": { - "description": "Message when no matches exist" - }, - "@no_players_created_yet": { - "description": "Message when no players exist" - }, - "@no_players_found_with_that_name": { - "description": "Message when search returns no results" - }, - "@no_players_selected": { - "description": "Message when no players are selected" - }, - "@no_recent_matches_available": { - "description": "Message when no recent matches exist" - }, - "@no_results_entered_yet": { - "description": "Message when no results have been entered yet" - }, - "@no_second_match_available": { - "description": "Message when no second match exists" - }, - "@no_statistics_available": { - "description": "Message when no statistics are available, because no matches were played yet" - }, - "@none": { - "description": "None option label" - }, - "@none_group": { - "description": "None group option label" - }, - "@not_available": { - "description": "Abbreviation for not available" - }, - "@placement": { - "description": "Title for placement ruleset" - }, - "@place": { - "description": "Label for placement text in match detail view" - }, - "@played_matches": { - "description": "Label for played matches statistic" - }, - "@player_name": { - "description": "Placeholder for player name input" - }, - "@players": { - "description": "Players label" - }, - "@players_count": { - "description": "Shows the number of players", - "placeholders": { - "count": { - "type": "int" - } - } - }, - "@points": { - "description": "Points label" - }, - "@privacy_policy": { - "description": "Privacy policy menu item" - }, - "@quick_create": { - "description": "Title for quick create section" - }, - "@recent_matches": { - "description": "Title for recent matches section" - }, - "@results": { - "description": "Label for match results" - }, - "@ruleset": { - "description": "Ruleset label" - }, - "@ruleset_least_points": { - "description": "Description for least points ruleset" - }, - "@ruleset_most_points": { - "description": "Description for most points ruleset" - }, - "@ruleset_placement": { - "description": "Description for placement ruleset" - }, - "@ruleset_single_loser": { - "description": "Description for single loser ruleset" - }, - "@ruleset_single_winner": { - "description": "Description for single winner ruleset" - }, - "@save_changes": { - "description": "Save changes button text" - }, - "@search_for_groups": { - "description": "Hint text for group search input field" - }, - "@search_for_players": { - "description": "Hint text for player search input field" - }, - "@select_winner": { - "description": "Label to select the winner" - }, - "@select_loser": { - "description": "Label to select the loser" - }, - "@selected_players": { - "description": "Shows the number of selected players" - }, - "@settings": { - "description": "Label for the App Settings" - }, - "@single_loser": { - "description": "Title for single loser ruleset" - }, - "@single_winner": { - "description": "Title for single winner ruleset" - }, - "@statistics": { - "description": "Statistics tab label" - }, - "@stats": { - "description": "Stats tab label (short)" - }, - "@successfully_added_player": { - "description": "Success message when adding a player", - "placeholders": { - "playerName": { - "type": "String", - "example": "John" - } - } - }, - "@there_is_no_group_matching_your_search": { - "description": "Message when search returns no groups" - }, - "@this_cannot_be_undone": { - "description": "Warning message for irreversible actions" - }, - "@today_at": { - "description": "Date format for today" - }, - "@undo": { - "description": "Undo button text" - }, - "@unknown_exception": { - "description": "Error message for unknown exceptions" - }, - "@winner": { - "description": "Winner label" - }, - "@winrate": { - "description": "Label for winrate statistic" - }, - "@wins": { - "description": "Label for wins statistic" - }, - "@yesterday_at": { - "description": "Date format for yesterday" - }, "all_players": "All players", "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", @@ -403,6 +57,7 @@ "error_deleting_group": "Error while deleting group, please try again", "error_editing_group": "Error while editing group, please try again", "error_reading_file": "Error reading file", + "exit_view": "Exit View", "export_canceled": "Export canceled", "export_data": "Export data", "format_exception": "Format Exception (see console)", @@ -421,6 +76,7 @@ "legal": "Legal", "legal_notice": "Legal Notice", "licenses": "Licenses", + "live_edit_mode": "Live Edit Mode", "match_in_progress": "Match in progress...", "match_name": "Match name", "match_profile": "Match Profile", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index fccffa1..3fa991e 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -392,6 +392,12 @@ abstract class AppLocalizations { /// **'Error reading file'** String get error_reading_file; + /// No description provided for @exit_view. + /// + /// In en, this message translates to: + /// **'Exit View'** + String get exit_view; + /// Message when export is canceled /// /// In en, this message translates to: @@ -500,6 +506,12 @@ abstract class AppLocalizations { /// **'Licenses'** String get licenses; + /// No description provided for @live_edit_mode. + /// + /// In en, this message translates to: + /// **'Live Edit Mode'** + String get live_edit_mode; + /// Message when match is in progress /// /// In en, this message translates to: diff --git a/lib/l10n/generated/app_localizations_de.dart b/lib/l10n/generated/app_localizations_de.dart index f91e0ba..344eb38 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -171,6 +171,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get error_reading_file => 'Fehler beim Lesen der Datei'; + @override + String get exit_view => 'Ansicht verlassen'; + @override String get export_canceled => 'Export abgebrochen'; @@ -225,6 +228,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get licenses => 'Lizenzen'; + @override + String get live_edit_mode => 'Live-Bearbeitungsmodus'; + @override String get match_in_progress => 'Spiel läuft...'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 3bc7b2f..a1ebd35 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -171,6 +171,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get error_reading_file => 'Error reading file'; + @override + String get exit_view => 'Exit View'; + @override String get export_canceled => 'Export canceled'; @@ -225,6 +228,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get licenses => 'Licenses'; + @override + String get live_edit_mode => 'Live Edit Mode'; + @override String get match_in_progress => 'Match in progress...'; diff --git a/lib/presentation/views/main_menu/match_view/match_detail_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart index 04e7505..86c26c6 100644 --- a/lib/presentation/views/main_menu/match_view/match_detail_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -240,6 +240,9 @@ class _MatchDetailViewState extends State { match: match, onWinnerChanged: () { widget.onMatchUpdate.call(); + setState(() { + updateScoresForCurrentMatch(); + }); }, ), ), @@ -428,4 +431,10 @@ class _MatchDetailViewState extends State { return '${number}th'; } } + + void updateScoresForCurrentMatch() { + db.scoreEntryDao + .getAllMatchScores(matchId: match.id) + .then((scores) => match.scores = scores); + } } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 06e0da5..61b2a55 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -8,8 +8,9 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/score_entry.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; -import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_list_tile.dart'; class MatchResultView extends StatefulWidget { @@ -31,6 +32,8 @@ class MatchResultView extends StatefulWidget { class _MatchResultViewState extends State { late final AppDatabase db; + bool isLiveEditMode = false; + late final Ruleset ruleset; /// List of all players who participated in the match @@ -39,6 +42,7 @@ class _MatchResultViewState extends State { /// List of text controllers for score entry, one for each player late final List controller; + /// Flag to indicate if the save button should be enabled late bool canSave; /// Currently selected winner player @@ -58,6 +62,7 @@ class _MatchResultViewState extends State { (index) => TextEditingController()..addListener(() => onTextEnter()), ); + // Prefill fields if (widget.match.mvp.isNotEmpty) { if (rulesetSupportsWinnerSelection()) { _selectedPlayer = allPlayers.firstWhere( @@ -108,186 +113,232 @@ class _MatchResultViewState extends State { child: Column( children: [ Expanded( - child: Container( - margin: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 10, - ), - decoration: BoxDecoration( - color: CustomTheme.boxColor, - border: Border.all(color: CustomTheme.boxBorderColor), - borderRadius: BorderRadius.circular(12), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${getTitleForRuleset(loc)}:', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 10), - if (rulesetSupportsWinnerSelection()) - Expanded( - child: RadioGroup( - groupValue: _selectedPlayer, - onChanged: (Player? value) async { + child: isLiveEditMode + // Live Edit Mode + ? ListView.builder( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { setState(() { - _selectedPlayer = value; + controller[index].text = value.toString(); }); }, - child: ListView.builder( - physics: const NeverScrollableScrollPhysics(), - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return CustomRadioListTile( - text: allPlayers[index].name, - value: allPlayers[index], - onContainerTap: (value) async { + value: int.tryParse(controller[index].text) ?? 0, + ); + }, + ) + // Normal Container + : Container( + margin: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 10, + ), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorderColor), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + getTitleForRuleset(loc), + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 10), + + // Show player selection + if (rulesetSupportsWinnerSelection()) + Expanded( + child: RadioGroup( + groupValue: _selectedPlayer, + onChanged: (Player? value) async { setState(() { - // Check if the already selected player is the same as the newly tapped player. - if (_selectedPlayer == value) { - // If yes deselected the player by setting it to null. - _selectedPlayer = null; - } else { - // If no assign the newly tapped player to the selected player. - (_selectedPlayer = value); - } + _selectedPlayer = value; }); }, - ); - }, - ), - ), - ), - if (rulesetSupportsScoreEntry()) - Expanded( - child: ListView.separated( - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return ScoreListTile( - text: allPlayers[index].name, - controller: controller[index], - ); - }, - separatorBuilder: (BuildContext context, int index) { - return const Padding( - padding: EdgeInsets.symmetric(vertical: 8.0), - child: Divider(indent: 20), - ); - }, - ), - ), - if (rulesetSupportsPlacement()) - Expanded( - child: Row( - children: [ - // Placement indicators - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Column( - children: [ - for (int i = 0; i < allPlayers.length; i++) - Container( - alignment: Alignment.center, - height: 60, - child: Container( - decoration: BoxDecoration( - color: CustomTheme.boxBorderColor, - borderRadius: CustomTheme - .standardBorderRadiusAll, - ), - alignment: Alignment.center, - height: 50, - width: 50, - child: Text( - ' #${i + 1} ', - style: const TextStyle( - color: CustomTheme.textColor, - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - ), - ], + child: ListView.builder( + physics: const NeverScrollableScrollPhysics(), + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return CustomRadioListTile( + text: allPlayers[index].name, + value: allPlayers[index], + onContainerTap: (value) async { + setState(() { + // Check if the already selected player is the same as the newly tapped player. + if (_selectedPlayer == value) { + // If yes deselected the player by setting it to null. + _selectedPlayer = null; + } else { + // If no assign the newly tapped player to the selected player. + (_selectedPlayer = value); + } + }); + }, + ); + }, + ), ), ), - // Drag list + // Show score entry + if (rulesetSupportsScoreEntry()) Expanded( - child: ReorderableListView.builder( - physics: const NeverScrollableScrollPhysics(), - padding: EdgeInsets.zero, - proxyDecorator: (child, index, animation) { - return AnimatedBuilder( - animation: animation, - child: child, - builder: (context, child) { - final alpha = - (Curves.easeInOut.transform( - animation.value, - ) * - 40) - .toInt(); - return Stack( - children: [ - child!, - Positioned.fill( - left: 4, - top: 4, - right: 4, - bottom: 4, - child: DecoratedBox( + child: ListView.separated( + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return ScoreListTile( + text: allPlayers[index].name, + controller: controller[index], + ); + }, + separatorBuilder: + (BuildContext context, int index) { + return const Padding( + padding: EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Divider(indent: 20), + ); + }, + ), + ), + + // Show draggable placement list + if (rulesetSupportsPlacement()) + Expanded( + child: Row( + children: [ + // Placement indicators + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Column( + children: [ + for ( + int i = 0; + i < allPlayers.length; + i++ + ) + Container( + alignment: Alignment.center, + height: 60, + child: Container( decoration: BoxDecoration( - color: Colors.white.withAlpha( - alpha, - ), + color: + CustomTheme.boxBorderColor, borderRadius: CustomTheme .standardBorderRadiusAll, ), + alignment: Alignment.center, + height: 50, + width: 50, + child: Text( + ' #${i + 1} ', + style: const TextStyle( + color: CustomTheme.textColor, + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), ), ), - ], - ); - }, - ); - }, - onReorder: (int oldIndex, int newIndex) { - setState(() { - if (newIndex > oldIndex) { - newIndex -= 1; - } - final Player item = allPlayers.removeAt( - oldIndex, - ); - allPlayers.insert(newIndex, item); - }); - }, - itemCount: allPlayers.length, - itemBuilder: (context, index) { - return TextIconListTile( - key: ValueKey(allPlayers[index].id), - text: allPlayers[index].name, - icon: Icons.drag_handle, - ); - }, + ], + ), + ), + + // Drag list + Expanded( + child: ReorderableListView.builder( + physics: + const NeverScrollableScrollPhysics(), + padding: EdgeInsets.zero, + proxyDecorator: (child, index, animation) { + return AnimatedBuilder( + animation: animation, + child: child, + builder: (context, child) { + final alpha = + (Curves.easeInOut.transform( + animation.value, + ) * + 40) + .toInt(); + return Stack( + children: [ + child!, + Positioned.fill( + left: 4, + top: 4, + right: 4, + bottom: 4, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.white + .withAlpha(alpha), + borderRadius: CustomTheme + .standardBorderRadiusAll, + ), + ), + ), + ], + ); + }, + ); + }, + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final Player item = allPlayers + .removeAt(oldIndex); + allPlayers.insert(newIndex, item); + }); + }, + itemCount: allPlayers.length, + itemBuilder: (context, index) { + return TextIconListTile( + key: ValueKey(allPlayers[index].id), + text: allPlayers[index].name, + icon: Icons.drag_handle, + ); + }, + ), + ), + ], ), ), - ], - ), + ], ), - ], - ), - ), + ), ), + + if (rulesetSupportsScoreEntry()) + // Button to switch to live edit mode + ...[ + CustomWidthButton( + text: isLiveEditMode ? loc.exit_view : loc.live_edit_mode, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.secondary, + onPressed: () => setState(() { + isLiveEditMode = !isLiveEditMode; + }), + ), + const SizedBox(height: 10), + ], + + // Save Changes Button CustomWidthButton( text: loc.save_changes, sizeRelativeToWidth: 0.95, diff --git a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart index 92f3080..5b9a689 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart @@ -54,6 +54,7 @@ const allDependencies = [ _flutter, _flutter_lints, _flutter_localizations, + _flutter_numeric_text, _flutter_plugin_android_lifecycle, _flutter_popup, _flutter_test, @@ -169,6 +170,7 @@ const dependencies = [ _file_saver, _flutter, _flutter_localizations, + _flutter_numeric_text, _flutter_popup, _fluttericon, _font_awesome_flutter, @@ -2591,6 +2593,42 @@ const _flutter_localizations = Package( devDependencies: [PackageRef('flutter_test')], ); +/// flutter_numeric_text 1.3.3 +const _flutter_numeric_text = Package( + name: 'flutter_numeric_text', + description: 'This widget allows you to animate any text. The widget is easy to use and allows you to seamlessly replace Text(data) with NumericText(data).', + homepage: 'https://github.com/strash/flutter_numeric_text', + repository: 'https://github.com/strash/flutter_numeric_text', + authors: [], + version: '1.3.3', + spdxIdentifiers: ['MIT'], + isMarkdown: false, + isSdk: false, + dependencies: [PackageRef('flutter')], + devDependencies: [PackageRef('flutter_test'), PackageRef('flutter_lints')], + license: '''MIT License + +Copyright (c) 2025 Strash One + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.''', + ); + /// flutter_plugin_android_lifecycle 2.0.34 const _flutter_plugin_android_lifecycle = Package( name: 'flutter_plugin_android_lifecycle', @@ -37713,16 +37751,16 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.''', ); -/// tallee 0.0.28+262 +/// tallee 0.0.29+263 const _tallee = Package( name: 'tallee', description: 'Tracking App for Card Games', authors: [], - version: '0.0.28+262', + version: '0.0.29+263', spdxIdentifiers: ['LGPL-3.0'], isMarkdown: false, isSdk: false, - dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], + dependencies: [PackageRef('clock'), PackageRef('collection'), PackageRef('cupertino_icons'), PackageRef('drift'), PackageRef('drift_flutter'), PackageRef('file_picker'), PackageRef('file_saver'), PackageRef('flutter'), PackageRef('flutter_localizations'), PackageRef('flutter_numeric_text'), PackageRef('flutter_popup'), PackageRef('fluttericon'), PackageRef('font_awesome_flutter'), PackageRef('intl'), PackageRef('json_schema'), PackageRef('package_info_plus'), PackageRef('path_provider'), PackageRef('provider'), PackageRef('skeletonizer'), PackageRef('url_launcher'), PackageRef('uuid')], devDependencies: [PackageRef('flutter_test'), PackageRef('build_runner'), PackageRef('dart_pubspec_licenses'), PackageRef('drift_dev'), PackageRef('flutter_lints')], license: '''GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/lib/presentation/widgets/buttons/custom_width_button.dart b/lib/presentation/widgets/buttons/custom_width_button.dart index 489ceae..4fde6f8 100644 --- a/lib/presentation/widgets/buttons/custom_width_button.dart +++ b/lib/presentation/widgets/buttons/custom_width_button.dart @@ -89,7 +89,7 @@ class CustomWidthButton extends StatelessWidget { MediaQuery.sizeOf(context).width * sizeRelativeToWidth, 60, ), - side: BorderSide(color: borderSideColor, width: 2), + side: BorderSide(color: borderSideColor, width: 3), shape: RoundedRectangleBorder( borderRadius: CustomTheme.standardBorderRadiusAll, ), diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index c583456..c5c7a34 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; class MainMenuButton extends StatefulWidget { @@ -10,6 +12,7 @@ class MainMenuButton extends StatefulWidget { required this.onPressed, required this.icon, this.text, + this.onLongPressed, }); /// The callback to be invoked when the button is pressed. @@ -21,6 +24,8 @@ class MainMenuButton extends StatefulWidget { /// The text of the button. final String? text; + final void Function()? onLongPressed; + @override State createState() => _MainMenuButtonState(); } @@ -30,6 +35,14 @@ class _MainMenuButtonState extends State late AnimationController _animationController; late Animation _scaleAnimation; + /// How long the button needs to be pressed to register it as long press + Timer? _longPressTimer; + + /// How much time between two onLongPressed calls + Timer? _repeatTimer; + + bool _isLongPressing = false; + @override void initState() { super.initState(); @@ -51,14 +64,29 @@ class _MainMenuButtonState extends State child: GestureDetector( onTapDown: (_) { _animationController.forward(); - }, - onTapUp: (_) async { - await _animationController.reverse(); - if (mounted) { - widget.onPressed(); + if (widget.onLongPressed != null) { + _longPressTimer = Timer(const Duration(milliseconds: 400), () { + _isLongPressing = true; + widget.onLongPressed?.call(); + _repeatTimer = Timer.periodic( + const Duration(milliseconds: 250), + (_) => widget.onLongPressed?.call(), + ); + }); } }, + onTapUp: (_) async { + _cancelTimers(); + if (mounted && !_isLongPressing) { + widget.onPressed(); + } + _isLongPressing = false; + await Future.delayed(const Duration(milliseconds: 100)); + await _animationController.reverse(); + }, onTapCancel: () { + _isLongPressing = false; + _cancelTimers(); _animationController.reverse(); }, child: Container( @@ -92,7 +120,15 @@ class _MainMenuButtonState extends State @override void dispose() { + _cancelTimers(); _animationController.dispose(); super.dispose(); } + + void _cancelTimers() { + _longPressTimer?.cancel(); + _longPressTimer = null; + _repeatTimer?.cancel(); + _repeatTimer = null; + } } diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart similarity index 100% rename from lib/presentation/widgets/tiles/custom_radio_list_tile.dart rename to lib/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart diff --git a/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart new file mode 100644 index 0000000..d663efc --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_numeric_text/flutter_numeric_text.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; + +class LiveEditListTile extends StatefulWidget { + const LiveEditListTile({ + super.key, + required this.title, + required this.value, + this.onChanged, + }); + + final String title; + + final int value; + + final void Function(int newValue)? onChanged; + + @override + State createState() => _LiveEditListTileState(); +} + +class _LiveEditListTileState extends State { + int _score = 0; + final int maxScore = 9999; + final int minScore = -9999; + + @override + void initState() { + _score = widget.value; + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), + margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: CustomTheme.standardBoxDecoration, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + MainMenuButton( + onPressed: () => _score > minScore + ? { + setState(() { + _score--; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score -= 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.remove_rounded, + ), + Expanded( + child: Column( + children: [ + Text( + widget.title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + SizedBox( + width: 150, + child: NumericText( + _score.toString(), + maxLines: 1, + textAlign: TextAlign.center, + textWidthBasis: TextWidthBasis.longestLine, + textHeightBehavior: const TextHeightBehavior( + applyHeightToFirstAscent: false, + applyHeightToLastDescent: false, + ), + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + MainMenuButton( + onPressed: () => _score < maxScore + ? { + setState(() { + _score++; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score += 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.add_rounded, + ), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/score_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart similarity index 75% rename from lib/presentation/widgets/tiles/score_list_tile.dart rename to lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart index 52103fa..e4cfff9 100644 --- a/lib/presentation/widgets/tiles/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart @@ -40,9 +40,13 @@ class ScoreListTile extends StatelessWidget { height: 40, child: TextField( controller: controller, - keyboardType: TextInputType.number, - maxLength: 4, - inputFormatters: [FilteringTextInputFormatter.digitsOnly], + keyboardType: const TextInputType.numberWithOptions(signed: true), + maxLength: 5, + inputFormatters: [ + TextInputFormatter.withFunction((oldValue, newValue) { + return isValidScoreInput(newValue.text) ? newValue : oldValue; + }), + ], textAlign: TextAlign.center, style: const TextStyle( fontSize: 16, @@ -62,7 +66,7 @@ class ScoreListTile extends StatelessWidget { enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide( - color: CustomTheme.textColor.withAlpha(100), + color: CustomTheme.textColor.withAlpha(250), width: 2, ), ), @@ -80,4 +84,21 @@ class ScoreListTile extends StatelessWidget { ), ); } + + /// Validates the input for the score text field. + bool isValidScoreInput(String text) { + if (text.isEmpty || text == '-') { + return true; + } + + final isNegative = text.startsWith('-'); + final digits = isNegative ? text.substring(1) : text; + + if (digits.isEmpty || digits.length > 4) { + return false; + } + + // CHeck if all characters are digits 0 <= x <= 9 + return digits.codeUnits.every((unit) => unit >= 48 && unit <= 57); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 5e3ec4c..f514b01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.28+262 +version: 0.0.29+263 environment: sdk: ^3.8.1 @@ -18,6 +18,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_numeric_text: ^1.3.3 flutter_popup: ^3.3.9 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0