From ea5577c288d8280263a146e5d2c5d07dcb2e6acd Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:00 +0200 Subject: [PATCH 01/15] Implemented new live edit mode --- .../match_view/match_result_view.dart | 239 +++++++++++------- .../live_edit_list_tile.dart | 97 +++++++ pubspec.yaml | 1 + 3 files changed, 241 insertions(+), 96 deletions(-) create mode 100644 lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart 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 8b41920..146b984 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'; class MatchResultView extends StatefulWidget { /// A view that allows selecting and saving the winner of a match @@ -30,6 +31,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 @@ -88,115 +91,159 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.close), - onPressed: () { - widget.onWinnerChanged?.call(); - Navigator.of(context).pop(_selectedPlayer); - }, - ), + leading: isLiveEditMode + ? IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: () { + setState(() { + isLiveEditMode = false; + }); + }, + ) + : IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(_selectedPlayer); + }, + ), title: Text(widget.match.name), ), body: SafeArea( 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 && rulesetSupportsScoreEntry() + ? 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( - 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, + ); + }, + ) + : 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 { 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; }); }, - ); - }, - ), - ), + child: ListView.builder( + 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); + } + }); + }, + ); + }, + ), + ), + ), + 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 (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 (!isLiveEditMode) ...[ + if (rulesetSupportsScoreEntry()) + // Button to switch to live edit mode + ...[ + CustomWidthButton( + text: 'Live-Edit Modus', + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.secondary, + onPressed: () => setState(() { + isLiveEditMode = true; + }), ), + const SizedBox(height: 10), + ], + + // Save Changes Button + CustomWidthButton( + text: loc.save_changes, + sizeRelativeToWidth: 0.95, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.of(context).pop(_selectedPlayer); + } + : null, ), - ), - CustomWidthButton( - text: loc.save_changes, - sizeRelativeToWidth: 0.95, - onPressed: canSave - ? () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.of(context).pop(_selectedPlayer); - } - : null, - ), + ], ], ), ), 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..4021755 --- /dev/null +++ b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart @@ -0,0 +1,97 @@ +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: 10), + margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), + decoration: CustomTheme.standardBoxDecoration, + child: Column( + children: [ + Text( + widget.title, + style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + ), + Padding( + padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MainMenuButton( + onPressed: () => _score > minScore + ? { + setState(() { + _score--; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.remove_rounded, + ), + SizedBox( + width: 150, + child: NumericText( + _score.toString(), + maxLines: 1, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 48, + fontWeight: FontWeight.w600, + ), + ), + ), + MainMenuButton( + onPressed: () => _score < maxScore + ? { + setState(() { + _score++; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.add_rounded, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 363ea7f..2b66b83 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter + flutter_numeric_text: ^1.3.3 fluttericon: ^2.0.0 font_awesome_flutter: ^11.0.0 intl: any From 5877793b99e9315c4a9ad4f98aaad207030634f7 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:13 +0200 Subject: [PATCH 02/15] Updated secondary button style --- lib/presentation/widgets/buttons/custom_width_button.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, ), From 1e95f1997dba1dee5837e7c21840fa07cb3c4518 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:26 +0200 Subject: [PATCH 03/15] Updated border color --- .../widgets/tiles/{ => match_result_view}/score_list_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename lib/presentation/widgets/tiles/{ => match_result_view}/score_list_tile.dart (97%) 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 97% 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..64492ae 100644 --- a/lib/presentation/widgets/tiles/score_list_tile.dart +++ b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart @@ -62,7 +62,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, ), ), From 3dfd2c7c087745697552b452aa61dba9c66284d9 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:00:33 +0200 Subject: [PATCH 04/15] Moved file --- .../tiles/{ => match_result_view}/custom_radio_list_tile.dart | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/presentation/widgets/tiles/{ => match_result_view}/custom_radio_list_tile.dart (100%) 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 From e25a2bde690dc4374d74a43b6da9c036cc16aff6 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:01:22 +0200 Subject: [PATCH 05/15] Added delay before reversing animation --- lib/presentation/widgets/buttons/main_menu_button.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index c583456..5eb76f1 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -53,10 +53,11 @@ class _MainMenuButtonState extends State _animationController.forward(); }, onTapUp: (_) async { - await _animationController.reverse(); if (mounted) { widget.onPressed(); } + await Future.delayed(const Duration(milliseconds: 100)); + await _animationController.reverse(); }, onTapCancel: () { _animationController.reverse(); From 3be7dac22751dc5ddd189849394eda8d8bb7d43f Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 4 May 2026 15:29:28 +0200 Subject: [PATCH 06/15] Added button to leave live edit mode --- .../match_view/match_result_view.dart | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) 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 146b984..549d86c 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,6 +8,7 @@ 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/buttons/main_menu_button.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'; @@ -91,22 +92,15 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - leading: isLiveEditMode + leading: !isLiveEditMode ? IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - setState(() { - isLiveEditMode = false; - }); - }, - ) - : IconButton( icon: const Icon(Icons.close), onPressed: () { widget.onWinnerChanged?.call(); Navigator.of(context).pop(_selectedPlayer); }, - ), + ) + : null, title: Text(widget.match.name), ), body: SafeArea( @@ -115,17 +109,34 @@ class _MatchResultViewState extends State { Expanded( child: isLiveEditMode && rulesetSupportsScoreEntry() ? ListView.builder( - itemCount: allPlayers.length, + itemCount: allPlayers.length + 1, itemBuilder: (context, index) { - return LiveEditListTile( - title: allPlayers[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); + if (index < allPlayers.length) { + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { + setState(() { + controller[index].text = value.toString(); + }); + }, + value: int.tryParse(controller[index].text) ?? 0, + ); + } else { + return Center( + child: Padding( + padding: const EdgeInsets.only(top: 30), + child: MainMenuButton( + text: 'Ansicht verlassen', + onPressed: () => { + setState(() { + isLiveEditMode = false; + }), + }, + icon: Icons.close, + ), + ), + ); + } }, ) : Container( @@ -147,7 +158,7 @@ class _MatchResultViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${getTitleForRuleset(loc)}:', + getTitleForRuleset(loc), style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, From dc3807356e8fc2037c6bc8ae795e04528a16343e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:28:55 +0200 Subject: [PATCH 07/15] Changed button style --- .../match_view/match_result_view.dart | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) 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 549d86c..ffa9ba3 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,7 +8,6 @@ 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/buttons/main_menu_button.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'; @@ -109,34 +108,17 @@ class _MatchResultViewState extends State { Expanded( child: isLiveEditMode && rulesetSupportsScoreEntry() ? ListView.builder( - itemCount: allPlayers.length + 1, + itemCount: allPlayers.length, itemBuilder: (context, index) { - if (index < allPlayers.length) { - return LiveEditListTile( - title: allPlayers[index].name, - onChanged: (value) { - setState(() { - controller[index].text = value.toString(); - }); - }, - value: int.tryParse(controller[index].text) ?? 0, - ); - } else { - return Center( - child: Padding( - padding: const EdgeInsets.only(top: 30), - child: MainMenuButton( - text: 'Ansicht verlassen', - onPressed: () => { - setState(() { - isLiveEditMode = false; - }), - }, - icon: Icons.close, - ), - ), - ); - } + return LiveEditListTile( + title: allPlayers[index].name, + onChanged: (value) { + setState(() { + controller[index].text = value.toString(); + }); + }, + value: int.tryParse(controller[index].text) ?? 0, + ); }, ) : Container( @@ -254,6 +236,14 @@ class _MatchResultViewState extends State { } : null, ), + ] else ...[ + CustomWidthButton( + text: 'Ansicht verlassen', + sizeRelativeToWidth: 0.95, + onPressed: () => setState(() { + isLiveEditMode = false; + }), + ), ], ], ), From bb46cace031ed6b7b71d0b33f70ac507d355097b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:36:01 +0200 Subject: [PATCH 08/15] Added localizations --- lib/l10n/arb/app_de.arb | 2 ++ lib/l10n/arb/app_en.arb | 2 ++ lib/l10n/generated/app_localizations.dart | 12 ++++++++++++ lib/l10n/generated/app_localizations_de.dart | 6 ++++++ lib/l10n/generated/app_localizations_en.dart | 6 ++++++ 5 files changed, 28 insertions(+) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 46c780a..65f1813 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -32,6 +32,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)", @@ -50,6 +51,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 a85e1b0..7d54e92 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -366,6 +366,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)", @@ -384,6 +385,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 99c9317..ea52dfc 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -290,6 +290,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: @@ -398,6 +404,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 51b4c62..45859c0 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -111,6 +111,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'; @@ -165,6 +168,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 2b42e47..d8b0261 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -111,6 +111,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'; @@ -165,6 +168,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...'; From 883a32e0caddc7fbba3ce9959142257333566ff8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:38:11 +0200 Subject: [PATCH 09/15] Fixed button --- .../views/main_menu/match_view/match_result_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 ffa9ba3..6a85945 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 @@ -91,6 +91,7 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( + automaticallyImplyLeading: !isLiveEditMode, leading: !isLiveEditMode ? IconButton( icon: const Icon(Icons.close), @@ -209,7 +210,7 @@ class _MatchResultViewState extends State { // Button to switch to live edit mode ...[ CustomWidthButton( - text: 'Live-Edit Modus', + text: loc.live_edit_mode, sizeRelativeToWidth: 0.95, buttonType: ButtonType.secondary, onPressed: () => setState(() { @@ -238,7 +239,7 @@ class _MatchResultViewState extends State { ), ] else ...[ CustomWidthButton( - text: 'Ansicht verlassen', + text: loc.exit_view, sizeRelativeToWidth: 0.95, onPressed: () => setState(() { isLiveEditMode = false; From ec4d6ce5ec951234c240363073ebb8af6b2668ec Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:45:35 +0200 Subject: [PATCH 10/15] Docs --- .../views/main_menu/match_view/match_result_view.dart | 2 ++ 1 file changed, 2 insertions(+) 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 6a85945..0de569a 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 @@ -41,6 +41,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 @@ -60,6 +61,7 @@ class _MatchResultViewState extends State { (index) => TextEditingController()..addListener(() => onTextEnter()), ); + // Prefill fields if (widget.match.mvp.isNotEmpty) { if (rulesetSupportsWinnerSelection()) { _selectedPlayer = allPlayers.firstWhere( From 5bac5f1c38649c750bfb51d171054701ecaff9c8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 5 May 2026 22:45:57 +0200 Subject: [PATCH 11/15] fix: score update --- .../views/main_menu/match_view/match_detail_view.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 2117b77..9b53b15 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 @@ -205,7 +205,9 @@ class _MatchDetailViewState extends State { match: match, onWinnerChanged: () { widget.onMatchUpdate.call(); - setState(() {}); + setState(() { + updateScoresForCurrentMatch(); + }); }, ), ), @@ -333,4 +335,10 @@ class _MatchDetailViewState extends State { return match.game.ruleset == Ruleset.singleWinner || match.game.ruleset == Ruleset.singleLoser; } + + void updateScoresForCurrentMatch() { + db.scoreEntryDao + .getAllMatchScores(matchId: match.id) + .then((scores) => match.scores = scores); + } } From 013fd2918254fd03bec503d00e3dd54fdcfdccc2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Wed, 6 May 2026 20:18:46 +0200 Subject: [PATCH 12/15] Updated LiveEditListTile --- .../match_view/match_result_view.dart | 5 ++ .../live_edit_list_tile.dart | 76 ++++++++++--------- 2 files changed, 47 insertions(+), 34 deletions(-) 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 0c991f1..357012c 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 @@ -110,6 +110,7 @@ class _MatchResultViewState extends State { children: [ Expanded( child: isLiveEditMode && rulesetSupportsScoreEntry() + // Live Edit Mode ? ListView.builder( itemCount: allPlayers.length, itemBuilder: (context, index) { @@ -124,6 +125,7 @@ class _MatchResultViewState extends State { ); }, ) + // Normal Mode : Container( margin: const EdgeInsets.symmetric( horizontal: 12, @@ -150,6 +152,8 @@ class _MatchResultViewState extends State { ), ), const SizedBox(height: 10), + + // Show player selection if (rulesetSupportsWinnerSelection()) Expanded( child: RadioGroup( @@ -182,6 +186,7 @@ class _MatchResultViewState extends State { ), ), ), + // Show score entry if (rulesetSupportsScoreEntry()) Expanded( child: ListView.separated( 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 index 4021755..80243b8 100644 --- 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 @@ -35,32 +35,35 @@ class _LiveEditListTileState extends State { @override Widget build(BuildContext context) { return Container( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), margin: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: CustomTheme.standardBoxDecoration, - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Text( - widget.title, - style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold), + MainMenuButton( + onPressed: () => _score > minScore + ? { + setState(() { + _score--; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.remove_rounded, ), - Padding( - padding: const EdgeInsets.only(left: 20, right: 20, bottom: 10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + Expanded( + child: Column( children: [ - MainMenuButton( - onPressed: () => _score > minScore - ? { - setState(() { - _score--; - if (widget.onChanged != null) { - widget.onChanged!(_score); - } - }), - } - : null, - icon: Icons.remove_rounded, + Text( + widget.title, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), ), SizedBox( width: 150, @@ -68,28 +71,33 @@ class _LiveEditListTileState extends State { _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, - icon: Icons.add_rounded, - ), ], ), ), + MainMenuButton( + onPressed: () => _score < maxScore + ? { + setState(() { + _score++; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, + icon: Icons.add_rounded, + ), ], ), ); From 90331bfc0779a9fc9234567ecd863f71d776d085 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 14:29:01 +0200 Subject: [PATCH 13/15] fix: button view logic --- .../match_view/match_result_view.dart | 78 ++++++++----------- 1 file changed, 33 insertions(+), 45 deletions(-) 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 357012c..2c6976e 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 @@ -50,7 +50,7 @@ class _MatchResultViewState extends State { @override void initState() { db = Provider.of(context, listen: false); - ruleset = widget.match.game.ruleset; + ruleset = Ruleset.highestScore; //widget.match.game.ruleset; canSave = !rulesetSupportsScoreEntry(); allPlayers = widget.match.players; @@ -93,16 +93,13 @@ class _MatchResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( - automaticallyImplyLeading: !isLiveEditMode, - leading: !isLiveEditMode - ? IconButton( - icon: const Icon(Icons.close), - onPressed: () { - widget.onWinnerChanged?.call(); - Navigator.of(context).pop(_selectedPlayer); - }, - ) - : null, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(_selectedPlayer); + }, + ), title: Text(widget.match.name), ), body: SafeArea( @@ -212,47 +209,38 @@ class _MatchResultViewState extends State { ), ), ), - if (!isLiveEditMode) ...[ - if (rulesetSupportsScoreEntry()) - // Button to switch to live edit mode - ...[ - CustomWidthButton( - text: loc.live_edit_mode, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.secondary, - onPressed: () => setState(() { - isLiveEditMode = true; - }), - ), - const SizedBox(height: 10), - ], - // Save Changes Button + if (rulesetSupportsScoreEntry()) + // Button to switch to live edit mode + ...[ CustomWidthButton( - text: loc.save_changes, - sizeRelativeToWidth: 0.95, - onPressed: canSave - ? () async { - final ending = DateTime.now(); - await db.matchDao.updateMatchEndedAt( - matchId: widget.match.id, - endedAt: ending, - ); - await _handleSaving(); - if (!context.mounted) return; - Navigator.of(context).pop(_selectedPlayer); - } - : null, - ), - ] else ...[ - CustomWidthButton( - text: loc.exit_view, + text: isLiveEditMode ? loc.exit_view : loc.live_edit_mode, sizeRelativeToWidth: 0.95, + buttonType: ButtonType.secondary, onPressed: () => setState(() { - isLiveEditMode = false; + isLiveEditMode = !isLiveEditMode; }), ), + const SizedBox(height: 10), ], + + // Save Changes Button + CustomWidthButton( + text: loc.save_changes, + sizeRelativeToWidth: 0.95, + onPressed: canSave + ? () async { + final ending = DateTime.now(); + await db.matchDao.updateMatchEndedAt( + matchId: widget.match.id, + endedAt: ending, + ); + await _handleSaving(); + if (!context.mounted) return; + Navigator.of(context).pop(_selectedPlayer); + } + : null, + ), ], ), ), From 8dbf2a573ebb35b866a4c5299a021815c9064d70 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 14:39:28 +0200 Subject: [PATCH 14/15] feat: negative numbers in score list tile --- .../match_result_view/score_list_tile.dart | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/score_list_tile.dart index 64492ae..e4cfff9 100644 --- a/lib/presentation/widgets/tiles/match_result_view/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, @@ -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); + } } From 40e2229aa58d8c71160943093e23324ced7a970e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 9 May 2026 14:52:12 +0200 Subject: [PATCH 15/15] feat: long press adds 10 points repeatedly --- .../widgets/buttons/main_menu_button.dart | 37 ++++++++++++++++++- .../live_edit_list_tile.dart | 20 ++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index 5eb76f1..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,15 +64,29 @@ class _MainMenuButtonState extends State child: GestureDetector( onTapDown: (_) { _animationController.forward(); + 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 { - if (mounted) { + _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( @@ -93,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/match_result_view/live_edit_list_tile.dart b/lib/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart index 80243b8..d663efc 100644 --- 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 @@ -53,6 +53,16 @@ class _LiveEditListTileState extends State { }), } : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score -= 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, icon: Icons.remove_rounded, ), Expanded( @@ -96,6 +106,16 @@ class _LiveEditListTileState extends State { }), } : null, + onLongPressed: () => _score > minScore + ? { + setState(() { + _score += 10; + if (widget.onChanged != null) { + widget.onChanged!(_score); + } + }), + } + : null, icon: Icons.add_rounded, ), ],