Merge branch 'development' into feature/206-Neuer-Regelsatz-Platzierung
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 48s

# Conflicts:
#	lib/l10n/generated/app_localizations.dart
#	lib/presentation/views/main_menu/match_view/match_detail_view.dart
#	lib/presentation/views/main_menu/match_view/match_result_view.dart
This commit is contained in:
2026-05-09 23:04:11 +02:00
14 changed files with 485 additions and 522 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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:

View File

@@ -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...';

View File

@@ -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...';

View File

@@ -240,6 +240,9 @@ class _MatchDetailViewState extends State<MatchDetailView> {
match: match,
onWinnerChanged: () {
widget.onMatchUpdate.call();
setState(() {
updateScoresForCurrentMatch();
});
},
),
),
@@ -428,4 +431,10 @@ class _MatchDetailViewState extends State<MatchDetailView> {
return '${number}th';
}
}
void updateScoresForCurrentMatch() {
db.scoreEntryDao
.getAllMatchScores(matchId: match.id)
.then((scores) => match.scores = scores);
}
}

View File

@@ -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<MatchResultView> {
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<MatchResultView> {
/// List of text controllers for score entry, one for each player
late final List<TextEditingController> 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<MatchResultView> {
(index) => TextEditingController()..addListener(() => onTextEnter()),
);
// Prefill fields
if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) {
_selectedPlayer = allPlayers.firstWhere(
@@ -108,7 +113,24 @@ class _MatchResultViewState extends State<MatchResultView> {
child: Column(
children: [
Expanded(
child: Container(
child: isLiveEditMode
// Live Edit Mode
? ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return LiveEditListTile(
title: allPlayers[index].name,
onChanged: (value) {
setState(() {
controller[index].text = value.toString();
});
},
value: int.tryParse(controller[index].text) ?? 0,
);
},
)
// Normal Container
: Container(
margin: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
@@ -127,13 +149,15 @@ class _MatchResultViewState extends State<MatchResultView> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${getTitleForRuleset(loc)}:',
getTitleForRuleset(loc),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
// Show player selection
if (rulesetSupportsWinnerSelection())
Expanded(
child: RadioGroup<Player>(
@@ -167,6 +191,8 @@ class _MatchResultViewState extends State<MatchResultView> {
),
),
),
// Show score entry
if (rulesetSupportsScoreEntry())
Expanded(
child: ListView.separated(
@@ -177,14 +203,19 @@ class _MatchResultViewState extends State<MatchResultView> {
controller: controller[index],
);
},
separatorBuilder: (BuildContext context, int index) {
separatorBuilder:
(BuildContext context, int index) {
return const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
padding: EdgeInsets.symmetric(
vertical: 8.0,
),
child: Divider(indent: 20),
);
},
),
),
// Show draggable placement list
if (rulesetSupportsPlacement())
Expanded(
child: Row(
@@ -194,13 +225,18 @@ class _MatchResultViewState extends State<MatchResultView> {
padding: const EdgeInsets.only(right: 8.0),
child: Column(
children: [
for (int i = 0; i < allPlayers.length; i++)
for (
int i = 0;
i < allPlayers.length;
i++
)
Container(
alignment: Alignment.center,
height: 60,
child: Container(
decoration: BoxDecoration(
color: CustomTheme.boxBorderColor,
color:
CustomTheme.boxBorderColor,
borderRadius: CustomTheme
.standardBorderRadiusAll,
),
@@ -224,7 +260,8 @@ class _MatchResultViewState extends State<MatchResultView> {
// Drag list
Expanded(
child: ReorderableListView.builder(
physics: const NeverScrollableScrollPhysics(),
physics:
const NeverScrollableScrollPhysics(),
padding: EdgeInsets.zero,
proxyDecorator: (child, index, animation) {
return AnimatedBuilder(
@@ -247,9 +284,8 @@ class _MatchResultViewState extends State<MatchResultView> {
bottom: 4,
child: DecoratedBox(
decoration: BoxDecoration(
color: Colors.white.withAlpha(
alpha,
),
color: Colors.white
.withAlpha(alpha),
borderRadius: CustomTheme
.standardBorderRadiusAll,
),
@@ -265,9 +301,8 @@ class _MatchResultViewState extends State<MatchResultView> {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final Player item = allPlayers.removeAt(
oldIndex,
);
final Player item = allPlayers
.removeAt(oldIndex);
allPlayers.insert(newIndex, item);
});
},
@@ -288,6 +323,22 @@ class _MatchResultViewState extends State<MatchResultView> {
),
),
),
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,

View File

@@ -54,6 +54,7 @@ const allDependencies = <Package>[
_flutter,
_flutter_lints,
_flutter_localizations,
_flutter_numeric_text,
_flutter_plugin_android_lifecycle,
_flutter_popup,
_flutter_test,
@@ -169,6 +170,7 @@ const dependencies = <Package>[
_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

View File

@@ -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,
),

View File

@@ -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<MainMenuButton> createState() => _MainMenuButtonState();
}
@@ -30,6 +35,14 @@ class _MainMenuButtonState extends State<MainMenuButton>
late AnimationController _animationController;
late Animation<double> _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<MainMenuButton>
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<MainMenuButton>
@override
void dispose() {
_cancelTimers();
_animationController.dispose();
super.dispose();
}
void _cancelTimers() {
_longPressTimer?.cancel();
_longPressTimer = null;
_repeatTimer?.cancel();
_repeatTimer = null;
}
}

View File

@@ -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<LiveEditListTile> createState() => _LiveEditListTileState();
}
class _LiveEditListTileState extends State<LiveEditListTile> {
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,
),
],
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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