From 45a419cae7010c55393e2df0fd95accc3b9b0dbb Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 10 Jan 2026 20:14:37 +0100 Subject: [PATCH 01/54] implement group edit view --- lib/l10n/arb/app_de.arb | 4 + lib/l10n/arb/app_en.arb | 20 +- lib/l10n/generated/app_localizations.dart | 28 ++- lib/l10n/generated/app_localizations_de.dart | 16 +- lib/l10n/generated/app_localizations_en.dart | 14 ++ .../group_view/create_group_view.dart | 113 ----------- .../group_view/group_detail_view.dart | 181 ++++++++++++++++++ .../main_menu/group_view/groups_view.dart | 16 +- .../create_match/create_match_view.dart | 4 + .../views/main_menu/settings_view.dart | 5 + .../widgets/player_selection.dart | 98 ++++++---- .../widgets/tiles/group_tile.dart | 107 ++++++----- 12 files changed, 395 insertions(+), 211 deletions(-) delete mode 100644 lib/presentation/views/main_menu/group_view/create_group_view.dart create mode 100644 lib/presentation/views/main_menu/group_view/group_detail_view.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 4d86460..938fbbd 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -18,7 +18,11 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen?", + "delete_all_data": "Diese Gruppe löschen?", + "edit_group": "Gruppe bearbeiten", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", + "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", "export_canceled": "Export abgebrochen", "export_data": "Daten exportieren", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 17c3b06..487b73a 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -34,10 +34,10 @@ "description": "Button text to create a match" }, "@create_new_group": { - "description": "Button text to create a new group" + "description": "Appbar text to create a group" }, "@create_new_match": { - "description": "Button text to create a new match" + "description": "Appbar text to create a match" }, "@data_successfully_deleted": { "description": "Success message after deleting data" @@ -62,9 +62,21 @@ "@delete_all_data": { "description": "Confirmation dialog for deleting all data" }, + "@delete_group": { + "description": "Confirmation dialog for deleting a group" + }, + "@edit_group": { + "description": "Button & Appbar label for editing a group" + }, "@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" }, @@ -278,7 +290,11 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data?", + "delete_group": "Delete this group?", + "edit_group": "Edit Group", "error_creating_group": "Error while creating group, please try again", + "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", "export_canceled": "Export canceled", "export_data": "Export data", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 5080ff3..5d8f454 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -164,13 +164,13 @@ abstract class AppLocalizations { /// **'Create match'** String get create_match; - /// Button text to create a new group + /// Appbar text to create a group /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Button text to create a new match + /// Appbar text to create a match /// /// In en, this message translates to: /// **'Create new match'** @@ -212,12 +212,36 @@ abstract class AppLocalizations { /// **'Delete all data?'** String get delete_all_data; + /// Confirmation dialog for deleting a group + /// + /// In en, this message translates to: + /// **'Delete this group?'** + String get delete_group; + + /// Button & Appbar label for editing a group + /// + /// In en, this message translates to: + /// **'Edit Group'** + String get edit_group; + /// Error message when group creation fails /// /// In en, this message translates to: /// **'Error while creating group, please try again'** String get error_creating_group; + /// Error message when group deletion fails + /// + /// In en, this message translates to: + /// **'Error while deleting group, please try again'** + String get error_deleting_group; + + /// Error message when group editing fails + /// + /// In en, this message translates to: + /// **'Error while editing group, please try again'** + String get error_editing_group; + /// Error message when file cannot be read /// /// 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 c720941..22f817e 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -67,12 +67,26 @@ class AppLocalizationsDe extends AppLocalizations { String get delete => 'Löschen'; @override - String get delete_all_data => 'Alle Daten löschen?'; + String get delete_all_data => 'Diese Gruppe löschen?'; + + @override + String get delete_group => 'Delete this group?'; + + @override + String get edit_group => 'Gruppe bearbeiten'; @override String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; + @override + String get error_deleting_group => + 'Fehler beim Löschen der Gruppe, bitte erneut versuchen'; + + @override + String get error_editing_group => + 'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen'; + @override String get error_reading_file => 'Fehler beim Lesen der Datei'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index cd71035..a28140b 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -69,10 +69,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_all_data => 'Delete all data?'; + @override + String get delete_group => 'Delete this group?'; + + @override + String get edit_group => 'Edit Group'; + @override String get error_creating_group => 'Error while creating group, please try again'; + @override + String get error_deleting_group => + 'Error while deleting group, please try again'; + + @override + String get error_editing_group => + 'Error while editing group, please try again'; + @override String get error_reading_file => 'Error reading file'; diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart deleted file mode 100644 index 8192c6b..0000000 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/player_selection.dart'; -import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; -import 'package:provider/provider.dart'; - -class CreateGroupView extends StatefulWidget { - const CreateGroupView({super.key}); - - @override - State createState() => _CreateGroupViewState(); -} - -class _CreateGroupViewState extends State { - late final AppDatabase db; - - /// Controller for the group name input field - final _groupNameController = TextEditingController(); - - /// List of currently selected players - List selectedPlayers = []; - - @override - void initState() { - super.initState(); - db = Provider.of(context, listen: false); - _groupNameController.addListener(() { - setState(() {}); - }); - } - - @override - void dispose() { - _groupNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return ScaffoldMessenger( - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: Text(loc.create_new_group)), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - margin: CustomTheme.standardMargin, - child: TextInputField( - controller: _groupNameController, - hintText: loc.group_name, - ), - ), - Expanded( - child: PlayerSelection( - onChanged: (value) { - setState(() { - selectedPlayers = [...value]; - }); - }, - ), - ), - CustomWidthButton( - text: loc.create_group, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.primary, - onPressed: - (_groupNameController.text.isEmpty || - (selectedPlayers.length < 2)) - ? null - : () async { - bool success = await db.groupDao.addGroup( - group: Group( - name: _groupNameController.text.trim(), - members: selectedPlayers, - ), - ); - if (!context.mounted) return; - if (success) { - Navigator.pop(context); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: CustomTheme.boxColor, - content: Center( - child: Text( - AppLocalizations.of( - context, - ).error_creating_group, - style: const TextStyle(color: Colors.white), - ), - ), - ), - ); - } - }, - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart new file mode 100644 index 0000000..9253820 --- /dev/null +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -0,0 +1,181 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/player_selection.dart'; +import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; +import 'package:provider/provider.dart'; + +class CreateGroupView extends StatefulWidget { + const CreateGroupView({super.key, this.groupToEdit}); + + /// The group to edit, if any + final Group? groupToEdit; + + @override + State createState() => _CreateGroupViewState(); +} + +class _CreateGroupViewState extends State { + late final AppDatabase db; + + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + /// Controller for the group name input field + final _groupNameController = TextEditingController(); + + /// List of currently selected players + List selectedPlayers = []; + + /// List of initially selected players (when editing a group) + List initialSelectedPlayers = []; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + if(widget.groupToEdit != null) { + _groupNameController.text = widget.groupToEdit!.name; + setState(() { + initialSelectedPlayers = widget.groupToEdit!.members; + selectedPlayers = widget.groupToEdit!.members; + }); + } + _groupNameController.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _groupNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { + if(widget.groupToEdit != null) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(loc.delete_group), + content: Text(loc.this_cannot_be_undone), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(loc.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(loc.delete), + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + showSnackbar(message: loc.error_deleting_group); + } + } + }); + } + },)],), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + margin: CustomTheme.standardMargin, + child: TextInputField( + controller: _groupNameController, + hintText: loc.group_name, + ), + ), + Expanded( + child: PlayerSelection( + initialSelectedPlayers: initialSelectedPlayers, + onChanged: (value) { + setState(() { + selectedPlayers = [...value]; + }); + }, + ), + ), + CustomWidthButton( + text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.primary, + onPressed: + (_groupNameController.text.isEmpty || + (selectedPlayers.length < 2)) + ? null + : () async { + late bool success; + if (widget.groupToEdit == null) { + success = await db.groupDao.addGroup( + group: Group( + name: _groupNameController.text.trim(), + members: selectedPlayers, + ), + ); + } else { + //TODO: Implement group editing in database + /* + success = await db.groupDao.updateGroup( + group: Group( + id: widget.groupToEdit!.id, + name: _groupNameController.text.trim(), + members: selectedPlayers, + ), + ); + */ + success = false; + }; + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); + } + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({ + required String message, + }) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } +} diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 239aa23..974b5b8 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -6,7 +6,7 @@ import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/group_detail_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; @@ -73,7 +73,19 @@ class _GroupsViewState extends State { height: MediaQuery.paddingOf(context).bottom - 20, ); } - return GroupTile(group: groups[index]); + return GroupTile(group: groups[index], onTap: () async { + await Navigator.push( + context, + adaptivePageRoute( + builder: (context) { + return CreateGroupView(groupToEdit: groups[index]); + }, + ), + ); + setState(() { + loadGroups(); + }); + }); }, ), ), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index b9885a4..cad3e9c 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -70,6 +70,9 @@ class _CreateMatchViewState extends State { /// List of available rulesets with their localized string representations late final List<(Ruleset, String)> _rulesets; + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + @override void initState() { super.initState(); @@ -120,6 +123,7 @@ class _CreateMatchViewState extends State { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return ScaffoldMessenger( + key: _scaffoldMessengerKey, child: Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_new_match)), diff --git a/lib/presentation/views/main_menu/settings_view.dart b/lib/presentation/views/main_menu/settings_view.dart index c5fcfa2..897ef5f 100644 --- a/lib/presentation/views/main_menu/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view.dart @@ -13,6 +13,10 @@ class SettingsView extends StatefulWidget { } class _SettingsViewState extends State { + + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + @override void initState() { super.initState(); @@ -22,6 +26,7 @@ class _SettingsViewState extends State { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return ScaffoldMessenger( + key: _scaffoldMessengerKey, child: Scaffold( appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), backgroundColor: CustomTheme.backgroundColor, diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 9280ae0..e910509 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -70,6 +70,7 @@ class _PlayerSelectionState extends State { super.initState(); db = Provider.of(context, listen: false); suggestedPlayers = skeletonData; + selectedPlayers = widget.initialSelectedPlayers ?? []; loadPlayerList(); } @@ -99,7 +100,7 @@ class _PlayerSelectionState extends State { if (value.isEmpty) { // If the search is empty, it shows all unselected players. suggestedPlayers = allPlayers.where((player) { - return !selectedPlayers.contains(player); + return !selectedPlayers.any((p) => p.id == player.id); }).toList(); } else { // If there is input, it filters by name match (case-insensitive) and ensures @@ -108,9 +109,7 @@ class _PlayerSelectionState extends State { final bool nameMatches = player.name.toLowerCase().contains( value.toLowerCase(), ); - final bool isNotSelected = !selectedPlayers.contains( - player, - ); + final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id); return nameMatches && isNotSelected; }).toList(); } @@ -125,46 +124,49 @@ class _PlayerSelectionState extends State { const SizedBox(height: 10), SizedBox( height: 50, - child: selectedPlayers.isEmpty - ? Center(child: Text(loc.no_players_selected)) - : SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: [ - for (var player in selectedPlayers) - Padding( - padding: const EdgeInsets.only(right: 8.0), - child: TextIconTile( - text: player.name, - onIconTap: () { - setState(() { - // Removes the player from the selection and notifies the parent. - selectedPlayers.remove(player); - widget.onChanged([...selectedPlayers]); + child: AppSkeleton( + enabled: isLoading, + child: selectedPlayers.isEmpty + ? Center(child: Text(loc.no_players_selected)) + : SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (var player in selectedPlayers) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: TextIconTile( + text: player.name, + onIconTap: () { + setState(() { + // Removes the player from the selection and notifies the parent. + selectedPlayers.remove(player); + widget.onChanged([...selectedPlayers]); - // Get the current search query - final currentSearch = _searchBarController - .text - .toLowerCase(); + // Get the current search query + final currentSearch = _searchBarController + .text + .toLowerCase(); - // If the player matches the current search query (or search is empty), - // they are added back to the `suggestedPlayers` and the list is re-sorted. - if (currentSearch.isEmpty || - player.name.toLowerCase().contains( - currentSearch, - )) { - suggestedPlayers.add(player); - suggestedPlayers.sort( - (a, b) => a.name.compareTo(b.name), - ); - } - }); - }, + // If the player matches the current search query (or search is empty), + // they are added back to the `suggestedPlayers` and the list is re-sorted. + if (currentSearch.isEmpty || + player.name.toLowerCase().contains( + currentSearch, + )) { + suggestedPlayers.add(player); + suggestedPlayers.sort( + (a, b) => a.name.compareTo(b.name), + ); + } + }); + }, + ), ), - ), - ], + ], + ), ), - ), + ), ), const SizedBox(height: 10), Text( @@ -243,7 +245,21 @@ class _PlayerSelectionState extends State { // Otherwise, use the loaded players from the database. loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); allPlayers = [...loadedPlayers]; - suggestedPlayers = [...loadedPlayers]; + if (widget.initialSelectedPlayers != null) { + // Excludes already selected players from the suggested players list. + suggestedPlayers = loadedPlayers.where((p) => !widget.initialSelectedPlayers!.any((ip) => ip.id == p.id)).toList(); + // Ensures that only players available for selection are pre-selected. + selectedPlayers = widget.initialSelectedPlayers! + .where( + (p) => allPlayers.any( + (available) => available.id == p.id, + ), + ) + .toList(); + } else { + // If no initial selection, all loaded players are suggested. + suggestedPlayers = [...loadedPlayers]; + } } isLoading = false; }); diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index 64d9caa..eb1d4ab 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -6,8 +6,9 @@ import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; /// A tile widget that displays information about a group, including its name and members. /// - [group]: The group data to be displayed. /// - [isHighlighted]: Whether the tile should be highlighted. +/// - [onTap]: An optional callback function to handle tap events. class GroupTile extends StatelessWidget { - const GroupTile({super.key, required this.group, this.isHighlighted = false}); + const GroupTile({super.key, required this.group, this.isHighlighted = false, this.onTap}); /// The group data to be displayed. final Group group; @@ -15,61 +16,67 @@ class GroupTile extends StatelessWidget { /// Whether the tile should be highlighted. final bool isHighlighted; + /// Callback function to handle tap events. + final VoidCallback? onTap; + @override Widget build(BuildContext context) { - return AnimatedContainer( - margin: CustomTheme.standardMargin, - padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - decoration: isHighlighted - ? CustomTheme.highlightedBoxDecoration - : CustomTheme.standardBoxDecoration, - duration: const Duration(milliseconds: 150), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - group.name, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - ), - ), - ), - Row( - children: [ - Text( - '${group.members.length}', + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + margin: CustomTheme.standardMargin, + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: isHighlighted + ? CustomTheme.highlightedBoxDecoration + : CustomTheme.standardBoxDecoration, + duration: const Duration(milliseconds: 150), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + group.name, + overflow: TextOverflow.ellipsis, style: const TextStyle( - fontWeight: FontWeight.w900, + fontWeight: FontWeight.bold, fontSize: 18, ), ), - const SizedBox(width: 3), - const Icon(Icons.group, size: 22), - ], - ), - ], - ), - const SizedBox(height: 5), - Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12.0, - runSpacing: 8.0, - children: [ - for (var member in [ - ...group.members, - ]..sort((a, b) => a.name.compareTo(b.name))) - TextIconTile(text: member.name, iconEnabled: false), - ], - ), - const SizedBox(height: 2.5), - ], + ), + Row( + children: [ + Text( + '${group.members.length}', + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 18, + ), + ), + const SizedBox(width: 3), + const Icon(Icons.group, size: 22), + ], + ), + ], + ), + const SizedBox(height: 5), + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12.0, + runSpacing: 8.0, + children: [ + for (var member in [ + ...group.members, + ]..sort((a, b) => a.name.compareTo(b.name))) + TextIconTile(text: member.name, iconEnabled: false), + ], + ), + const SizedBox(height: 2.5), + ], + ), ), ); } From b6dd0541aeec9fea0ed160f512fdb7b0fc2202f8 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 10 Jan 2026 22:11:28 +0100 Subject: [PATCH 02/54] rename CreateGroupView to GroupDetailView for clarity and consistency --- .../views/main_menu/group_view/group_detail_view.dart | 10 +++++----- .../views/main_menu/group_view/groups_view.dart | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 9253820..966add0 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -10,17 +10,17 @@ import 'package:game_tracker/presentation/widgets/player_selection.dart'; import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; import 'package:provider/provider.dart'; -class CreateGroupView extends StatefulWidget { - const CreateGroupView({super.key, this.groupToEdit}); +class GroupDetailView extends StatefulWidget { + const GroupDetailView({super.key, this.groupToEdit}); /// The group to edit, if any final Group? groupToEdit; @override - State createState() => _CreateGroupViewState(); + State createState() => _GroupDetailViewState(); } -class _CreateGroupViewState extends State { +class _GroupDetailViewState extends State { late final AppDatabase db; /// GlobalKey for ScaffoldMessenger to show snackbars @@ -145,7 +145,7 @@ class _CreateGroupViewState extends State { ); */ success = false; - }; + } if (!context.mounted) return; if (success) { Navigator.pop(context); diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 974b5b8..e85fcda 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -78,7 +78,7 @@ class _GroupsViewState extends State { context, adaptivePageRoute( builder: (context) { - return CreateGroupView(groupToEdit: groups[index]); + return GroupDetailView(groupToEdit: groups[index]); }, ), ); @@ -100,7 +100,7 @@ class _GroupsViewState extends State { context, adaptivePageRoute( builder: (context) { - return const CreateGroupView(); + return const GroupDetailView(); }, ), ); From ee84c60ba6b359b8e87111cdfa542f5df7f1ac16 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 10 Jan 2026 22:13:26 +0100 Subject: [PATCH 03/54] remove questionmark --- lib/l10n/arb/app_de.arb | 2 +- lib/l10n/arb/app_en.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 938fbbd..51faa1e 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -18,7 +18,7 @@ "days_ago": "vor {count} Tagen", "delete": "Löschen", "delete_all_data": "Alle Daten löschen?", - "delete_all_data": "Diese Gruppe löschen?", + "delete_all_data": "Diese Gruppe löschen", "edit_group": "Gruppe bearbeiten", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 487b73a..cee5ba8 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -290,7 +290,7 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data?", - "delete_group": "Delete this group?", + "delete_group": "Delete this group", "edit_group": "Edit Group", "error_creating_group": "Error while creating group, please try again", "error_deleting_group": "Error while deleting group, please try again", From caf60d046b2dfece1464057978a25b88fd29dc87 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 10 Jan 2026 22:20:14 +0100 Subject: [PATCH 04/54] fix merge mistake --- lib/l10n/arb/app_en.arb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 29dd4b0..122d238 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -34,10 +34,10 @@ "description": "Button text to create a match" }, "@create_new_group": { - "description": "Button text to create a new group" + "description": "Appbar text to create a new group" }, "@create_new_match": { - "description": "Button text to create a new match" + "description": "Appbar text to create a new match" }, "@data_successfully_deleted": { "description": "Success message after deleting data" From b1e9bb3aeb2f7eb8ecd091ffcf2bc52bde80c767 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Tue, 13 Jan 2026 21:01:31 +0100 Subject: [PATCH 05/54] update localization comments for clarity in group and match creation --- lib/l10n/generated/app_localizations.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 0fd55db..1ef86f5 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -164,13 +164,13 @@ abstract class AppLocalizations { /// **'Create match'** String get create_match; - /// Button text to create a new group + /// Appbar text to create a new group /// /// In en, this message translates to: /// **'Create new group'** String get create_new_group; - /// Button text to create a new match + /// Appbar text to create a new match /// /// In en, this message translates to: /// **'Create new match'** From ed642e3d4f12a481b8858c5e17adf9719b928497 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Tue, 13 Jan 2026 21:07:23 +0100 Subject: [PATCH 06/54] merge dev into #118 --- .../views/main_menu/settings_view/settings_view.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index 1843c90..c41c7d0 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -21,12 +21,17 @@ class SettingsView extends StatefulWidget { } class _SettingsViewState extends State { + + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + PackageInfo _packageInfo = PackageInfo( appName: 'n.A.', packageName: 'n.A.', version: 'n.A.', buildNumber: 'n.A.', ); + @override void initState() { super.initState(); @@ -37,6 +42,7 @@ class _SettingsViewState extends State { Widget build(BuildContext context) { final loc = AppLocalizations.of(context); return ScaffoldMessenger( + key: _scaffoldMessengerKey, child: Scaffold( appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), backgroundColor: CustomTheme.backgroundColor, From 1b297d15b0b73e5eedd5b28dd537d385dc253fef Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Tue, 13 Jan 2026 21:35:10 +0100 Subject: [PATCH 07/54] fix snackbar showing also showing on other screens (#155) --- .../group_view/group_detail_view.dart | 3 +- .../settings_view/settings_view.dart | 37 ++++++++----------- 2 files changed, 17 insertions(+), 23 deletions(-) diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 966add0..e338522 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -83,12 +83,13 @@ class _GroupDetailViewState extends State { ], ), ).then((confirmed) async { - if (confirmed == true && context.mounted) { + if (confirmed == true && mounted) { bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); if (!context.mounted) return; if (success) { Navigator.pop(context); } else { + if (!mounted) return; showSnackbar(message: loc.error_deleting_group); } } diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index c41c7d0..9a3d33c 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -22,7 +22,7 @@ class SettingsView extends StatefulWidget { class _SettingsViewState extends State { - /// GlobalKey for ScaffoldMessenger to show snackbars + /// GlobalKey for ScaffoldMessenger to show snackbars only on this screen final _scaffoldMessengerKey = GlobalKey(); PackageInfo _packageInfo = PackageInfo( @@ -84,8 +84,8 @@ class _SettingsViewState extends State { json, 'game_tracker-data', ); - if (!context.mounted) return; - showExportSnackBar(context: context, result: result); + if (!mounted) return; + showExportSnackBar(result: result); }, ), SettingsListTile( @@ -94,8 +94,8 @@ class _SettingsViewState extends State { suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), onPressed: () async { final result = await DataTransferService.importData(context); - if (!context.mounted) return; - showImportSnackBar(context: context, result: result); + if (!mounted) return; + showImportSnackBar(result: result); }, ), SettingsListTile( @@ -123,7 +123,6 @@ class _SettingsViewState extends State { if (confirmed == true && context.mounted) { DataTransferService.deleteAllData(context); showSnackbar( - context: context, message: AppLocalizations.of( context, ).data_successfully_deleted, @@ -237,62 +236,56 @@ class _SettingsViewState extends State { /// Displays a snackbar based on the import result. /// - /// [context] The BuildContext to show the snackbar in. /// [result] The result of the import operation. void showImportSnackBar({ - required BuildContext context, required ImportResult result, }) { final loc = AppLocalizations.of(context); switch (result) { case ImportResult.success: - showSnackbar(context: context, message: loc.data_successfully_imported); + showSnackbar(message: loc.data_successfully_imported); case ImportResult.invalidSchema: - showSnackbar(context: context, message: loc.invalid_schema); + showSnackbar(message: loc.invalid_schema); case ImportResult.fileReadError: - showSnackbar(context: context, message: loc.error_reading_file); + showSnackbar(message: loc.error_reading_file); case ImportResult.canceled: - showSnackbar(context: context, message: loc.import_canceled); + showSnackbar(message: loc.import_canceled); case ImportResult.formatException: - showSnackbar(context: context, message: loc.format_exception); + showSnackbar(message: loc.format_exception); case ImportResult.unknownException: - showSnackbar(context: context, message: loc.unknown_exception); + showSnackbar(message: loc.unknown_exception); } } /// Displays a snackbar based on the export result. /// - /// [context] The BuildContext to show the snackbar in. /// [result] The result of the export operation. void showExportSnackBar({ - required BuildContext context, required ExportResult result, }) { final loc = AppLocalizations.of(context); switch (result) { case ExportResult.success: - showSnackbar(context: context, message: loc.data_successfully_exported); + showSnackbar(message: loc.data_successfully_exported); case ExportResult.canceled: - showSnackbar(context: context, message: loc.export_canceled); + showSnackbar(message: loc.export_canceled); case ExportResult.unknownException: - showSnackbar(context: context, message: loc.unknown_exception); + showSnackbar(message: loc.unknown_exception); } } /// Displays a snackbar with the given message and optional action. /// - /// [context] The BuildContext to show the snackbar in. /// [message] The message to display in the snackbar. /// [duration] The duration for which the snackbar is displayed. /// [action] An optional callback function to execute when the action button is pressed. void showSnackbar({ - required BuildContext context, required String message, Duration duration = const Duration(seconds: 3), VoidCallback? action, }) { final loc = AppLocalizations.of(context); - final messenger = ScaffoldMessenger.of(context); + final messenger = _scaffoldMessengerKey.currentState!; messenger.hideCurrentSnackBar(); messenger.showSnackBar( SnackBar( From 016c1ceb6e692333350ee6c80df1254c9b211214 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Tue, 13 Jan 2026 21:38:53 +0100 Subject: [PATCH 08/54] add context to mounted check --- .../views/main_menu/group_view/group_detail_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index e338522..e372726 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -83,7 +83,7 @@ class _GroupDetailViewState extends State { ], ), ).then((confirmed) async { - if (confirmed == true && mounted) { + if (confirmed == true && context.mounted) { bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); if (!context.mounted) return; if (success) { From 919a38afe5f63d5dcb36a5f48106fe0af587e04d Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 17 Jan 2026 10:21:55 +0100 Subject: [PATCH 09/54] fix merge conflicts --- .../views/main_menu/settings_view/settings_view.dart | 6 +++--- pubspec.yaml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/settings_view/settings_view.dart b/lib/presentation/views/main_menu/settings_view/settings_view.dart index 22b3ce9..78c7efc 100644 --- a/lib/presentation/views/main_menu/settings_view/settings_view.dart +++ b/lib/presentation/views/main_menu/settings_view/settings_view.dart @@ -304,11 +304,11 @@ class _SettingsViewState extends State { final loc = AppLocalizations.of(context); switch (result) { case ExportResult.success: - showSnackbar(message: loc.data_successfully_exported); + showSnackbar(context: context, message: loc.data_successfully_exported); case ExportResult.canceled: - showSnackbar(message: loc.export_canceled); + showSnackbar(context: context, message: loc.export_canceled); case ExportResult.unknownException: - showSnackbar(message: loc.unknown_exception); + showSnackbar(context: context, message: loc.unknown_exception); } } diff --git a/pubspec.yaml b/pubspec.yaml index e9fd894..a6eaea1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: game_tracker description: "Game Tracking App for Card Games" publish_to: 'none' -version: 0.0.7+212 +version: 0.0.7+214 environment: sdk: ^3.8.1 From 374c9295ef0c584c5f40ac8da762937a501323fb Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:08:39 +0100 Subject: [PATCH 10/54] Implemented MatchProfileView --- lib/l10n/arb/app_de.arb | 5 + lib/l10n/arb/app_en.arb | 16 ++ lib/l10n/generated/app_localizations.dart | 24 ++ lib/l10n/generated/app_localizations_de.dart | 12 + lib/l10n/generated/app_localizations_en.dart | 12 + .../match_view/match_profile_view.dart | 261 ++++++++++++++++++ .../main_menu/match_view/match_view.dart | 8 +- .../widgets/buttons/main_menu_button.dart | 28 +- pubspec.yaml | 2 +- 9 files changed, 349 insertions(+), 19 deletions(-) create mode 100644 lib/presentation/views/main_menu/match_view/match_profile_view.dart diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 2ef9ee9..65aa2b3 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -23,7 +23,9 @@ "delete": "Löschen", "delete_all_data": "Alle Daten löschen", "delete_group": "Gruppe löschen", + "delete_match": "Spiel löschen", "edit_group": "Gruppe bearbeiten", + "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_reading_file": "Fehler beim Lesen der Datei", "export_canceled": "Export abgebrochen", @@ -46,6 +48,7 @@ "licenses": "Lizenzen", "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", + "match_profile": "Spielprofil", "matches": "Spiele", "members": "Mitglieder", "most_points": "Höchste Punkte", @@ -70,6 +73,8 @@ "privacy_policy": "Datenschutzerklärung", "quick_create": "Schnellzugriff", "recent_matches": "Letzte Spiele", + "result": "Ergebnis", + "results": "Ergebnisse", "ruleset": "Regelwerk", "ruleset_least_points": "Umgekehrte Wertung: Der/die Spieler:in mit den wenigsten Punkten gewinnt.", "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index fa4adc8..cc07a45 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -74,9 +74,15 @@ "@delete_group": { "description": "Button text to delete a group" }, + "@delete_match": { + "description": "Button text to delete a match" + }, "@edit_group": { "description": "Button text to edit a group" }, + "@enter_results": { + "description": "Button text to enter match results" + }, "@error_creating_group": { "description": "Error message when group creation fails" }, @@ -143,6 +149,9 @@ "@match_name": { "description": "Placeholder for match name input" }, + "@match_profile": { + "description": "Title for match profile view" + }, "@matches": { "description": "Label for matches" }, @@ -220,6 +229,9 @@ "@recent_matches": { "description": "Title for recent matches section" }, + "@results": { + "description": "Label for match results" + }, "@ruleset": { "description": "Ruleset label" }, @@ -321,7 +333,9 @@ "delete": "Delete", "delete_all_data": "Delete all data", "delete_group": "Delete Group", + "delete_match": "Delete Match", "edit_group": "Edit Group", + "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", "error_reading_file": "Error reading file", "export_canceled": "Export canceled", @@ -344,6 +358,7 @@ "licenses": "Licenses", "match_in_progress": "Match in progress...", "match_name": "Match name", + "match_profile": "Match Profile", "matches": "Matches", "members": "Members", "most_points": "Most Points", @@ -368,6 +383,7 @@ "privacy_policy": "Privacy Policy", "quick_create": "Quick Create", "recent_matches": "Recent Matches", + "results": "Results", "ruleset": "Ruleset", "ruleset_least_points": "Inverse scoring: the player with the fewest points wins.", "ruleset_most_points": "Traditional ruleset: the player with the most points wins.", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 57dbdd8..627d4b1 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -236,12 +236,24 @@ abstract class AppLocalizations { /// **'Delete Group'** String get delete_group; + /// Button text to delete a match + /// + /// In en, this message translates to: + /// **'Delete Match'** + String get delete_match; + /// Button text to edit a group /// /// In en, this message translates to: /// **'Edit Group'** String get edit_group; + /// Button text to enter match results + /// + /// In en, this message translates to: + /// **'Enter Results'** + String get enter_results; + /// Error message when group creation fails /// /// In en, this message translates to: @@ -374,6 +386,12 @@ abstract class AppLocalizations { /// **'Match name'** String get match_name; + /// Title for match profile view + /// + /// In en, this message translates to: + /// **'Match Profile'** + String get match_profile; + /// Label for matches /// /// In en, this message translates to: @@ -518,6 +536,12 @@ abstract class AppLocalizations { /// **'Recent Matches'** String get recent_matches; + /// Label for match results + /// + /// In en, this message translates to: + /// **'Results'** + String get results; + /// Ruleset label /// /// 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 f78f9f4..3078855 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -81,9 +81,15 @@ class AppLocalizationsDe extends AppLocalizations { @override String get delete_group => 'Gruppe löschen'; + @override + String get delete_match => 'Spiel löschen'; + @override String get edit_group => 'Gruppe bearbeiten'; + @override + String get enter_results => 'Ergebnisse eintragen'; + @override String get error_creating_group => 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; @@ -151,6 +157,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get match_name => 'Spieltitel'; + @override + String get match_profile => 'Spielprofil'; + @override String get matches => 'Spiele'; @@ -226,6 +235,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get recent_matches => 'Letzte Spiele'; + @override + String get results => 'Ergebnisse'; + @override String get ruleset => 'Regelwerk'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 32512c7..12d8a36 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -81,9 +81,15 @@ class AppLocalizationsEn extends AppLocalizations { @override String get delete_group => 'Delete Group'; + @override + String get delete_match => 'Delete Match'; + @override String get edit_group => 'Edit Group'; + @override + String get enter_results => 'Enter Results'; + @override String get error_creating_group => 'Error while creating group, please try again'; @@ -151,6 +157,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get match_name => 'Match name'; + @override + String get match_profile => 'Match Profile'; + @override String get matches => 'Matches'; @@ -226,6 +235,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get recent_matches => 'Recent Matches'; + @override + String get results => 'Results'; + @override String get ruleset => 'Ruleset'; diff --git a/lib/presentation/views/main_menu/match_view/match_profile_view.dart b/lib/presentation/views/main_menu/match_view/match_profile_view.dart new file mode 100644 index 0000000..7c18f11 --- /dev/null +++ b/lib/presentation/views/main_menu/match_view/match_profile_view.dart @@ -0,0 +1,261 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/adaptive_page_route.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/match.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart'; +import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; +import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart'; +import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; +import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class MatchProfileView extends StatefulWidget { + /// A view that displays the profile of a match + /// - [match]: The match to display + /// - [callback]: Callback to refresh the match list + const MatchProfileView({ + super.key, + required this.match, + required this.callback, + }); + + /// The match to display + final Match match; + + /// Callback to refresh the match list + final VoidCallback callback; + + @override + State createState() => _MatchProfileViewState(); +} + +class _MatchProfileViewState extends State { + late final AppDatabase db; + + /// All players who participated in the match + late final List allPlayers; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + allPlayers = _getAllPlayers(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final extraPlayersCount = + (widget.match.players?.length ?? 0) + + (widget.match.group?.members.length ?? 0) - + allPlayers.length; + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + title: Text(loc.match_profile), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: '${loc.delete_match}?', + content: loc.this_cannot_be_undone, + actions: [ + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + loc.cancel, + style: const TextStyle(color: CustomTheme.textColor), + ), + ), + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + loc.delete, + style: TextStyle(color: CustomTheme.secondaryColor), + ), + ), + ], + ), + ).then((confirmed) async { + if (confirmed! && context.mounted) { + await db.matchDao.deleteMatch(matchId: widget.match.id); + if (!context.mounted) return; + Navigator.pop(context); + widget.callback.call(); + } + }); + }, + ), + ], + ), + body: SafeArea( + child: Stack( + alignment: Alignment.center, + children: [ + ListView( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 20, + bottom: 100, + ), + children: [ + const Center( + child: ColoredIconContainer( + icon: Icons.sports_esports, + containerSize: 55, + iconSize: 38, + ), + ), + const SizedBox(height: 10), + Text( + widget.match.name, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + Text( + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.match.createdAt)}', + style: const TextStyle( + fontSize: 12, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + if (widget.match.group != null) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.group), + const SizedBox(width: 8), + Text( + '${widget.match.group!.name} ${extraPlayersCount > 0 ? '+ $extraPlayersCount' : ''}', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ], + ), + const SizedBox(height: 20), + ], + InfoTile( + title: loc.players, + icon: Icons.people, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: allPlayers.map((player) { + return TextIconTile( + text: player.name, + iconEnabled: false, + ); + }).toList(), + ), + ), + const SizedBox(height: 15), + InfoTile( + title: loc.results, + icon: Icons.emoji_events, + content: Padding( + padding: const EdgeInsets.symmetric( + vertical: 4, + horizontal: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + loc.winner, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + Text( + widget.match.winner?.name ?? '-', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: widget.match.winner != null + ? CustomTheme.primaryColor + : CustomTheme.textColor, + ), + ), + ], + ), + ), + ), + ], + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom, + child: Row( + children: [ + MainMenuButton(icon: Icons.edit, onPressed: () {}), + const SizedBox(width: 15), + MainMenuButton( + text: loc.enter_results, + icon: Icons.note_add, + onPressed: () async { + await Navigator.push( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => MatchResultView( + match: widget.match, + onWinnerChanged: () { + widget.callback.call(); + setState(() {}); + }, + ), + ), + ); + }, + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// Gets all players who participated in the match (from group and individual players) + List _getAllPlayers() { + final List players = []; + + // Add group members if group exists + if (widget.match.group != null) { + players.addAll(widget.match.group!.members); + } + + // Add individual players + if (widget.match.players != null) { + for (var player in widget.match.players!) { + // Avoid duplicates + if (!players.any((p) => p.id == player.id)) { + players.add(player); + } + } + } + + return players; + } +} diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index e85bf77..65ff29c 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -11,7 +11,7 @@ import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/match_profile_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; @@ -89,10 +89,9 @@ class _MatchViewState extends State { Navigator.push( context, adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => MatchResultView( + builder: (context) => MatchProfileView( match: matches[index], - onWinnerChanged: loadGames, + callback: loadGames, ), ), ); @@ -128,6 +127,7 @@ class _MatchViewState extends State { /// Loads the games from the database and sorts them by creation date. void loadGames() { + isLoading = true; Future.wait([ db.matchDao.getAllMatches(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index 747c31e..417a296 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -7,16 +7,16 @@ class MainMenuButton extends StatefulWidget { /// - [onPressed]: The callback to be invoked when the button is pressed. const MainMenuButton({ super.key, - required this.text, - this.icon, + required this.icon, required this.onPressed, + this.text, }); /// The text of the button. - final String text; + final String? text; /// The icon of the button. - final IconData? icon; + final IconData icon; /// The callback to be invoked when the button is pressed. final void Function() onPressed; @@ -71,18 +71,18 @@ class _MainMenuButtonState extends State mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (widget.icon != null) ...[ - Icon(widget.icon, size: 26, color: Colors.black), + Icon(widget.icon, size: 26, color: Colors.black), + if (widget.text != null) ...[ const SizedBox(width: 7), - ], - Text( - widget.text, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Colors.black, + Text( + widget.text!, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), ), - ), + ], ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 33091e5..54ff72b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: game_tracker description: "Game Tracking App for Card Games" publish_to: 'none' -version: 0.0.9+236 +version: 0.0.9+242 environment: sdk: ^3.8.1 From 8fe01c332e3cbe1b19c0bb36c23f4ef6959e3c57 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:10:31 +0100 Subject: [PATCH 11/54] Added comment --- .../views/main_menu/group_view/group_profile_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/presentation/views/main_menu/group_view/group_profile_view.dart b/lib/presentation/views/main_menu/group_view/group_profile_view.dart index e366834..7c4102c 100644 --- a/lib/presentation/views/main_menu/group_view/group_profile_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_profile_view.dart @@ -40,6 +40,7 @@ class _GroupProfileViewState extends State { /// Total matches played in this group int totalMatches = 0; + /// The best player in this group String bestPlayer = ''; @override From 7faf80de0326758d7d09cf7aa9a00f16fc35cdd8 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:14:45 +0100 Subject: [PATCH 12/54] Updated extra player count --- .../views/main_menu/match_view/match_profile_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/match_profile_view.dart b/lib/presentation/views/main_menu/match_view/match_profile_view.dart index 7c18f11..11f7bd9 100644 --- a/lib/presentation/views/main_menu/match_view/match_profile_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_profile_view.dart @@ -144,7 +144,7 @@ class _MatchProfileViewState extends State { const Icon(Icons.group), const SizedBox(width: 8), Text( - '${widget.match.group!.name} ${extraPlayersCount > 0 ? '+ $extraPlayersCount' : ''}', + '${widget.match.group!.name} ${widget.match.players != null ? '+ ${widget.match.players!.length}' : ''}', style: const TextStyle(fontWeight: FontWeight.bold), ), ], From eb114f285398700e8005d2aac96014791f8a09b5 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:36:15 +0100 Subject: [PATCH 13/54] Added empty winner tile --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 4 ++ lib/l10n/generated/app_localizations.dart | 6 +++ lib/l10n/generated/app_localizations_de.dart | 3 ++ lib/l10n/generated/app_localizations_en.dart | 3 ++ .../match_view/match_profile_view.dart | 46 +++++++++++-------- 6 files changed, 44 insertions(+), 19 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 65aa2b3..fa976b5 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -61,6 +61,7 @@ "no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden", "no_players_selected": "Keine Spieler:innen ausgewählt", "no_recent_matches_available": "Keine letzten Spiele verfügbar", + "no_results_entered_yet": "Noch keine Ergebnisse eingetragen", "no_second_match_available": "Kein zweites Spiel verfügbar", "no_statistics_available": "Keine Statistiken verfügbar", "none": "Kein", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index cc07a45..c5ea0fc 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -188,6 +188,9 @@ "@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" }, @@ -371,6 +374,7 @@ "no_players_found_with_that_name": "No players found with that name", "no_players_selected": "No players selected", "no_recent_matches_available": "No recent matches available", + "no_results_entered_yet": "No results entered yet", "no_second_match_available": "No second match available", "no_statistics_available": "No statistics available", "none": "None", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 627d4b1..ad62957 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -464,6 +464,12 @@ abstract class AppLocalizations { /// **'No recent matches available'** String get no_recent_matches_available; + /// Message when no results have been entered yet + /// + /// In en, this message translates to: + /// **'No results entered yet'** + String get no_results_entered_yet; + /// Message when no second match exists /// /// 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 3078855..d78f926 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -197,6 +197,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_recent_matches_available => 'Keine letzten Spiele verfügbar'; + @override + String get no_results_entered_yet => 'Noch keine Ergebnisse eingetragen'; + @override String get no_second_match_available => 'Kein zweites Spiel verfügbar'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 12d8a36..0dad111 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -197,6 +197,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_recent_matches_available => 'No recent matches available'; + @override + String get no_results_entered_yet => 'No results entered yet'; + @override String get no_second_match_available => 'No second match available'; diff --git a/lib/presentation/views/main_menu/match_view/match_profile_view.dart b/lib/presentation/views/main_menu/match_view/match_profile_view.dart index 11f7bd9..6c9241b 100644 --- a/lib/presentation/views/main_menu/match_view/match_profile_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_profile_view.dart @@ -51,10 +51,6 @@ class _MatchProfileViewState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - final extraPlayersCount = - (widget.match.players?.length ?? 0) + - (widget.match.group?.members.length ?? 0) - - allPlayers.length; return Scaffold( backgroundColor: CustomTheme.backgroundColor, @@ -144,6 +140,7 @@ class _MatchProfileViewState extends State { const Icon(Icons.group), const SizedBox(width: 8), Text( + // TODO: Update after DB changes '${widget.match.group!.name} ${widget.match.players != null ? '+ ${widget.match.players!.length}' : ''}', style: const TextStyle(fontWeight: FontWeight.bold), ), @@ -180,23 +177,34 @@ class _MatchProfileViewState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - loc.winner, - style: const TextStyle( - fontSize: 16, - color: CustomTheme.textColor, + /// TODO: Implement different ruleset results display + if (widget.match.winner != null) ...[ + Text( + loc.winner, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), ), - ), - Text( - widget.match.winner?.name ?? '-', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: widget.match.winner != null - ? CustomTheme.primaryColor - : CustomTheme.textColor, + Text( + widget.match.winner?.name ?? '-', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: widget.match.winner != null + ? CustomTheme.primaryColor + : CustomTheme.textColor, + ), ), - ), + ] else ...[ + Text( + loc.no_results_entered_yet, + style: const TextStyle( + fontSize: 14, + color: CustomTheme.textColor, + ), + ), + ], ], ), ), From 1450e9b9584fcf67da8f77aa42d811275666bf81 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:36:21 +0100 Subject: [PATCH 14/54] Updated attributes --- .../widgets/buttons/main_menu_button.dart | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart index 417a296..c583456 100644 --- a/lib/presentation/widgets/buttons/main_menu_button.dart +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -2,24 +2,24 @@ import 'package:flutter/material.dart'; class MainMenuButton extends StatefulWidget { /// A button for the main menu with an optional icon and a press animation. - /// - [text]: The text of the button. - /// - [icon]: The icon of the button. /// - [onPressed]: The callback to be invoked when the button is pressed. + /// - [icon]: The icon of the button. + /// - [text]: The text of the button. const MainMenuButton({ super.key, - required this.icon, required this.onPressed, + required this.icon, this.text, }); - /// The text of the button. - final String? text; + /// The callback to be invoked when the button is pressed. + final void Function() onPressed; /// The icon of the button. final IconData icon; - /// The callback to be invoked when the button is pressed. - final void Function() onPressed; + /// The text of the button. + final String? text; @override State createState() => _MainMenuButtonState(); From a56e738064ee990462e370317d9f68f5822f0648 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:42:18 +0100 Subject: [PATCH 15/54] Implemented winner update --- .../views/main_menu/match_view/match_profile_view.dart | 9 ++++++--- .../views/main_menu/match_view/match_result_view.dart | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_profile_view.dart b/lib/presentation/views/main_menu/match_view/match_profile_view.dart index 6c9241b..6558916 100644 --- a/lib/presentation/views/main_menu/match_view/match_profile_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_profile_view.dart @@ -38,6 +38,8 @@ class MatchProfileView extends StatefulWidget { class _MatchProfileViewState extends State { late final AppDatabase db; + late Player? currentWinner; + /// All players who participated in the match late final List allPlayers; @@ -46,6 +48,7 @@ class _MatchProfileViewState extends State { super.initState(); db = Provider.of(context, listen: false); allPlayers = _getAllPlayers(); + currentWinner = widget.match.winner; } @override @@ -178,7 +181,7 @@ class _MatchProfileViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ /// TODO: Implement different ruleset results display - if (widget.match.winner != null) ...[ + if (currentWinner != null) ...[ Text( loc.winner, style: const TextStyle( @@ -187,7 +190,7 @@ class _MatchProfileViewState extends State { ), ), Text( - widget.match.winner?.name ?? '-', + currentWinner!.name, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, @@ -221,7 +224,7 @@ class _MatchProfileViewState extends State { text: loc.enter_results, icon: Icons.note_add, onPressed: () async { - await Navigator.push( + currentWinner = await Navigator.push( context, adaptivePageRoute( fullscreenDialog: true, 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 1deb385..8f20344 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 @@ -54,7 +54,7 @@ class _MatchResultViewState extends State { icon: const Icon(Icons.close), onPressed: () { widget.onWinnerChanged?.call(); - Navigator.of(context).pop(); + Navigator.of(context).pop(_selectedPlayer); }, ), title: Text(widget.match.name), From 48d99a0386db6d292adeb8b3abf89bc7987c4610 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 00:46:50 +0100 Subject: [PATCH 16/54] Added match editing --- .../create_match/create_match_view.dart | 20 +++++++++++++++++-- .../match_view/match_profile_view.dart | 13 +++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index e106de7..a66178e 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -20,11 +20,14 @@ import 'package:provider/provider.dart'; class CreateMatchView extends StatefulWidget { /// A view that allows creating a new match /// [onWinnerChanged]: Optional callback invoked when the winner is changed - const CreateMatchView({super.key, this.onWinnerChanged}); + const CreateMatchView({super.key, this.onWinnerChanged, this.match}); /// Optional callback invoked when the winner is changed final VoidCallback? onWinnerChanged; + /// An optional match to prefill the fields + final Match? match; + @override State createState() => _CreateMatchViewState(); } @@ -83,6 +86,19 @@ class _CreateMatchViewState extends State { filteredPlayerList = List.from(playerList); }); }); + + if (widget.match != null) { + final match = widget.match!; + _matchNameController.text = match.name; + selectedGroup = match.group; + selectedGroupId = match.group?.id ?? ''; + selectedPlayers = match.players ?? []; + if (selectedGroup != null) { + filteredPlayerList = playerList + .where((p) => !selectedGroup!.members.any((m) => m.id == p.id)) + .toList(); + } + } } @override @@ -229,6 +245,6 @@ class _CreateMatchViewState extends State { /// - Either a group is selected OR at least 2 players are selected bool _enableCreateGameButton() { return (selectedGroup != null || - (selectedPlayers != null && selectedPlayers!.length > 1)); + (selectedPlayers != null && selectedPlayers!.length > 1)); } } diff --git a/lib/presentation/views/main_menu/match_view/match_profile_view.dart b/lib/presentation/views/main_menu/match_view/match_profile_view.dart index 6558916..d301f9e 100644 --- a/lib/presentation/views/main_menu/match_view/match_profile_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_profile_view.dart @@ -5,6 +5,7 @@ import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; @@ -218,7 +219,17 @@ class _MatchProfileViewState extends State { bottom: MediaQuery.paddingOf(context).bottom, child: Row( children: [ - MainMenuButton(icon: Icons.edit, onPressed: () {}), + MainMenuButton( + icon: Icons.edit, + onPressed: () => Navigator.push( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => + CreateMatchView(match: widget.match), + ), + ), + ), const SizedBox(width: 15), MainMenuButton( text: loc.enter_results, From 765610b184227e8860db35b210f57b3e63208fff Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 01:02:35 +0100 Subject: [PATCH 17/54] Enhanced editing --- lib/l10n/arb/app_de.arb | 1 + lib/l10n/arb/app_en.arb | 10 +++- lib/l10n/generated/app_localizations.dart | 6 ++ lib/l10n/generated/app_localizations_de.dart | 3 + lib/l10n/generated/app_localizations_en.dart | 3 + .../create_match/create_match_view.dart | 60 ++++++++++++------- 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index fa976b5..5a554c5 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -81,6 +81,7 @@ "ruleset_most_points": "Traditionelles Regelwerk: Der/die Spieler:in mit den meisten Punkten gewinnt.", "ruleset_single_loser": "Genau ein:e Verlierer:in wird bestimmt; der letzte Platz erhält die Strafe oder Konsequenz.", "ruleset_single_winner": "Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.", + "save_changes": "Änderungen speichern", "search_for_groups": "Nach Gruppen suchen", "search_for_players": "Nach Spieler:innen suchen", "select_winner": "Gewinner:in wählen:", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c5ea0fc..c818920 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -188,9 +188,9 @@ "@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_results_entered_yet": { + "description": "Message when no results have been entered yet" + }, "@no_second_match_available": { "description": "Message when no second match exists" }, @@ -250,6 +250,9 @@ "@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" }, @@ -393,6 +396,7 @@ "ruleset_most_points": "Traditional ruleset: the player with the most points wins.", "ruleset_single_loser": "Exactly one loser is determined; last place receives the penalty or consequence.", "ruleset_single_winner": "Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.", + "save_changes": "Save Changes", "search_for_groups": "Search for groups", "search_for_players": "Search for players", "select_winner": "Select Winner:", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index ad62957..49c2e20 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -578,6 +578,12 @@ abstract class AppLocalizations { /// **'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'** String get ruleset_single_winner; + /// Save changes button text + /// + /// In en, this message translates to: + /// **'Save Changes'** + String get save_changes; + /// Hint text for group search input field /// /// 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 d78f926..8259aa5 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -260,6 +260,9 @@ class AppLocalizationsDe extends AppLocalizations { String get ruleset_single_winner => 'Genau ein:e Gewinner:in wird gewählt; Unentschieden werden durch einen vordefinierten Tie-Breaker aufgelöst.'; + @override + String get save_changes => 'Änderungen speichern'; + @override String get search_for_groups => 'Nach Gruppen suchen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 0dad111..b66b293 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -260,6 +260,9 @@ class AppLocalizationsEn extends AppLocalizations { String get ruleset_single_winner => 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.'; + @override + String get save_changes => 'Save Changes'; + @override String get search_for_groups => 'Search for groups'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index a66178e..fa685c3 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -87,6 +87,7 @@ class _CreateMatchViewState extends State { }); }); + // If a match is provided, prefill the fields if (widget.match != null) { final match = widget.match!; _matchNameController.text = match.name; @@ -122,6 +123,10 @@ class _CreateMatchViewState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); + final buttonText = widget.match != null + ? loc.save_changes + : loc.create_match; + return ScaffoldMessenger( child: Scaffold( backgroundColor: CustomTheme.backgroundColor, @@ -202,32 +207,12 @@ class _CreateMatchViewState extends State { ), ), CustomWidthButton( - text: loc.create_match, + text: buttonText, sizeRelativeToWidth: 0.95, buttonType: ButtonType.primary, onPressed: _enableCreateGameButton() - ? () async { - Match match = Match( - name: _matchNameController.text.isEmpty - ? (hintText ?? '') - : _matchNameController.text.trim(), - createdAt: DateTime.now(), - group: selectedGroup, - players: selectedPlayers, - ); - await db.matchDao.addMatch(match: match); - if (context.mounted) { - Navigator.pushReplacement( - context, - adaptivePageRoute( - fullscreenDialog: true, - builder: (context) => MatchResultView( - match: match, - onWinnerChanged: widget.onWinnerChanged, - ), - ), - ); - } + ? () { + buttonNavigation(context); } : null, ), @@ -247,4 +232,33 @@ class _CreateMatchViewState extends State { return (selectedGroup != null || (selectedPlayers != null && selectedPlayers!.length > 1)); } + + void buttonNavigation(BuildContext context) async { + if (widget.match != null) { + // TODO: Implement updating match logic here + Navigator.pop(context); + } else { + Match match = Match( + name: _matchNameController.text.isEmpty + ? (hintText ?? '') + : _matchNameController.text.trim(), + createdAt: DateTime.now(), + group: selectedGroup, + players: selectedPlayers, + ); + await db.matchDao.addMatch(match: match); + if (context.mounted) { + Navigator.pushReplacement( + context, + adaptivePageRoute( + fullscreenDialog: true, + builder: (context) => MatchResultView( + match: match, + onWinnerChanged: widget.onWinnerChanged, + ), + ), + ); + } + } + } } From f1df0678243312d7f49862d7dc179dc39f2c6c65 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 18 Jan 2026 10:51:53 +0100 Subject: [PATCH 18/54] delete group view & update build nr --- .../group_view/create_group_view.dart | 116 ------------------ pubspec.yaml | 2 +- 2 files changed, 1 insertion(+), 117 deletions(-) delete mode 100644 lib/presentation/views/main_menu/group_view/create_group_view.dart diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart deleted file mode 100644 index da7eb1d..0000000 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ /dev/null @@ -1,116 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/constants.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/player_selection.dart'; -import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; -import 'package:provider/provider.dart'; - -class CreateGroupView extends StatefulWidget { - /// A view that allows the user to create a new group - const CreateGroupView({super.key}); - - @override - State createState() => _CreateGroupViewState(); -} - -class _CreateGroupViewState extends State { - late final AppDatabase db; - - /// Controller for the group name input field - final _groupNameController = TextEditingController(); - - /// List of currently selected players - List selectedPlayers = []; - - @override - void initState() { - super.initState(); - db = Provider.of(context, listen: false); - _groupNameController.addListener(() { - setState(() {}); - }); - } - - @override - void dispose() { - _groupNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return ScaffoldMessenger( - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: Text(loc.create_new_group)), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - margin: CustomTheme.standardMargin, - child: TextInputField( - controller: _groupNameController, - hintText: loc.group_name, - maxLength: Constants.MAX_GROUP_NAME_LENGTH, - ), - ), - Expanded( - child: PlayerSelection( - onChanged: (value) { - setState(() { - selectedPlayers = [...value]; - }); - }, - ), - ), - CustomWidthButton( - text: loc.create_group, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.primary, - onPressed: - (_groupNameController.text.isEmpty || - (selectedPlayers.length < 2)) - ? null - : () async { - bool success = await db.groupDao.addGroup( - group: Group( - name: _groupNameController.text.trim(), - members: selectedPlayers, - ), - ); - if (!context.mounted) return; - if (success) { - Navigator.pop(context); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: CustomTheme.boxColor, - content: Center( - child: Text( - AppLocalizations.of( - context, - ).error_creating_group, - style: const TextStyle(color: Colors.white), - ), - ), - ), - ); - } - }, - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } -} diff --git a/pubspec.yaml b/pubspec.yaml index c217de3..5e4432d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: game_tracker description: "Game Tracking App for Card Games" publish_to: 'none' -version: 0.0.7+215 +version: 0.0.7+216 environment: sdk: ^3.8.1 From e4c3bc1c5e2498597469c9bd073a633840ef4f41 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 18 Jan 2026 11:41:50 +0100 Subject: [PATCH 19/54] merge & made group_detail_view.dart & group_create_view.dart work together when editing --- lib/l10n/generated/app_localizations.dart | 32 +- lib/l10n/generated/app_localizations_de.dart | 17 +- lib/l10n/generated/app_localizations_en.dart | 17 +- .../group_view/group_create_view.dart | 184 +++++++++ .../group_view/group_detail_view.dart | 387 +++++++++++------- .../group_view/group_profile_view.dart | 271 ------------ .../main_menu/group_view/groups_view.dart | 6 +- .../create_match/create_match_view.dart | 3 + pubspec.yaml | 2 +- 9 files changed, 496 insertions(+), 423 deletions(-) create mode 100644 lib/presentation/views/main_menu/group_view/group_create_view.dart delete mode 100644 lib/presentation/views/main_menu/group_view/group_profile_view.dart diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index eebf3f9..b8f80db 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -122,6 +122,12 @@ abstract class AppLocalizations { /// **'Game Tracker'** String get app_name; + /// Label for best player statistic + /// + /// In en, this message translates to: + /// **'Best Player'** + String get best_player; + /// Cancel button text /// /// In en, this message translates to: @@ -170,6 +176,12 @@ abstract class AppLocalizations { /// **'Create new group'** String get create_new_group; + /// Label for creation date + /// + /// In en, this message translates to: + /// **'Created on'** + String get created_on; + /// Appbar text to create a new match /// /// In en, this message translates to: @@ -221,7 +233,7 @@ abstract class AppLocalizations { /// Confirmation dialog for deleting a group /// /// In en, this message translates to: - /// **'Delete this group'** + /// **'Delete Group'** String get delete_group; /// Button & Appbar label for editing a group @@ -296,6 +308,12 @@ abstract class AppLocalizations { /// **'Group name'** String get group_name; + /// Title for group profile view + /// + /// In en, this message translates to: + /// **'Group Profile'** + String get group_profile; + /// Label for groups /// /// In en, this message translates to: @@ -374,6 +392,12 @@ abstract class AppLocalizations { /// **'Matches'** String get matches; + /// Label for group members + /// + /// In en, this message translates to: + /// **'Members'** + String get members; + /// Title for most points ruleset /// /// In en, this message translates to: @@ -464,6 +488,12 @@ abstract class AppLocalizations { /// **'Not available'** String get not_available; + /// Label for played matches statistic + /// + /// In en, this message translates to: + /// **'Played Matches'** + String get played_matches; + /// Placeholder for player name input /// /// 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 277f5c4..20b1ffb 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -20,6 +20,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get app_name => 'Game Tracker'; + @override + String get best_player => 'Beste:r Spieler:in'; + @override String get cancel => 'Abbrechen'; @@ -46,6 +49,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get create_new_group => 'Neue Gruppe erstellen'; + @override + String get created_on => 'Erstellt am'; + @override String get create_new_match => 'Neues Spiel erstellen'; @@ -73,7 +79,7 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_all_data => 'Alle Daten löschen'; @override - String get delete_group => 'Diese Gruppe löschen'; + String get delete_group => 'Gruppe löschen'; @override String get edit_group => 'Gruppe bearbeiten'; @@ -114,6 +120,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get group_name => 'Gruppenname'; + @override + String get group_profile => 'Gruppenprofil'; + @override String get groups => 'Gruppen'; @@ -153,6 +162,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get matches => 'Spiele'; + @override + String get members => 'Mitglieder'; + @override String get most_points => 'Höchste Punkte'; @@ -199,6 +211,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get not_available => 'Nicht verfügbar'; + @override + String get played_matches => 'Gespielte Spiele'; + @override String get player_name => 'Spieler:innenname'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 79d924e..35d2d2a 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -20,6 +20,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get app_name => 'Game Tracker'; + @override + String get best_player => 'Best Player'; + @override String get cancel => 'Cancel'; @@ -46,6 +49,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get create_new_group => 'Create new group'; + @override + String get created_on => 'Created on'; + @override String get create_new_match => 'Create new match'; @@ -73,7 +79,7 @@ class AppLocalizationsEn extends AppLocalizations { String get delete_all_data => 'Delete all data'; @override - String get delete_group => 'Delete this group'; + String get delete_group => 'Delete Group'; @override String get edit_group => 'Edit Group'; @@ -114,6 +120,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get group_name => 'Group name'; + @override + String get group_profile => 'Group Profile'; + @override String get groups => 'Groups'; @@ -153,6 +162,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get matches => 'Matches'; + @override + String get members => 'Members'; + @override String get most_points => 'Most Points'; @@ -199,6 +211,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get not_available => 'Not available'; + @override + String get played_matches => 'Played Matches'; + @override String get player_name => 'Player name'; diff --git a/lib/presentation/views/main_menu/group_view/group_create_view.dart b/lib/presentation/views/main_menu/group_view/group_create_view.dart new file mode 100644 index 0000000..74b3974 --- /dev/null +++ b/lib/presentation/views/main_menu/group_view/group_create_view.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/player_selection.dart'; +import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; +import 'package:provider/provider.dart'; + +class GroupCreateView extends StatefulWidget { + const GroupCreateView({super.key, this.groupToEdit}); + + /// The group to edit, if any + final Group? groupToEdit; + + @override + State createState() => _GroupCreateViewState(); +} + +class _GroupCreateViewState extends State { + late final AppDatabase db; + + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + /// Controller for the group name input field + final _groupNameController = TextEditingController(); + + /// List of currently selected players + List selectedPlayers = []; + + /// List of initially selected players (when editing a group) + List initialSelectedPlayers = []; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + if(widget.groupToEdit != null) { + _groupNameController.text = widget.groupToEdit!.name; + setState(() { + initialSelectedPlayers = widget.groupToEdit!.members; + selectedPlayers = widget.groupToEdit!.members; + }); + } + _groupNameController.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _groupNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { + if(widget.groupToEdit != null) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(loc.delete_group), + content: Text(loc.this_cannot_be_undone), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(loc.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(loc.delete), + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_group); + } + } + }); + } + },)],), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + margin: CustomTheme.standardMargin, + child: TextInputField( + controller: _groupNameController, + hintText: loc.group_name, + ), + ), + Expanded( + child: PlayerSelection( + initialSelectedPlayers: initialSelectedPlayers, + onChanged: (value) { + setState(() { + selectedPlayers = [...value]; + }); + }, + ), + ), + CustomWidthButton( + text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.primary, + onPressed: + (_groupNameController.text.isEmpty || + (selectedPlayers.length < 2)) + ? null + : () async { + late Group? updatedGroup; + late bool success; + if (widget.groupToEdit == null) { + success = await db.groupDao.addGroup( + group: Group( + name: _groupNameController.text.trim(), + members: selectedPlayers, + ), + ); + } else { + updatedGroup = Group( + id: widget.groupToEdit!.id, + name: _groupNameController.text.trim(), + members: selectedPlayers, + ); + //TODO: Implement group editing in database + /* + success = await db.groupDao.updateGroup( + group: updatedGroup, + ); + */ + success = true; + } + if (!context.mounted) return; + if (success) { + Navigator.pop(context, updatedGroup); + } else { + showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); + } + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({ + required String message, + }) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } +} diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index e372726..7fe88b7 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -1,20 +1,35 @@ import 'package:flutter/material.dart'; +import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/player_selection.dart'; -import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/group_create_view.dart'; +import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; +import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart'; +import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; +import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart'; +import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; +import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; +import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; class GroupDetailView extends StatefulWidget { - const GroupDetailView({super.key, this.groupToEdit}); + /// A view that displays the profile of a group + /// - [group]: The group to display + const GroupDetailView({ + super.key, + required this.group, + required this.callback, + }); - /// The group to edit, if any - final Group? groupToEdit; + /// The group to display + final Group group; + + final VoidCallback callback; @override State createState() => _GroupDetailViewState(); @@ -22,161 +37,243 @@ class GroupDetailView extends StatefulWidget { class _GroupDetailViewState extends State { late final AppDatabase db; + bool isLoading = true; + late Group _group; - /// GlobalKey for ScaffoldMessenger to show snackbars - final _scaffoldMessengerKey = GlobalKey(); + /// Total matches played in this group + int totalMatches = 0; - /// Controller for the group name input field - final _groupNameController = TextEditingController(); - - /// List of currently selected players - List selectedPlayers = []; - - /// List of initially selected players (when editing a group) - List initialSelectedPlayers = []; + String bestPlayer = ''; @override void initState() { super.initState(); + _group = widget.group; db = Provider.of(context, listen: false); - if(widget.groupToEdit != null) { - _groupNameController.text = widget.groupToEdit!.name; - setState(() { - initialSelectedPlayers = widget.groupToEdit!.members; - selectedPlayers = widget.groupToEdit!.members; - }); - } - _groupNameController.addListener(() { - setState(() {}); - }); - } - - @override - void dispose() { - _groupNameController.dispose(); - super.dispose(); + _loadStatistics(); } @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - return ScaffoldMessenger( - key: _scaffoldMessengerKey, - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { - if(widget.groupToEdit != null) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(loc.delete_group), - content: Text(loc.this_cannot_be_undone), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(loc.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(loc.delete), - ), - ], - ), - ).then((confirmed) async { - if (confirmed == true && context.mounted) { - bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); - if (!context.mounted) return; - if (success) { + + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar( + title: Text(loc.group_profile), + actions: [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + showDialog( + context: context, + builder: (context) => CustomAlertDialog( + title: '${loc.delete_group}?', + content: loc.this_cannot_be_undone, + actions: [ + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + loc.cancel, + style: const TextStyle(color: CustomTheme.textColor), + ), + ), + AnimatedDialogButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + loc.delete, + style: TextStyle(color: CustomTheme.secondaryColor), + ), + ), + ], + ), + ).then((confirmed) async { + if (confirmed! && context.mounted) { + await db.groupDao.deleteGroup(groupId: _group.id); + if (!context.mounted) return; Navigator.pop(context); - } else { - if (!mounted) return; - showSnackbar(message: loc.error_deleting_group); + widget.callback.call(); } - } - }); - } - },)],), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - margin: CustomTheme.standardMargin, - child: TextInputField( - controller: _groupNameController, - hintText: loc.group_name, - ), - ), - Expanded( - child: PlayerSelection( - initialSelectedPlayers: initialSelectedPlayers, - onChanged: (value) { - setState(() { - selectedPlayers = [...value]; - }); - }, - ), - ), - CustomWidthButton( - text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.primary, - onPressed: - (_groupNameController.text.isEmpty || - (selectedPlayers.length < 2)) - ? null - : () async { - late bool success; - if (widget.groupToEdit == null) { - success = await db.groupDao.addGroup( - group: Group( - name: _groupNameController.text.trim(), - members: selectedPlayers, - ), - ); - } else { - //TODO: Implement group editing in database - /* - success = await db.groupDao.updateGroup( - group: Group( - id: widget.groupToEdit!.id, - name: _groupNameController.text.trim(), - members: selectedPlayers, - ), - ); - */ - success = false; - } - if (!context.mounted) return; - if (success) { - Navigator.pop(context); - } else { - showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); - } - }, - ), - const SizedBox(height: 20), - ], + }); + }, ), + ], + ), + body: SafeArea( + child: Stack( + alignment: Alignment.center, + children: [ + ListView( + padding: const EdgeInsets.only( + left: 12, + right: 12, + top: 20, + bottom: 100, + ), + children: [ + const Center( + child: ColoredIconContainer( + icon: Icons.group, + containerSize: 55, + iconSize: 38, + ), + ), + const SizedBox(height: 10), + Text( + _group.name, + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 5), + Text( + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(_group.createdAt)}', + style: const TextStyle( + fontSize: 12, + color: CustomTheme.textColor, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + InfoTile( + title: loc.members, + icon: Icons.people, + horizontalAlignment: CrossAxisAlignment.start, + content: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12, + runSpacing: 8, + children: _group.members.map((member) { + return TextIconTile( + text: member.name, + iconEnabled: false, + ); + }).toList(), + ), + ), + const SizedBox(height: 15), + InfoTile( + title: loc.statistics, + icon: Icons.bar_chart, + content: AppSkeleton( + enabled: isLoading, + child: Column( + children: [ + _buildStatRow( + loc.members, + _group.members.length.toString(), + ), + _buildStatRow( + loc.played_matches, + totalMatches.toString(), + ), + _buildStatRow(loc.best_player, bestPlayer), + ], + ), + ), + ), + ], + ), + Positioned( + bottom: MediaQuery.paddingOf(context).bottom, + child: MainMenuButton( + text: loc.edit_group, + icon: Icons.edit, + onPressed: () async { + final updatedGroup = await Navigator.push( + context, + adaptivePageRoute( + builder: (context) { + return GroupCreateView( + groupToEdit: _group, + ); + }, + ), + ); + if (updatedGroup != null && mounted) { + setState(() { + _group = updatedGroup; + }); + _loadStatistics(); + widget.callback(); + } + }, + ), + ), + ], ), ), ); } - /// Displays a snackbar with the given message and optional action. - /// - /// [message] The message to display in the snackbar. - void showSnackbar({ - required String message, - }) { - final messenger = _scaffoldMessengerKey.currentState; - if (messenger != null) { - messenger.hideCurrentSnackBar(); - messenger.showSnackBar( - SnackBar( - content: Text(message, style: const TextStyle(color: Colors.white)), - backgroundColor: CustomTheme.boxColor, - ), - ); - } + + /// Builds a single statistic row with a label and value + /// - [label]: The label of the statistic + /// - [value]: The value of the statistic + Widget _buildStatRow(String label, String value) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 16, + color: CustomTheme.textColor, + ), + ), + ], + ), + Text( + value, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ); } -} + + /// Loads statistics for this group + Future _loadStatistics() async { + final matches = await db.matchDao.getAllMatches(); + final groupMatches = + matches.where((match) => match.group?.id == _group.id).toList(); + + setState(() { + totalMatches = groupMatches.length; + bestPlayer = _getBestPlayer(groupMatches); + isLoading = false; + }); + } + + /// Determines the best player in the group based on match wins + String _getBestPlayer(List matches) { + final bestPlayerCounts = {}; + + // Count wins for each player + for (var match in matches) { + if (match.winner != null) { + bestPlayerCounts.update( + match.winner!, + (value) => value + 1, + ifAbsent: () => 1, + ); + } + } + + // Sort players by win count + final sortedPlayers = bestPlayerCounts.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + // Get the best player + bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-'; + + return bestPlayer; + } +} \ No newline at end of file diff --git a/lib/presentation/views/main_menu/group_view/group_profile_view.dart b/lib/presentation/views/main_menu/group_view/group_profile_view.dart deleted file mode 100644 index e366834..0000000 --- a/lib/presentation/views/main_menu/group_view/group_profile_view.dart +++ /dev/null @@ -1,271 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart'; -import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; -import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; -import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart'; -import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; -import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; -import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; - -class GroupProfileView extends StatefulWidget { - /// A view that displays the profile of a group - /// - [group]: The group to display - const GroupProfileView({ - super.key, - required this.group, - required this.callback, - }); - - /// The group to display - final Group group; - - final VoidCallback callback; - - @override - State createState() => _GroupProfileViewState(); -} - -class _GroupProfileViewState extends State { - late final AppDatabase db; - bool isLoading = true; - - /// Total matches played in this group - int totalMatches = 0; - - String bestPlayer = ''; - - @override - void initState() { - super.initState(); - db = Provider.of(context, listen: false); - _loadStatistics(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - - return Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - title: Text(loc.group_profile), - actions: [ - IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - showDialog( - context: context, - builder: (context) => CustomAlertDialog( - title: '${loc.delete_group}?', - content: loc.this_cannot_be_undone, - actions: [ - AnimatedDialogButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text( - loc.cancel, - style: const TextStyle(color: CustomTheme.textColor), - ), - ), - AnimatedDialogButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text( - loc.delete, - style: TextStyle(color: CustomTheme.secondaryColor), - ), - ), - ], - ), - ).then((confirmed) async { - if (confirmed! && context.mounted) { - await db.groupDao.deleteGroup(groupId: widget.group.id); - if (!context.mounted) return; - Navigator.pop(context); - widget.callback.call(); - } - }); - }, - ), - ], - ), - body: SafeArea( - child: Stack( - alignment: Alignment.center, - children: [ - ListView( - padding: const EdgeInsets.only( - left: 12, - right: 12, - top: 20, - bottom: 100, - ), - children: [ - const Center( - child: ColoredIconContainer( - icon: Icons.group, - containerSize: 55, - iconSize: 38, - ), - ), - const SizedBox(height: 10), - Text( - widget.group.name, - style: const TextStyle( - fontSize: 28, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 5), - Text( - '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.group.createdAt)}', - style: const TextStyle( - fontSize: 12, - color: CustomTheme.textColor, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 20), - InfoTile( - title: loc.members, - icon: Icons.people, - horizontalAlignment: CrossAxisAlignment.start, - content: Wrap( - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.start, - spacing: 12, - runSpacing: 8, - children: widget.group.members.map((member) { - return TextIconTile( - text: member.name, - iconEnabled: false, - ); - }).toList(), - ), - ), - const SizedBox(height: 15), - InfoTile( - title: loc.statistics, - icon: Icons.bar_chart, - content: AppSkeleton( - enabled: isLoading, - child: Column( - children: [ - _buildStatRow( - loc.members, - widget.group.members.length.toString(), - ), - _buildStatRow( - loc.played_matches, - totalMatches.toString(), - ), - _buildStatRow(loc.best_player, bestPlayer), - ], - ), - ), - ), - ], - ), - Positioned( - bottom: MediaQuery.paddingOf(context).bottom, - child: MainMenuButton( - text: loc.edit_group, - icon: Icons.edit, - onPressed: () { - // TODO: Uncomment when GroupDetailView is implemented - /* - await Navigator.push( - context, - adaptivePageRoute( - builder: (context) { - - return const GroupDetailView(); - }, - ), - );*/ - print('Edit Group pressed'); - }, - ), - ), - ], - ), - ), - ); - } - - /// Builds a single statistic row with a label and value - /// - [label]: The label of the statistic - /// - [value]: The value of the statistic - Widget _buildStatRow(String label, String value) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Text( - label, - style: const TextStyle( - fontSize: 16, - color: CustomTheme.textColor, - ), - ), - ], - ), - Text( - value, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - ], - ), - ); - } - - /// Loads statistics for this group - Future _loadStatistics() async { - final matches = await db.matchDao.getAllMatches(); - final groupMatches = matches - .where((match) => match.group?.id == widget.group.id) - .toList(); - - setState(() { - totalMatches = groupMatches.length; - bestPlayer = _getBestPlayer(groupMatches); - isLoading = false; - }); - } - - /// Determines the best player in the group based on match wins - String _getBestPlayer(List matches) { - final bestPlayerCounts = {}; - - // Count wins for each player - for (var match in matches) { - if (match.winner != null) { - bestPlayerCounts.update( - match.winner!, - (value) => value + 1, - ifAbsent: () => 1, - ); - } - } - - // Sort players by win count - final sortedPlayers = bestPlayerCounts.entries.toList() - ..sort((a, b) => b.value.compareTo(a.value)); - - // Get the best player - bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-'; - - return bestPlayer; - } -} diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 61dbe51..6462205 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -6,8 +6,8 @@ import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/group_profile_view.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/group_detail_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/group_create_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; @@ -82,7 +82,7 @@ class _GroupsViewState extends State { context, adaptivePageRoute( builder: (context) { - return GroupProfileView( + return GroupDetailView( group: groups[index], callback: loadGroups, ); @@ -105,7 +105,7 @@ class _GroupsViewState extends State { context, adaptivePageRoute( builder: (context) { - return const GroupDetailView(); + return const GroupCreateView(); }, ), ); diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index a3d5cf1..70f0929 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -64,6 +64,9 @@ class _CreateMatchViewState extends State { /// The currently selected players List? selectedPlayers; + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + @override void initState() { super.initState(); diff --git a/pubspec.yaml b/pubspec.yaml index aa253b3..210c625 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: game_tracker description: "Game Tracking App for Card Games" publish_to: 'none' -version: 0.0.7+239 +version: 0.0.7+241 environment: sdk: ^3.8.1 From 810f635987520ad969c50dc095ceda5bac975110 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 18 Jan 2026 12:25:47 +0100 Subject: [PATCH 20/54] merge fix --- .../main_menu/custom_navigation_bar.dart | 2 +- .../group_view/create_group_view.dart | 185 ++++++++++++++++++ .../group_view/group_create_view.dart | 184 ----------------- .../group_view/group_detail_view.dart | 4 +- .../{groups_view.dart => group_view.dart} | 4 +- 5 files changed, 190 insertions(+), 189 deletions(-) delete mode 100644 lib/presentation/views/main_menu/group_view/group_create_view.dart rename lib/presentation/views/main_menu/group_view/{groups_view.dart => group_view.dart} (98%) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index a110419..b17f63d 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/group_view.dart'; import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/settings_view.dart'; diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index e69de29..678872a 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/core/enums.dart'; +import 'package:game_tracker/data/db/database.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/l10n/generated/app_localizations.dart'; +import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/player_selection.dart'; +import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; +import 'package:provider/provider.dart'; + +class CreateGroupView extends StatefulWidget { + const CreateGroupView({super.key, this.groupToEdit}); + + /// The group to edit, if any + final Group? groupToEdit; + + @override + State createState() => _CreateGroupViewState(); +} + +class _CreateGroupViewState extends State { + late final AppDatabase db; + + /// GlobalKey for ScaffoldMessenger to show snackbars + final _scaffoldMessengerKey = GlobalKey(); + + /// Controller for the group name input field + final _groupNameController = TextEditingController(); + + /// List of currently selected players + List selectedPlayers = []; + + /// List of initially selected players (when editing a group) + List initialSelectedPlayers = []; + + @override + void initState() { + super.initState(); + db = Provider.of(context, listen: false); + if(widget.groupToEdit != null) { + _groupNameController.text = widget.groupToEdit!.name; + setState(() { + initialSelectedPlayers = widget.groupToEdit!.members; + selectedPlayers = widget.groupToEdit!.members; + }); + } + _groupNameController.addListener(() { + setState(() {}); + }); + } + + @override + void dispose() { + _groupNameController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + return ScaffoldMessenger( + key: _scaffoldMessengerKey, + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: CustomTheme.backgroundColor, + appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { + if(widget.groupToEdit != null) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(loc.delete_group), + content: Text(loc.this_cannot_be_undone), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(loc.cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(loc.delete), + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_group); + } + } + }); + } + },)],), + body: SafeArea( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + margin: CustomTheme.standardMargin, + child: TextInputField( + controller: _groupNameController, + hintText: loc.group_name, + ), + ), + Expanded( + child: PlayerSelection( + initialSelectedPlayers: initialSelectedPlayers, + onChanged: (value) { + setState(() { + selectedPlayers = [...value]; + }); + }, + ), + ), + CustomWidthButton( + text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, + sizeRelativeToWidth: 0.95, + buttonType: ButtonType.primary, + onPressed: + (_groupNameController.text.isEmpty || + (selectedPlayers.length < 2)) + ? null + : () async { + late Group? updatedGroup; + late bool success; + if (widget.groupToEdit == null) { + success = await db.groupDao.addGroup( + group: Group( + name: _groupNameController.text.trim(), + members: selectedPlayers, + ), + ); + } else { + updatedGroup = Group( + id: widget.groupToEdit!.id, + name: _groupNameController.text.trim(), + members: selectedPlayers, + ); + //TODO: Implement group editing in database + /* + success = await db.groupDao.updateGroup( + group: updatedGroup, + ); + */ + success = true; + } + if (!context.mounted) return; + if (success) { + Navigator.pop(context, updatedGroup); + } else { + showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); + } + }, + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } + /// Displays a snackbar with the given message and optional action. + /// + /// [message] The message to display in the snackbar. + void showSnackbar({ + required String message, + }) { + final messenger = _scaffoldMessengerKey.currentState; + if (messenger != null) { + messenger.hideCurrentSnackBar(); + messenger.showSnackBar( + SnackBar( + content: Text(message, style: const TextStyle(color: Colors.white)), + backgroundColor: CustomTheme.boxColor, + ), + ); + } + } +} diff --git a/lib/presentation/views/main_menu/group_view/group_create_view.dart b/lib/presentation/views/main_menu/group_view/group_create_view.dart deleted file mode 100644 index 74b3974..0000000 --- a/lib/presentation/views/main_menu/group_view/group_create_view.dart +++ /dev/null @@ -1,184 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/core/enums.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/group.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; -import 'package:game_tracker/presentation/widgets/player_selection.dart'; -import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; -import 'package:provider/provider.dart'; - -class GroupCreateView extends StatefulWidget { - const GroupCreateView({super.key, this.groupToEdit}); - - /// The group to edit, if any - final Group? groupToEdit; - - @override - State createState() => _GroupCreateViewState(); -} - -class _GroupCreateViewState extends State { - late final AppDatabase db; - - /// GlobalKey for ScaffoldMessenger to show snackbars - final _scaffoldMessengerKey = GlobalKey(); - - /// Controller for the group name input field - final _groupNameController = TextEditingController(); - - /// List of currently selected players - List selectedPlayers = []; - - /// List of initially selected players (when editing a group) - List initialSelectedPlayers = []; - - @override - void initState() { - super.initState(); - db = Provider.of(context, listen: false); - if(widget.groupToEdit != null) { - _groupNameController.text = widget.groupToEdit!.name; - setState(() { - initialSelectedPlayers = widget.groupToEdit!.members; - selectedPlayers = widget.groupToEdit!.members; - }); - } - _groupNameController.addListener(() { - setState(() {}); - }); - } - - @override - void dispose() { - _groupNameController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return ScaffoldMessenger( - key: _scaffoldMessengerKey, - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { - if(widget.groupToEdit != null) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(loc.delete_group), - content: Text(loc.this_cannot_be_undone), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(loc.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(loc.delete), - ), - ], - ), - ).then((confirmed) async { - if (confirmed == true && context.mounted) { - bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); - if (!context.mounted) return; - if (success) { - Navigator.pop(context); - } else { - if (!mounted) return; - showSnackbar(message: loc.error_deleting_group); - } - } - }); - } - },)],), - body: SafeArea( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - margin: CustomTheme.standardMargin, - child: TextInputField( - controller: _groupNameController, - hintText: loc.group_name, - ), - ), - Expanded( - child: PlayerSelection( - initialSelectedPlayers: initialSelectedPlayers, - onChanged: (value) { - setState(() { - selectedPlayers = [...value]; - }); - }, - ), - ), - CustomWidthButton( - text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, - sizeRelativeToWidth: 0.95, - buttonType: ButtonType.primary, - onPressed: - (_groupNameController.text.isEmpty || - (selectedPlayers.length < 2)) - ? null - : () async { - late Group? updatedGroup; - late bool success; - if (widget.groupToEdit == null) { - success = await db.groupDao.addGroup( - group: Group( - name: _groupNameController.text.trim(), - members: selectedPlayers, - ), - ); - } else { - updatedGroup = Group( - id: widget.groupToEdit!.id, - name: _groupNameController.text.trim(), - members: selectedPlayers, - ); - //TODO: Implement group editing in database - /* - success = await db.groupDao.updateGroup( - group: updatedGroup, - ); - */ - success = true; - } - if (!context.mounted) return; - if (success) { - Navigator.pop(context, updatedGroup); - } else { - showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); - } - }, - ), - const SizedBox(height: 20), - ], - ), - ), - ), - ); - } - /// Displays a snackbar with the given message and optional action. - /// - /// [message] The message to display in the snackbar. - void showSnackbar({ - required String message, - }) { - final messenger = _scaffoldMessengerKey.currentState; - if (messenger != null) { - messenger.hideCurrentSnackBar(); - messenger.showSnackBar( - SnackBar( - content: Text(message, style: const TextStyle(color: Colors.white)), - backgroundColor: CustomTheme.boxColor, - ), - ); - } - } -} diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 7fe88b7..81a1c6e 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -6,7 +6,7 @@ import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/group_create_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; @@ -188,7 +188,7 @@ class _GroupDetailViewState extends State { context, adaptivePageRoute( builder: (context) { - return GroupCreateView( + return CreateGroupView( groupToEdit: _group, ); }, diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart similarity index 98% rename from lib/presentation/views/main_menu/group_view/groups_view.dart rename to lib/presentation/views/main_menu/group_view/group_view.dart index 6462205..a995f12 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -7,7 +7,7 @@ import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/group_detail_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/group_view/group_create_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; @@ -105,7 +105,7 @@ class _GroupsViewState extends State { context, adaptivePageRoute( builder: (context) { - return const GroupCreateView(); + return const CreateGroupView(); }, ), ); From cca09cc27ebeeb9e372464cb1128b997c37bd128 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 13:01:24 +0100 Subject: [PATCH 21/54] Renamed MatchProfileView to MatchDetailView --- .../{match_profile_view.dart => match_detail_view.dart} | 8 ++++---- .../views/main_menu/match_view/match_view.dart | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) rename lib/presentation/views/main_menu/match_view/{match_profile_view.dart => match_detail_view.dart} (98%) diff --git a/lib/presentation/views/main_menu/match_view/match_profile_view.dart b/lib/presentation/views/main_menu/match_view/match_detail_view.dart similarity index 98% rename from lib/presentation/views/main_menu/match_view/match_profile_view.dart rename to lib/presentation/views/main_menu/match_view/match_detail_view.dart index d301f9e..d70d9aa 100644 --- a/lib/presentation/views/main_menu/match_view/match_profile_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_detail_view.dart @@ -16,11 +16,11 @@ import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -class MatchProfileView extends StatefulWidget { +class MatchDetailView extends StatefulWidget { /// A view that displays the profile of a match /// - [match]: The match to display /// - [callback]: Callback to refresh the match list - const MatchProfileView({ + const MatchDetailView({ super.key, required this.match, required this.callback, @@ -33,10 +33,10 @@ class MatchProfileView extends StatefulWidget { final VoidCallback callback; @override - State createState() => _MatchProfileViewState(); + State createState() => _MatchDetailViewState(); } -class _MatchProfileViewState extends State { +class _MatchDetailViewState extends State { late final AppDatabase db; late Player? currentWinner; diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 65ff29c..b0d540b 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -11,7 +11,7 @@ import 'package:game_tracker/data/dto/match.dart'; import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_profile_view.dart'; +import 'package:game_tracker/presentation/views/main_menu/match_view/match_detail_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; @@ -89,7 +89,7 @@ class _MatchViewState extends State { Navigator.push( context, adaptivePageRoute( - builder: (context) => MatchProfileView( + builder: (context) => MatchDetailView( match: matches[index], callback: loadGames, ), From 9a5929382b3dfb1c4b6a39e837183b27d6db7099 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 18 Jan 2026 13:08:33 +0100 Subject: [PATCH 22/54] Changed button icon --- .../views/main_menu/match_view/match_detail_view.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 d70d9aa..05fb6c0 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 @@ -233,7 +233,7 @@ class _MatchDetailViewState extends State { const SizedBox(width: 15), MainMenuButton( text: loc.enter_results, - icon: Icons.note_add, + icon: Icons.emoji_events, onPressed: () async { currentWinner = await Navigator.push( context, diff --git a/pubspec.yaml b/pubspec.yaml index 54ff72b..a2025b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: game_tracker description: "Game Tracking App for Card Games" publish_to: 'none' -version: 0.0.9+242 +version: 0.0.9+243 environment: sdk: ^3.8.1 From 527ffd194f5fc33f33733ddab23a0b010c202033 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 23 Feb 2026 21:34:29 +0100 Subject: [PATCH 23/54] Fixed PR problems --- .../main_menu/custom_navigation_bar.dart | 2 +- .../group_view/create_group_view.dart | 94 +++++++++++-------- .../group_view/group_detail_view.dart | 15 +-- .../main_menu/group_view/group_view.dart | 2 +- .../create_match/create_match_view.dart | 10 -- .../match_view/match_detail_view.dart | 32 ++++--- .../main_menu/match_view/match_view.dart | 2 +- 7 files changed, 85 insertions(+), 72 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 6d18091..cbea02a 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; -import 'package:tallee/presentation/views/main_menu/group_view/groups_view.dart'; +import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart'; import 'package:tallee/presentation/views/main_menu/home_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart'; diff --git a/lib/presentation/views/main_menu/group_view/create_group_view.dart b/lib/presentation/views/main_menu/group_view/create_group_view.dart index 262a6e8..be4ed70 100644 --- a/lib/presentation/views/main_menu/group_view/create_group_view.dart +++ b/lib/presentation/views/main_menu/group_view/create_group_view.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:tallee/core/constants.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; @@ -40,7 +39,7 @@ class _CreateGroupViewState extends State { void initState() { super.initState(); db = Provider.of(context, listen: false); - if(widget.groupToEdit != null) { + if (widget.groupToEdit != null) { _groupNameController.text = widget.groupToEdit!.name; setState(() { initialSelectedPlayers = widget.groupToEdit!.members; @@ -66,38 +65,54 @@ class _CreateGroupViewState extends State { child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { - if(widget.groupToEdit != null) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(loc.delete_group), - content: Text(loc.this_cannot_be_undone), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(loc.cancel), - ), - TextButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(loc.delete), + appBar: AppBar( + title: Text( + widget.groupToEdit == null ? loc.create_new_group : loc.edit_group, + ), + actions: widget.groupToEdit == null + ? [] + : [ + IconButton( + icon: const Icon(Icons.delete), + onPressed: () async { + if (widget.groupToEdit != null) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(loc.delete_group), + content: Text(loc.this_cannot_be_undone), + actions: [ + TextButton( + onPressed: () => + Navigator.of(context).pop(false), + child: Text(loc.cancel), + ), + TextButton( + onPressed: () => + Navigator.of(context).pop(true), + child: Text(loc.delete), + ), + ], + ), + ).then((confirmed) async { + if (confirmed == true && context.mounted) { + bool success = await db.groupDao.deleteGroup( + groupId: widget.groupToEdit!.id, + ); + if (!context.mounted) return; + if (success) { + Navigator.pop(context); + } else { + if (!mounted) return; + showSnackbar(message: loc.error_deleting_group); + } + } + }); + } + }, ), ], - ), - ).then((confirmed) async { - if (confirmed == true && context.mounted) { - bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id); - if (!context.mounted) return; - if (success) { - Navigator.pop(context); - } else { - if (!mounted) return; - showSnackbar(message: loc.error_deleting_group); - } - } - }); - } - },)],), + ), body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -120,7 +135,9 @@ class _CreateGroupViewState extends State { ), ), CustomWidthButton( - text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, + text: widget.groupToEdit == null + ? loc.create_group + : loc.edit_group, sizeRelativeToWidth: 0.95, buttonType: ButtonType.primary, onPressed: @@ -155,7 +172,11 @@ class _CreateGroupViewState extends State { if (success) { Navigator.pop(context, updatedGroup); } else { - showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); + showSnackbar( + message: widget.groupToEdit == null + ? loc.error_creating_group + : loc.error_editing_group, + ); } }, ), @@ -166,12 +187,11 @@ class _CreateGroupViewState extends State { ), ); } + /// Displays a snackbar with the given message and optional action. /// /// [message] The message to display in the snackbar. - void showSnackbar({ - required String message, - }) { + void showSnackbar({required String message}) { final messenger = _scaffoldMessengerKey.currentState; if (messenger != null) { messenger.hideCurrentSnackBar(); diff --git a/lib/presentation/views/main_menu/group_view/group_detail_view.dart b/lib/presentation/views/main_menu/group_view/group_detail_view.dart index 802701a..ad88d66 100644 --- a/lib/presentation/views/main_menu/group_view/group_detail_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_detail_view.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; @@ -189,9 +191,7 @@ class _GroupDetailViewState extends State { context, adaptivePageRoute( builder: (context) { - return CreateGroupView( - groupToEdit: _group, - ); + return CreateGroupView(groupToEdit: _group); }, ), ); @@ -243,8 +243,9 @@ class _GroupDetailViewState extends State { /// Loads statistics for this group Future _loadStatistics() async { final matches = await db.matchDao.getAllMatches(); - final groupMatches = - matches.where((match) => match.group?.id == _group.id).toList(); + final groupMatches = matches + .where((match) => match.group?.id == _group.id) + .toList(); setState(() { totalMatches = groupMatches.length; @@ -262,7 +263,7 @@ class _GroupDetailViewState extends State { if (match.winner != null) { bestPlayerCounts.update( match.winner!, - (value) => value + 1, + (value) => value + 1, ifAbsent: () => 1, ); } @@ -277,4 +278,4 @@ class _GroupDetailViewState extends State { return bestPlayer; } -} \ No newline at end of file +} diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index 2c9a1fd..cf6f550 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -8,7 +8,7 @@ import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/dto/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; -import 'package:tallee/presentation/views/main_menu/group_view/group_profile_view.dart'; +import 'package:tallee/presentation/views/main_menu/group_view/group_detail_view.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 3f46c8d..cf7ce86 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -266,14 +266,4 @@ class _CreateMatchViewState extends State { } } } - - /// Determines whether the "Create Match" button should be enabled. - /// - /// Returns `true` if: - /// - A ruleset is selected AND - /// - Either a group is selected OR at least 2 players are selected - bool _enableCreateGameButton() { - return (selectedGroup != null || - (selectedPlayers != null && selectedPlayers!.length > 1)); - } } 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 05fb6c0..6336bd0 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 @@ -1,20 +1,20 @@ import 'package:flutter/material.dart'; -import 'package:game_tracker/core/adaptive_page_route.dart'; -import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/data/db/database.dart'; -import 'package:game_tracker/data/dto/match.dart'; -import 'package:game_tracker/data/dto/player.dart'; -import 'package:game_tracker/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; -import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart'; -import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; -import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; -import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart'; -import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; -import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/custom_theme.dart'; +import 'package:tallee/data/db/database.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/data/dto/player.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; +import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/colored_icon_container.dart'; +import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class MatchDetailView extends StatefulWidget { /// A view that displays the profile of a match @@ -81,7 +81,9 @@ class _MatchDetailViewState extends State { onPressed: () => Navigator.of(context).pop(true), child: Text( loc.delete, - style: TextStyle(color: CustomTheme.secondaryColor), + style: const TextStyle( + color: CustomTheme.secondaryColor, + ), ), ), ], diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 4929f0c..c6abd2b 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -12,7 +12,7 @@ import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; -import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; +import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/match_tile.dart'; From b84a8937065b38f70bd4361fae663d6c58b57e1c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Mon, 23 Feb 2026 21:35:26 +0100 Subject: [PATCH 24/54] Typo --- .../views/main_menu/custom_navigation_bar.dart | 2 +- .../views/main_menu/group_view/group_view.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index cbea02a..508463d 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -38,7 +38,7 @@ class _CustomNavigationBarState extends State ), KeyedSubtree( key: ValueKey('groups_$tabKeyCount'), - child: const GroupsView(), + child: const GroupView(), ), KeyedSubtree( key: ValueKey('stats_$tabKeyCount'), diff --git a/lib/presentation/views/main_menu/group_view/group_view.dart b/lib/presentation/views/main_menu/group_view/group_view.dart index cf6f550..92d4489 100644 --- a/lib/presentation/views/main_menu/group_view/group_view.dart +++ b/lib/presentation/views/main_menu/group_view/group_view.dart @@ -14,15 +14,15 @@ import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/tiles/group_tile.dart'; import 'package:tallee/presentation/widgets/top_centered_message.dart'; -class GroupsView extends StatefulWidget { +class GroupView extends StatefulWidget { /// A view that displays a list of groups - const GroupsView({super.key}); + const GroupView({super.key}); @override - State createState() => _GroupsViewState(); + State createState() => _GroupViewState(); } -class _GroupsViewState extends State { +class _GroupViewState extends State { late final AppDatabase db; /// Loaded groups from the database From f07103a5163ed277c8ed9452a547e9ede9e06bca Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 24 Feb 2026 17:49:24 +0100 Subject: [PATCH 25/54] Fixed theme issue --- lib/main.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 0002531..8818444 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,10 +40,17 @@ class GameTracker extends StatelessWidget { primaryColor: CustomTheme.primaryColor, scaffoldBackgroundColor: CustomTheme.backgroundColor, appBarTheme: CustomTheme.appBarTheme, - colorScheme: ColorScheme.fromSeed( - seedColor: CustomTheme.primaryColor, + colorScheme: const ColorScheme( brightness: Brightness.dark, - ).copyWith(surface: CustomTheme.backgroundColor), + primary: CustomTheme.primaryColor, + onPrimary: CustomTheme.textColor, + secondary: CustomTheme.textColor, + onSecondary: Color(0xFF000000), + error: Color(0xFFFF0000), + onError: CustomTheme.textColor, + surface: CustomTheme.backgroundColor, + onSurface: CustomTheme.textColor, + ), pageTransitionsTheme: const PageTransitionsTheme( builders: { TargetPlatform.iOS: CupertinoPageTransitionsBuilder(), From e71943f6e2f09f3203ff1220c12f6974f4120b1c Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Tue, 24 Feb 2026 18:01:10 +0100 Subject: [PATCH 26/54] Implemented Radio Theme --- lib/main.dart | 15 ++++++++++----- .../widgets/tiles/custom_radio_list_tile.dart | 6 +----- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 8818444..ab0f34d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -40,14 +40,19 @@ class GameTracker extends StatelessWidget { primaryColor: CustomTheme.primaryColor, scaffoldBackgroundColor: CustomTheme.backgroundColor, appBarTheme: CustomTheme.appBarTheme, - colorScheme: const ColorScheme( + radioTheme: RadioThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return CustomTheme.primaryColor; + } + return CustomTheme.textColor; + }), + ), + colorScheme: ColorScheme.fromSeed( + seedColor: CustomTheme.primaryColor, brightness: Brightness.dark, primary: CustomTheme.primaryColor, onPrimary: CustomTheme.textColor, - secondary: CustomTheme.textColor, - onSecondary: Color(0xFF000000), - error: Color(0xFFFF0000), - onError: CustomTheme.textColor, surface: CustomTheme.backgroundColor, onSurface: CustomTheme.textColor, ), diff --git a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart index 53d0a03..5559d10 100644 --- a/lib/presentation/widgets/tiles/custom_radio_list_tile.dart +++ b/lib/presentation/widgets/tiles/custom_radio_list_tile.dart @@ -36,11 +36,7 @@ class CustomRadioListTile extends StatelessWidget { ), child: Row( children: [ - Radio( - value: value, - activeColor: CustomTheme.primaryColor, - toggleable: true, - ), + Radio(value: value, toggleable: true), Expanded( child: Text( text, From 5ed35362acc6b3d28df523bcf88a8115de367568 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 5 Mar 2026 12:25:01 +0100 Subject: [PATCH 27/54] Fix: Setting & fetching winner --- lib/data/dao/match_dao.dart | 117 ++++++++++++++++++++---------------- 1 file changed, 66 insertions(+), 51 deletions(-) diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 5726df5..cc30b03 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -27,9 +27,9 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { if (row.groupId != null) { group = await db.groupDao.getGroupById(groupId: row.groupId!); } - final players = await db.playerMatchDao.getPlayersOfMatch( - matchId: row.id, - ) ?? []; + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: row.id) ?? []; + final winner = await getWinner(matchId: row.id); return Match( id: row.id, name: row.name ?? '', @@ -39,6 +39,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { notes: row.notes ?? '', createdAt: row.createdAt, endedAt: row.endedAt, + winner: winner, ); }), ); @@ -56,7 +57,10 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { group = await db.groupDao.getGroupById(groupId: result.groupId!); } - final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + + final winner = await getWinner(matchId: matchId); return Match( id: result.id, @@ -67,6 +71,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { notes: result.notes ?? '', createdAt: result.createdAt, endedAt: result.endedAt, + winner: winner, ); } @@ -94,6 +99,10 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { playerId: p.id, ); } + + if (match.winner != null) { + await setWinner(matchId: match.id, winnerId: match.winner!.id); + } }); } @@ -112,20 +121,20 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { if (uniqueGames.isNotEmpty) { await db.batch( - (b) => b.insertAll( + (b) => b.insertAll( db.gameTable, uniqueGames.values .map( (game) => GameTableCompanion.insert( - id: game.id, - name: game.name, - ruleset: game.ruleset.name, - description: game.description, - color: game.color.name, - icon: game.icon, - createdAt: game.createdAt, - ), - ) + id: game.id, + name: game.name, + ruleset: game.ruleset.name, + description: game.description, + color: game.color.name, + icon: game.icon, + createdAt: game.createdAt, + ), + ) .toList(), mode: InsertMode.insertOrIgnore, ), @@ -134,18 +143,18 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { // Add all groups of the matches in batch await db.batch( - (b) => b.insertAll( + (b) => b.insertAll( db.groupTable, matches .where((match) => match.group != null) .map( (match) => GroupTableCompanion.insert( - id: match.group!.id, - name: match.group!.name, - description: match.group!.description, - createdAt: match.group!.createdAt, - ), - ) + id: match.group!.id, + name: match.group!.name, + description: match.group!.description, + createdAt: match.group!.createdAt, + ), + ) .toList(), mode: InsertMode.insertOrIgnore, ), @@ -153,20 +162,20 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { // Add all matches in batch await db.batch( - (b) => b.insertAll( + (b) => b.insertAll( matchTable, matches .map( (match) => MatchTableCompanion.insert( - id: match.id, - gameId: match.game.id, - groupId: Value(match.group?.id), - name: Value(match.name), - notes: Value(match.notes), - createdAt: match.createdAt, - endedAt: Value(match.endedAt), - ), - ) + id: match.id, + gameId: match.game.id, + groupId: Value(match.group?.id), + name: Value(match.name), + notes: Value(match.notes), + createdAt: match.createdAt, + endedAt: Value(match.endedAt), + ), + ) .toList(), mode: InsertMode.insertOrReplace, ), @@ -188,17 +197,17 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { if (uniquePlayers.isNotEmpty) { await db.batch( - (b) => b.insertAll( + (b) => b.insertAll( db.playerTable, uniquePlayers.values .map( (p) => PlayerTableCompanion.insert( - id: p.id, - name: p.name, - description: p.description, - createdAt: p.createdAt, - ), - ) + id: p.id, + name: p.name, + description: p.description, + createdAt: p.createdAt, + ), + ) .toList(), mode: InsertMode.insertOrIgnore, ), @@ -253,9 +262,9 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Retrieves the number of matches in the database. Future getMatchCount() async { final count = - await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) - .map((row) => row.read(matchTable.id.count())) - .getSingle(); + await (selectOnly(matchTable)..addColumns([matchTable.id.count()])) + .map((row) => row.read(matchTable.id.count())) + .getSingle(); return count ?? 0; } @@ -315,15 +324,16 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } /// Updates the group of the match with the given [matchId]. + /// Replaces the existing group association with the new group specified by [newGroupId]. /// Pass null to remove the group association. /// Returns `true` if more than 0 rows were affected, otherwise `false`. Future updateMatchGroup({ required String matchId, - required String? groupId, + required String? newGroupId, }) async { final query = update(matchTable)..where((g) => g.id.equals(matchId)); final rowsAffected = await query.write( - MatchTableCompanion(groupId: Value(groupId)), + MatchTableCompanion(groupId: Value(newGroupId)), ); return rowsAffected > 0; } @@ -379,10 +389,12 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { // Add the new players to the match await Future.wait( - newPlayers.map((player) => db.playerMatchDao.addPlayerToMatch( - matchId: matchId, - playerId: player.id, - )), + newPlayers.map( + (player) => db.playerMatchDao.addPlayerToMatch( + matchId: matchId, + playerId: player.id, + ), + ), ); }); } @@ -394,7 +406,8 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Checks if a match has a winner. /// Returns true if any player in the match has their score set to 1. Future hasWinner({required String matchId}) async { - final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; for (final player in players) { final score = await db.playerMatchDao.getPlayerScore( @@ -411,7 +424,8 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Gets the winner of a match. /// Returns the player with score 1, or null if no winner is set. Future getWinner({required String matchId}) async { - final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; for (final player in players) { final score = await db.playerMatchDao.getPlayerScore( @@ -433,7 +447,8 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { required String winnerId, }) async { await db.transaction(() async { - final players = await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; + final players = + await db.playerMatchDao.getPlayersOfMatch(matchId: matchId) ?? []; // Set all players' scores to 0 for (final player in players) { @@ -470,4 +485,4 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); return success; } -} \ No newline at end of file +} From 37955c5701bb3c180baedf7a4960b2a8378ba7ab Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 5 Mar 2026 12:25:09 +0100 Subject: [PATCH 28/54] test: Setting & fetching winner --- test/db_tests/aggregates/match_test.dart | 49 ++++++++++++++++++------ 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/test/db_tests/aggregates/match_test.dart b/test/db_tests/aggregates/match_test.dart index 0718e0d..ea80369 100644 --- a/test/db_tests/aggregates/match_test.dart +++ b/test/db_tests/aggregates/match_test.dart @@ -1,13 +1,13 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:tallee/core/enums.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/game.dart'; import 'package:tallee/data/dto/group.dart'; import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; -import 'package:tallee/core/enums.dart'; void main() { late AppDatabase database; @@ -51,7 +51,13 @@ void main() { description: '', members: [testPlayer4, testPlayer5], ); - testGame = Game(name: 'Test Game', ruleset: Ruleset.singleWinner, description: 'A test game', color: GameColor.blue, icon: ''); + testGame = Game( + name: 'Test Game', + ruleset: Ruleset.singleWinner, + description: 'A test game', + color: GameColor.blue, + icon: '', + ); testMatch1 = Match( name: 'First Test Match', game: testGame, @@ -99,7 +105,6 @@ void main() { }); group('Match Tests', () { - // Verifies that a single match can be added and retrieved with all fields, group, and players intact. test('Adding and fetching single match works correctly', () async { await database.matchDao.addMatch(match: testMatch1); @@ -127,10 +132,7 @@ void main() { for (int i = 0; i < testMatch1.players.length; i++) { expect(result.players[i].id, testMatch1.players[i].id); expect(result.players[i].name, testMatch1.players[i].name); - expect( - result.players[i].createdAt, - testMatch1.players[i].createdAt, - ); + expect(result.players[i].createdAt, testMatch1.players[i].createdAt); } }); @@ -191,10 +193,7 @@ void main() { for (int i = 0; i < testMatch.players.length; i++) { expect(match.players[i].id, testMatch.players[i].id); expect(match.players[i].name, testMatch.players[i].name); - expect( - match.players[i].createdAt, - testMatch.players[i].createdAt, - ); + expect(match.players[i].createdAt, testMatch.players[i].createdAt); } } }); @@ -282,5 +281,31 @@ void main() { ); expect(fetchedMatch.name, newName); }); + + test('Fetching a winner works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + var fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + + expect(fetchedMatch.winner, isNotNull); + expect(fetchedMatch.winner!.id, testPlayer4.id); + }); + + test('Setting a winner works correctly', () async { + await database.matchDao.addMatch(match: testMatch1); + + await database.matchDao.setWinner( + matchId: testMatch1.id, + winnerId: testPlayer5.id, + ); + + final fetchedMatch = await database.matchDao.getMatchById( + matchId: testMatch1.id, + ); + expect(fetchedMatch.winner, isNotNull); + expect(fetchedMatch.winner!.id, testPlayer5.id); + }); }); } From f9edf64e83fc4177c3269d74b4e96bc69549d6cf Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 5 Mar 2026 22:21:50 +0100 Subject: [PATCH 29/54] Small changes --- .../views/main_menu/match_view/match_view.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 7741e29..e5a4e29 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -1,5 +1,3 @@ -import 'dart:core' hide Match; - import 'package:flutter/material.dart'; import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:provider/provider.dart'; @@ -38,7 +36,13 @@ class _MatchViewState extends State { 4, Match( name: 'Skeleton match name', - game: Game(name: '', ruleset: Ruleset.singleWinner, description: '', color: GameColor.blue, icon: ''), + game: Game( + name: '', + ruleset: Ruleset.singleWinner, + description: '', + color: GameColor.blue, + icon: '', + ), group: Group( name: 'Group name', description: '', @@ -96,7 +100,7 @@ class _MatchViewState extends State { adaptivePageRoute( builder: (context) => MatchDetailView( match: matches[index], - callback: loadGames, + onMatchUpdate: loadGames, ), ), ); From 89ad6824e6fa747d388196918f5146e257d0a318 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 5 Mar 2026 22:22:13 +0100 Subject: [PATCH 30/54] Added import, removed unessecary player add --- lib/presentation/widgets/tiles/match_tile.dart | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 7862000..ddc7887 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -1,3 +1,5 @@ +import 'dart:core' hide Match; + import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:tallee/core/custom_theme.dart'; @@ -248,16 +250,6 @@ class _MatchTileState extends State { } } - // Add players from game.group.players - if (widget.match.group?.members != null) { - for (var player in widget.match.group!.members) { - if (!playerIds.contains(player.id)) { - allPlayers.add(player); - playerIds.add(player.id); - } - } - } - allPlayers.sort((a, b) => a.name.compareTo(b.name)); return allPlayers; } From 8bd251ac7d09786c4b1527d5feb9322a3ec60534 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Thu, 5 Mar 2026 22:24:14 +0100 Subject: [PATCH 31/54] Fixed match result view with new db --- .../views/main_menu/match_view/match_result_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 820dcca..3d4587e 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 @@ -35,7 +35,10 @@ class _MatchResultViewState extends State { @override void initState() { db = Provider.of(context, listen: false); - allPlayers = getAllPlayers(widget.match); + + allPlayers = widget.match.players; + allPlayers.sort((a, b) => a.name.compareTo(b.name)); + if (widget.match.winner != null) { _selectedPlayer = allPlayers.firstWhere( (p) => p.id == widget.match.winner!.id, From b68c570d47c49bb3eb19e9d08c56a0b3124f998d Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 16:49:56 +0100 Subject: [PATCH 32/54] Fixed issues with match.players including group.members, added callback --- .../match_view/match_detail_view.dart | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) 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 6336bd0..4ce66da 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 @@ -19,18 +19,18 @@ import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; class MatchDetailView extends StatefulWidget { /// A view that displays the profile of a match /// - [match]: The match to display - /// - [callback]: Callback to refresh the match list + /// - [onMatchUpdate]: Callback to refresh the match list const MatchDetailView({ super.key, required this.match, - required this.callback, + required this.onMatchUpdate, }); /// The match to display final Match match; /// Callback to refresh the match list - final VoidCallback callback; + final VoidCallback onMatchUpdate; @override State createState() => _MatchDetailViewState(); @@ -41,15 +41,14 @@ class _MatchDetailViewState extends State { late Player? currentWinner; - /// All players who participated in the match - late final List allPlayers; + late Match match; @override void initState() { super.initState(); db = Provider.of(context, listen: false); - allPlayers = _getAllPlayers(); currentWinner = widget.match.winner; + match = widget.match; } @override @@ -90,10 +89,10 @@ class _MatchDetailViewState extends State { ), ).then((confirmed) async { if (confirmed! && context.mounted) { - await db.matchDao.deleteMatch(matchId: widget.match.id); + await db.matchDao.deleteMatch(matchId: match.id); if (!context.mounted) return; Navigator.pop(context); - widget.callback.call(); + widget.onMatchUpdate.call(); } }); }, @@ -121,7 +120,7 @@ class _MatchDetailViewState extends State { ), const SizedBox(height: 10), Text( - widget.match.name, + match.name, style: const TextStyle( fontSize: 28, fontWeight: FontWeight.bold, @@ -131,7 +130,7 @@ class _MatchDetailViewState extends State { ), const SizedBox(height: 5), Text( - '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.match.createdAt)}', + '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(match.createdAt)}', style: const TextStyle( fontSize: 12, color: CustomTheme.textColor, @@ -139,7 +138,7 @@ class _MatchDetailViewState extends State { textAlign: TextAlign.center, ), const SizedBox(height: 10), - if (widget.match.group != null) ...[ + if (match.group != null) ...[ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -147,7 +146,7 @@ class _MatchDetailViewState extends State { const SizedBox(width: 8), Text( // TODO: Update after DB changes - '${widget.match.group!.name} ${widget.match.players != null ? '+ ${widget.match.players!.length}' : ''}', + '${match.group!.name}${getExtraPlayerCount()}', style: const TextStyle(fontWeight: FontWeight.bold), ), ], @@ -163,7 +162,7 @@ class _MatchDetailViewState extends State { crossAxisAlignment: WrapCrossAlignment.start, spacing: 12, runSpacing: 8, - children: allPlayers.map((player) { + children: match.players.map((player) { return TextIconTile( text: player.name, iconEnabled: false, @@ -197,7 +196,7 @@ class _MatchDetailViewState extends State { style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: widget.match.winner != null + color: match.winner != null ? CustomTheme.primaryColor : CustomTheme.textColor, ), @@ -227,8 +226,10 @@ class _MatchDetailViewState extends State { context, adaptivePageRoute( fullscreenDialog: true, - builder: (context) => - CreateMatchView(match: widget.match), + builder: (context) => CreateMatchView( + match: match, + onMatchUpdated: onMatchUpdated, + ), ), ), ), @@ -242,9 +243,9 @@ class _MatchDetailViewState extends State { adaptivePageRoute( fullscreenDialog: true, builder: (context) => MatchResultView( - match: widget.match, + match: match, onWinnerChanged: () { - widget.callback.call(); + widget.onMatchUpdate.call(); setState(() {}); }, ), @@ -261,25 +262,32 @@ class _MatchDetailViewState extends State { ); } - /// Gets all players who participated in the match (from group and individual players) - List _getAllPlayers() { - final List players = []; + /// Counts how many players in the match are not part of the group + /// Returns the count as a string, or an empty string if there is no group + String getExtraPlayerCount() { + int count = 0; - // Add group members if group exists - if (widget.match.group != null) { - players.addAll(widget.match.group!.members); - } + final groupMembers = match.group!.members; + final players = match.players; - // Add individual players - if (widget.match.players != null) { - for (var player in widget.match.players!) { - // Avoid duplicates - if (!players.any((p) => p.id == player.id)) { - players.add(player); - } + for (var player in players) { + if (!groupMembers.any((member) => member.id == player.id)) { + count++; } } - return players; + if (count == 0) { + return ''; + } + return ' + ${count.toString()}'; + } + + /// Callback for when the match is updated in the edit view, + /// updates the match in this view + onMatchUpdated(editedMatch) { + setState(() { + match = editedMatch; + }); + widget.onMatchUpdate.call(); } } From f07532c1e2fbb48dc793294e393c6ccf81acc902 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 16:51:01 +0100 Subject: [PATCH 33/54] Fix: Added correct localizations --- lib/l10n/arb/app_de.arb | 3 +-- lib/l10n/arb/app_en.arb | 4 ++++ lib/l10n/generated/app_localizations.dart | 6 ++++++ lib/l10n/generated/app_localizations_de.dart | 5 ++++- lib/l10n/generated/app_localizations_en.dart | 3 +++ 5 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 1995e19..cec565c 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -23,10 +23,9 @@ "delete": "Löschen", "delete_all_data": "Alle Daten löschen", "delete_group": "Diese Gruppe löschen", - "edit_group": "Gruppe bearbeiten", - "delete_group": "Gruppe löschen", "delete_match": "Spiel löschen", "edit_group": "Gruppe bearbeiten", + "edit_match": "Gruppe bearbeiten", "enter_results": "Ergebnisse eintragen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index c56d5de..aea47f7 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -80,6 +80,9 @@ "@edit_group": { "description": "Button & Appbar label for editing a group" }, + "@edit_match": { + "description": "Button & Appbar label for editing a match" + }, "@enter_results": { "description": "Button text to enter match results" }, @@ -347,6 +350,7 @@ "delete_group": "Delete Group", "delete_match": "Delete Match", "edit_group": "Edit Group", + "edit_match": "Edit Match", "enter_results": "Enter Results", "error_creating_group": "Error while creating group, please try again", "error_deleting_group": "Error while deleting group, please try again", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 7dd5eb3..586ac30 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -248,6 +248,12 @@ abstract class AppLocalizations { /// **'Edit Group'** String get edit_group; + /// No description provided for @edit_match. + /// + /// In en, this message translates to: + /// **'Edit Match'** + String get edit_match; + /// Button text to enter match results /// /// 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 bf7a628..501f9c6 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -79,7 +79,7 @@ class AppLocalizationsDe extends AppLocalizations { String get delete_all_data => 'Alle Daten löschen'; @override - String get delete_group => 'Gruppe löschen'; + String get delete_group => 'Diese Gruppe löschen'; @override String get delete_match => 'Spiel löschen'; @@ -87,6 +87,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get edit_group => 'Gruppe bearbeiten'; + @override + String get edit_match => 'Gruppe bearbeiten'; + @override String get enter_results => 'Ergebnisse eintragen'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 348b37e..cdebc69 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -87,6 +87,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get edit_group => 'Edit Group'; + @override + String get edit_match => 'Edit Match'; + @override String get enter_results => 'Enter Results'; From cff95aff00bfb18aee1ce0089102d291715388ae Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 16:51:26 +0100 Subject: [PATCH 34/54] Updated view aligning with new database --- .../create_match/create_match_view.dart | 255 ++++++++++++------ 1 file changed, 177 insertions(+), 78 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index e8ba856..ef843e9 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -21,11 +21,19 @@ import 'package:tallee/presentation/widgets/tiles/choose_tile.dart'; class CreateMatchView extends StatefulWidget { /// A view that allows creating a new match /// [onWinnerChanged]: Optional callback invoked when the winner is changed - const CreateMatchView({super.key, this.onWinnerChanged, this.match}); + const CreateMatchView({ + super.key, + this.onWinnerChanged, + this.match, + this.onMatchUpdated, + }); /// Optional callback invoked when the winner is changed final VoidCallback? onWinnerChanged; + /// Optional callback invoked when the match is updated + final void Function(Match)? onMatchUpdated; + /// An optional match to prefill the fields final Match? match; @@ -52,15 +60,11 @@ class _CreateMatchViewState extends State { /// If a group is selected, this list contains all players from [playerList] /// who are not members of the selected group. If no group is selected, /// this list is identical to [playerList]. - List filteredPlayerList = []; + /*List filteredPlayerList = [];*/ /// The currently selected group Group? selectedGroup; - /// The index of the currently selected group in [groupsList] to mark it in - /// the [ChooseGroupView] - String selectedGroupId = ''; - /// The index of the currently selected game in [games] to mark it in /// the [ChooseGameView] int selectedGameIndex = -1; @@ -86,24 +90,12 @@ class _CreateMatchViewState extends State { ]).then((result) async { groupsList = result[0] as List; playerList = result[1] as List; - setState(() { - filteredPlayerList = List.from(playerList); - }); - }); - // If a match is provided, prefill the fields - if (widget.match != null) { - final match = widget.match!; - _matchNameController.text = match.name; - selectedGroup = match.group; - selectedGroupId = match.group?.id ?? ''; - selectedPlayers = match.players ?? []; - if (selectedGroup != null) { - filteredPlayerList = playerList - .where((p) => !selectedGroup!.members.any((m) => m.id == p.id)) - .toList(); + // If a match is provided, prefill the fields + if (widget.match != null) { + prefillMatchDetails(); } - } + }); } @override @@ -130,13 +122,16 @@ class _CreateMatchViewState extends State { final buttonText = widget.match != null ? loc.save_changes : loc.create_match; + final viewTitle = widget.match != null + ? loc.edit_match + : loc.create_new_match; return ScaffoldMessenger( key: _scaffoldMessengerKey, child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar(title: Text(loc.create_new_match)), + appBar: AppBar(title: Text(viewTitle)), body: SafeArea( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -178,36 +173,43 @@ class _CreateMatchViewState extends State { ? loc.none_group : selectedGroup!.name, onPressed: () async { + // Remove all players from the previously selected group from + // the selected players list, in case the user deselects the + // group or selects a different group. + selectedPlayers.removeWhere( + (player) => + selectedGroup?.members.any( + (member) => member.id == player.id, + ) ?? + false, + ); + selectedGroup = await Navigator.of(context).push( adaptivePageRoute( builder: (context) => ChooseGroupView( groups: groupsList, - initialGroupId: selectedGroupId, + initialGroupId: selectedGroup?.id ?? '', ), ), ); - selectedGroupId = selectedGroup?.id ?? ''; - if (selectedGroup != null) { - filteredPlayerList = playerList - .where( - (p) => - !selectedGroup!.members.any((m) => m.id == p.id), - ) - .toList(); - } else { - filteredPlayerList = List.from(playerList); - } - setState(() {}); + + setState(() { + if (selectedGroup != null) { + setState(() { + selectedPlayers = [...selectedGroup!.members]; + }); + } + }); }, ), Expanded( child: PlayerSelection( key: ValueKey(selectedGroup?.id ?? 'no_group'), initialSelectedPlayers: selectedPlayers, - availablePlayers: filteredPlayerList, onChanged: (value) { setState(() { selectedPlayers = value; + removeGroupWhenNoMemberLeft(); }); }, ), @@ -235,51 +237,22 @@ class _CreateMatchViewState extends State { /// - A ruleset is selected AND /// - Either a group is selected OR at least 2 players are selected bool _enableCreateGameButton() { - return (selectedGroup != null || - (selectedPlayers.length > 1)); + return (selectedGroup != null || (selectedPlayers.length > 1)); } + // If a match was provied to the view, it updates the match in the database + // and navigates back to the previous screen. + // If no match was provided, it creates a new match in the database and + // navigates to the MatchResultView for the newly created match. void buttonNavigation(BuildContext context) async { - // Use a game from the games list - Game? gameToUse; - if (selectedGameIndex == -1) { - // Use the first game as default if none selected - final selectedGame = games[0]; - gameToUse = Game( - name: selectedGame.$1, - description: selectedGame.$2, - ruleset: selectedGame.$3, - color: GameColor.blue, - icon: '', - ); - } else { - // Use the selected game from the list - final selectedGame = games[selectedGameIndex]; - gameToUse = Game( - name: selectedGame.$1, - description: selectedGame.$2, - ruleset: selectedGame.$3, - color: GameColor.blue, - icon: '', - ); - } - // Add the game to the database if it doesn't exist - await db.gameDao.addGame(game: gameToUse); - if (widget.match != null) { - // TODO: Implement updating match logic here - Navigator.pop(context); + await updateMatch(); + if (context.mounted) { + Navigator.pop(context); + } } else { - Match match = Match( - name: _matchNameController.text.isEmpty - ? (hintText ?? '') - : _matchNameController.text.trim(), - createdAt: DateTime.now(), - group: selectedGroup, - players: selectedPlayers, - game: gameToUse - ); - await db.matchDao.addMatch(match: match); + final match = await createMatch(); + if (context.mounted) { Navigator.pushReplacement( context, @@ -294,4 +267,130 @@ class _CreateMatchViewState extends State { } } } -} \ No newline at end of file + + /// Updates attributes of the existing match in the database based on the + /// changes made in the edit view. + Future updateMatch() async { + //TODO: Remove when Games implemented + final tempGame = await getTemporaryGame(); + + final updatedMatch = Match( + id: widget.match!.id, + name: _matchNameController.text.isEmpty + ? (hintText ?? '') + : _matchNameController.text.trim(), + group: selectedGroup, + players: selectedPlayers, + game: tempGame, + ); + + if (widget.match!.name != updatedMatch.name) { + await db.matchDao.updateMatchName( + matchId: widget.match!.id, + newName: updatedMatch.name, + ); + } + + if (widget.match!.group?.id != updatedMatch.group?.id) { + await db.matchDao.updateMatchGroup( + matchId: widget.match!.id, + newGroupId: updatedMatch.group?.id, + ); + } + + // Add players who are in updatedMatch but not in the original match + for (var player in updatedMatch.players) { + if (!widget.match!.players.any((p) => p.id == player.id)) { + await db.playerMatchDao.addPlayerToMatch( + matchId: widget.match!.id, + playerId: player.id, + ); + } + } + + // Remove players who are in the original match but not in updatedMatch + for (var player in widget.match!.players) { + if (!updatedMatch.players.any((p) => p.id == player.id)) { + await db.playerMatchDao.removePlayerFromMatch( + matchId: widget.match!.id, + playerId: player.id, + ); + } + } + + widget.onMatchUpdated?.call(updatedMatch); + } + + Future createMatch() async { + final tempGame = await getTemporaryGame(); + + Match match = Match( + name: _matchNameController.text.isEmpty + ? (hintText ?? '') + : _matchNameController.text.trim(), + createdAt: DateTime.now(), + group: selectedGroup, + players: selectedPlayers, + game: tempGame, + ); + await db.matchDao.addMatch(match: match); + return match; + } + + // TODO: Remove when games fully implemented + Future getTemporaryGame() async { + Game? game; + + // No game is selected + if (selectedGameIndex == -1) { + // Use the first game as default if none selected + final selectedGame = games[0]; + game = Game( + name: selectedGame.$1, + description: selectedGame.$2, + ruleset: selectedGame.$3, + color: GameColor.blue, + icon: '', + ); + } else { + // Use the selected game from the list + final selectedGame = games[selectedGameIndex]; + game = Game( + name: selectedGame.$1, + description: selectedGame.$2, + ruleset: selectedGame.$3, + color: GameColor.blue, + icon: '', + ); + } + // Add the game to the database if it doesn't exist + await db.gameDao.addGame(game: game); + return game; + } + + // If a match was provided to the view, this method prefills the input fields + void prefillMatchDetails() { + final match = widget.match!; + _matchNameController.text = match.name; + selectedPlayers = match.players; + + if (match.group != null) { + selectedGroup = match.group; + } + } + + // If none of the selected players are from the currently selected group, + // the group is also deselected. + Future removeGroupWhenNoMemberLeft() async { + if (selectedGroup == null) return; + + if (!selectedPlayers.any( + (player) => + selectedGroup!.members.any((member) => member.id == player.id), + )) { + setState(() { + selectedGroup = null; + }); + } + } +} From 598b8d0f9e66a02d035643776d50714a7baffcdc Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 20:59:04 +0100 Subject: [PATCH 35/54] Updated player receiving logic --- .../widgets/tiles/match_tile.dart | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index ddc7887..6ac382a 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -40,12 +40,13 @@ class MatchTile extends StatefulWidget { } class _MatchTileState extends State { - late final List _allPlayers; + late List _allPlayers; @override void initState() { super.initState(); - _allPlayers = _getCombinedPlayers(); + _allPlayers = [...widget.match.players]; + _allPlayers.sort((a, b) => a.name.compareTo(b.name)); } @override @@ -93,7 +94,7 @@ class _MatchTileState extends State { const SizedBox(width: 6), Expanded( child: Text( - '${group.name} + ${widget.match.players.length}', + '${group.name}${getExtraPlayerCount()}', style: const TextStyle(fontSize: 14, color: Colors.grey), overflow: TextOverflow.ellipsis, ), @@ -236,21 +237,23 @@ class _MatchTileState extends State { } } - /// Retrieves all unique players associated with the match, - /// combining players from both the match and its group. - List _getCombinedPlayers() { - final allPlayers = []; - final playerIds = {}; + /// Counts how many players in the match are not part of the group + /// Returns the count as a string, or an empty string if there is no group + String getExtraPlayerCount() { + int count = 0; - // Add players from game.players - for (var player in widget.match.players) { - if (!playerIds.contains(player.id)) { - allPlayers.add(player); - playerIds.add(player.id); + final groupMembers = widget.match.group!.members; + final players = widget.match.players; + + for (var player in players) { + if (!groupMembers.any((member) => member.id == player.id)) { + count++; } } - allPlayers.sort((a, b) => a.name.compareTo(b.name)); - return allPlayers; + if (count == 0) { + return ''; + } + return ' + ${count.toString()}'; } } From 688a8a1706a1f75fdc0d591475455151adec176b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 20:59:13 +0100 Subject: [PATCH 36/54] Added comment --- lib/l10n/generated/app_localizations.dart | 2 +- .../main_menu/match_view/create_match/create_match_view.dart | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index 586ac30..eb8a609 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -248,7 +248,7 @@ abstract class AppLocalizations { /// **'Edit Group'** String get edit_group; - /// No description provided for @edit_match. + /// Button & Appbar label for editing a match /// /// In en, this message translates to: /// **'Edit Match'** diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index ef843e9..3217669 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -321,6 +321,8 @@ class _CreateMatchViewState extends State { widget.onMatchUpdated?.call(updatedMatch); } + // Creates a new match and adds it to the database. + // Returns the created match. Future createMatch() async { final tempGame = await getTemporaryGame(); From a8ade294b574e5348a9a3b31324daf9d3cfa3664 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 21:53:46 +0100 Subject: [PATCH 37/54] Added common.dart --- lib/core/common.dart | 45 +++++++++++++++++++ lib/core/enums.dart | 28 +++--------- .../create_match/choose_game_view.dart | 1 + .../match_view/match_detail_view.dart | 23 +--------- .../widgets/tiles/match_tile.dart | 30 +++---------- 5 files changed, 61 insertions(+), 66 deletions(-) create mode 100644 lib/core/common.dart diff --git a/lib/core/common.dart b/lib/core/common.dart new file mode 100644 index 0000000..a27daf0 --- /dev/null +++ b/lib/core/common.dart @@ -0,0 +1,45 @@ +import 'package:flutter/cupertino.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/dto/match.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; + +/// Translates a [Ruleset] enum value to its corresponding localized string. +String translateRulesetToString(Ruleset ruleset, BuildContext context) { + final loc = AppLocalizations.of(context); + switch (ruleset) { + case Ruleset.highestScore: + return loc.highest_score; + case Ruleset.lowestScore: + return loc.lowest_score; + case Ruleset.singleWinner: + return loc.single_winner; + case Ruleset.singleLoser: + return loc.single_loser; + case Ruleset.multipleWinners: + return loc.multiple_winners; + } +} + +/// Counts how many players in the match are not part of the group +/// Returns the count as a string, or an empty string if there is no group +String getExtraPlayerCount(Match match) { + int count = 0; + + if (match.group == null) { + return ''; + } + + final groupMembers = match.group!.members; + final players = match.players; + + for (var player in players) { + if (!groupMembers.any((member) => member.id == player.id)) { + count++; + } + } + + if (count == 0) { + return ''; + } + return ' + ${count.toString()}'; +} diff --git a/lib/core/enums.dart b/lib/core/enums.dart index d3e0610..6b33124 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:tallee/l10n/generated/app_localizations.dart'; - /// Button types used for styling the [CustomWidthButton] /// - [ButtonType.primary]: Primary button style. /// - [ButtonType.secondary]: Secondary button style. @@ -35,7 +32,13 @@ enum ExportResult { success, canceled, unknownException } /// - [Ruleset.singleWinner]: The match is won by a single player. /// - [Ruleset.singleLoser]: The match has a single loser. /// - [Ruleset.multipleWinners]: Multiple players can be winners. -enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWinners } +enum Ruleset { + highestScore, + lowestScore, + singleWinner, + singleLoser, + multipleWinners, +} /// Different colors available for games /// - [GameColor.red]: Red color @@ -47,20 +50,3 @@ enum Ruleset { highestScore, lowestScore, singleWinner, singleLoser, multipleWin /// - [GameColor.pink]: Pink color /// - [GameColor.teal]: Teal color enum GameColor { red, blue, green, yellow, purple, orange, pink, teal } - -/// Translates a [Ruleset] enum value to its corresponding localized string. -String translateRulesetToString(Ruleset ruleset, BuildContext context) { - final loc = AppLocalizations.of(context); - switch (ruleset) { - case Ruleset.highestScore: - return loc.highest_score; - case Ruleset.lowestScore: - return loc.lowest_score; - case Ruleset.singleWinner: - return loc.single_winner; - case Ruleset.singleLoser: - return loc.single_loser; - case Ruleset.multipleWinners: - return loc.multiple_winners; - } -} diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart index 447b9c5..d4d7f4d 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_game_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; 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 4ce66da..d5791a5 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 @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:tallee/core/adaptive_page_route.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/match.dart'; @@ -146,7 +147,7 @@ class _MatchDetailViewState extends State { const SizedBox(width: 8), Text( // TODO: Update after DB changes - '${match.group!.name}${getExtraPlayerCount()}', + '${match.group!.name}${getExtraPlayerCount(match)}', style: const TextStyle(fontWeight: FontWeight.bold), ), ], @@ -262,26 +263,6 @@ class _MatchDetailViewState extends State { ); } - /// Counts how many players in the match are not part of the group - /// Returns the count as a string, or an empty string if there is no group - String getExtraPlayerCount() { - int count = 0; - - final groupMembers = match.group!.members; - final players = match.players; - - for (var player in players) { - if (!groupMembers.any((member) => member.id == player.id)) { - count++; - } - } - - if (count == 0) { - return ''; - } - return ' + ${count.toString()}'; - } - /// Callback for when the match is updated in the edit view, /// updates the match in this view onMatchUpdated(editedMatch) { diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 6ac382a..2f990cb 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -2,6 +2,7 @@ import 'dart:core' hide Match; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/dto/match.dart'; import 'package:tallee/data/dto/player.dart'; @@ -51,6 +52,7 @@ class _MatchTileState extends State { @override Widget build(BuildContext context) { + final match = widget.match; final group = widget.match.group; final winner = widget.match.winner; final loc = AppLocalizations.of(context); @@ -70,7 +72,7 @@ class _MatchTileState extends State { children: [ Expanded( child: Text( - widget.match.name, + match.name, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, @@ -79,7 +81,7 @@ class _MatchTileState extends State { ), ), Text( - _formatDate(widget.match.createdAt, context), + _formatDate(match.createdAt, context), style: const TextStyle(fontSize: 12, color: Colors.grey), ), ], @@ -94,7 +96,7 @@ class _MatchTileState extends State { const SizedBox(width: 6), Expanded( child: Text( - '${group.name}${getExtraPlayerCount()}', + '${match.group!.name}${getExtraPlayerCount(match)}', style: const TextStyle(fontSize: 14, color: Colors.grey), overflow: TextOverflow.ellipsis, ), @@ -109,7 +111,7 @@ class _MatchTileState extends State { const SizedBox(width: 6), Expanded( child: Text( - '${widget.match.players.length} ${loc.players}', + '${match.players.length} ${loc.players}', style: const TextStyle(fontSize: 14, color: Colors.grey), overflow: TextOverflow.ellipsis, ), @@ -236,24 +238,4 @@ class _MatchTileState extends State { return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}'; } } - - /// Counts how many players in the match are not part of the group - /// Returns the count as a string, or an empty string if there is no group - String getExtraPlayerCount() { - int count = 0; - - final groupMembers = widget.match.group!.members; - final players = widget.match.players; - - for (var player in players) { - if (!groupMembers.any((member) => member.id == player.id)) { - count++; - } - } - - if (count == 0) { - return ''; - } - return ' + ${count.toString()}'; - } } From 8e4fe26ad9878054d5a26a9de9acbadb065f7db4 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 22:09:58 +0100 Subject: [PATCH 38/54] Updated theme --- lib/core/custom_theme.dart | 24 ++++++++++++++++++- lib/main.dart | 22 ++++++++--------- .../widgets/text_input/custom_search_bar.dart | 1 - 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 0e9fec2..d1b158e 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -27,6 +27,9 @@ class CustomTheme { /// Text color used throughout the app static const Color textColor = Color(0xFFFFFFFF); + /// Text color used throughout the app + static const Color hintColor = Color(0xFF888888); + /// Background color for the navigation bar static const Color navBarBackgroundColor = Color(0xFF131313); @@ -65,7 +68,7 @@ class CustomTheme { boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)], ); - // ==================== App Bar Theme ==================== + // ==================== Component Themes ==================== static const AppBarTheme appBarTheme = AppBarTheme( backgroundColor: backgroundColor, foregroundColor: textColor, @@ -80,4 +83,23 @@ class CustomTheme { ), iconTheme: IconThemeData(color: textColor), ); + + static const SearchBarThemeData searchBarTheme = SearchBarThemeData( + textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)), + hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)), + ); + + static final RadioThemeData radioTheme = RadioThemeData( + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return CustomTheme.primaryColor; + } + return CustomTheme.textColor; + }), + ); + + static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme( + labelStyle: TextStyle(color: CustomTheme.textColor), + hintStyle: TextStyle(color: CustomTheme.hintColor), + ); } diff --git a/lib/main.dart b/lib/main.dart index f5394b8..f159ef7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,25 +29,25 @@ class GameTracker extends StatelessWidget { return supportedLocale; } } - return supportedLocales.firstWhere((locale) => locale.languageCode == 'en'); + return supportedLocales.firstWhere( + (locale) => locale.languageCode == 'en', + ); }, debugShowCheckedModeBanner: false, onGenerateTitle: (context) => AppLocalizations.of(context).app_name, - themeMode: ThemeMode.dark, // forces dark mode + themeMode: ThemeMode.dark, theme: ThemeData( + // main colors primaryColor: CustomTheme.primaryColor, scaffoldBackgroundColor: CustomTheme.backgroundColor, + // themes appBarTheme: CustomTheme.appBarTheme, - radioTheme: RadioThemeData( - fillColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return CustomTheme.primaryColor; - } - return CustomTheme.textColor; - }), - ), + inputDecorationTheme: CustomTheme.inputDecorationTheme, + searchBarTheme: CustomTheme.searchBarTheme, + radioTheme: CustomTheme.radioTheme, + // color scheme colorScheme: ColorScheme.fromSeed( - seedColor: CustomTheme.primaryColor, + seedColor: CustomTheme.textColor, brightness: Brightness.dark, primary: CustomTheme.primaryColor, onPrimary: CustomTheme.textColor, diff --git a/lib/presentation/widgets/text_input/custom_search_bar.dart b/lib/presentation/widgets/text_input/custom_search_bar.dart index 313fc1a..e5fc498 100644 --- a/lib/presentation/widgets/text_input/custom_search_bar.dart +++ b/lib/presentation/widgets/text_input/custom_search_bar.dart @@ -69,7 +69,6 @@ class CustomSearchBar extends StatelessWidget { constraints ?? const BoxConstraints(maxHeight: 45, minHeight: 45), hintText: hintText, onChanged: onChanged, - hintStyle: WidgetStateProperty.all(const TextStyle(fontSize: 16)), leading: const Icon(Icons.search), trailing: [ Visibility( From e909f347e316d2a9b44d64695f1fce52d184feae Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 22:11:15 +0100 Subject: [PATCH 39/54] Removed unused function --- .../main_menu/match_view/match_result_view.dart | 17 ----------------- 1 file changed, 17 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 3d4587e..8a46f13 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 @@ -148,21 +148,4 @@ class _MatchResultViewState extends State { } widget.onWinnerChanged?.call(); } - - /// Retrieves all players associated with the given [match]. - /// This includes players directly assigned to the match - /// as well as members of the group (if any). - /// The returned list is sorted alphabetically by player name. - List getAllPlayers(Match match) { - List players = []; - - if (match.group == null) { - players = [...match.players]; - } else { - players = [...match.players, ...match.group!.members]; - } - - players.sort((a, b) => a.name.compareTo(b.name)); - return players; - } } From 2bd5c30094959da41f2bf35ee7e5bcb03b268750 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Fri, 6 Mar 2026 22:21:07 +0100 Subject: [PATCH 40/54] Fix: Setting winner --- .../views/main_menu/match_view/match_detail_view.dart | 1 + 1 file changed, 1 insertion(+) 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 d5791a5..ced982b 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 @@ -252,6 +252,7 @@ class _MatchDetailViewState extends State { ), ), ); + match.winner = currentWinner; }, ), ], From 664af7ffee55e9d64f13727f0dbe261a62d9a5d2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 16:47:48 +0100 Subject: [PATCH 41/54] Updated dependencies --- ios/Flutter/AppFrameworkInfo.plist | 2 -- ios/Runner/AppDelegate.swift | 7 +++++-- ios/Runner/Info.plist | 21 +++++++++++++++++++ .../main_menu/custom_navigation_bar.dart | 2 +- .../match_view/match_detail_view.dart | 2 +- lib/presentation/widgets/app_skeleton.dart | 5 +---- pubspec.yaml | 2 +- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..391a902 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 6266644..c30b367 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 7e79382..b320936 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -31,6 +31,27 @@ LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + UIApplicationSupportsIndirectInputEvents UILaunchStoryboardName diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index 508463d..16316ad 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -141,7 +141,7 @@ class _CustomNavigationBarState extends State } /// Returns the title of the current tab based on [currentIndex]. - String _currentTabTitle(context) { + String _currentTabTitle(BuildContext context) { final loc = AppLocalizations.of(context); switch (currentIndex) { case 0: 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 ced982b..57ec2ed 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 @@ -266,7 +266,7 @@ class _MatchDetailViewState extends State { /// Callback for when the match is updated in the edit view, /// updates the match in this view - onMatchUpdated(editedMatch) { + void onMatchUpdated(Match editedMatch) { setState(() { match = editedMatch; }); diff --git a/lib/presentation/widgets/app_skeleton.dart b/lib/presentation/widgets/app_skeleton.dart index 98f2ca7..8a21320 100644 --- a/lib/presentation/widgets/app_skeleton.dart +++ b/lib/presentation/widgets/app_skeleton.dart @@ -47,10 +47,7 @@ class _AppSkeletonState extends State { : (Widget? currentChild, List previousChildren) { return Stack( alignment: Alignment.topCenter, - children: [ - ...previousChildren, - if (currentChild != null) currentChild, - ], + children: [...previousChildren, ?currentChild], ); }, ), diff --git a/pubspec.yaml b/pubspec.yaml index e13f0cc..6192380 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,7 @@ dev_dependencies: build_runner: ^2.5.4 dart_pubspec_licenses: ^3.0.14 drift_dev: ^2.27.0 - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true From 4bcb10df8157dcacc6bf7983eab4731a59136b90 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 17:03:52 +0100 Subject: [PATCH 42/54] Fix: Winner gets resetted, if player gets removed from the game --- .../create_match/create_match_view.dart | 3 +++ .../main_menu/match_view/match_detail_view.dart | 16 +++++----------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 3217669..314b37c 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -315,6 +315,9 @@ class _CreateMatchViewState extends State { matchId: widget.match!.id, playerId: player.id, ); + if (widget.match!.winner?.id == player.id) { + updatedMatch.winner = null; + } } } 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 57ec2ed..451a08b 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 @@ -40,15 +40,12 @@ class MatchDetailView extends StatefulWidget { class _MatchDetailViewState extends State { late final AppDatabase db; - late Player? currentWinner; - late Match match; @override void initState() { super.initState(); db = Provider.of(context, listen: false); - currentWinner = widget.match.winner; match = widget.match; } @@ -184,7 +181,7 @@ class _MatchDetailViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ /// TODO: Implement different ruleset results display - if (currentWinner != null) ...[ + if (match.winner != null) ...[ Text( loc.winner, style: const TextStyle( @@ -193,13 +190,11 @@ class _MatchDetailViewState extends State { ), ), Text( - currentWinner!.name, - style: TextStyle( + match.winner!.name, + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, - color: match.winner != null - ? CustomTheme.primaryColor - : CustomTheme.textColor, + color: CustomTheme.primaryColor, ), ), ] else ...[ @@ -239,7 +234,7 @@ class _MatchDetailViewState extends State { text: loc.enter_results, icon: Icons.emoji_events, onPressed: () async { - currentWinner = await Navigator.push( + match.winner = await Navigator.push( context, adaptivePageRoute( fullscreenDialog: true, @@ -252,7 +247,6 @@ class _MatchDetailViewState extends State { ), ), ); - match.winner = currentWinner; }, ), ], From 4c479676d288b7e8616df708d626565d602ef044 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 17:38:32 +0100 Subject: [PATCH 43/54] Added properties --- .../main_menu/match_view/create_match/create_match_view.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 314b37c..3205883 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -282,6 +282,10 @@ class _CreateMatchViewState extends State { group: selectedGroup, players: selectedPlayers, game: tempGame, + winner: widget.match!.winner, + createdAt: widget.match!.createdAt, + endedAt: widget.match!.endedAt, + notes: widget.match!.notes, ); if (widget.match!.name != updatedMatch.name) { From 90cc9587ca8c835083d9d57386b653934cf969b0 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 17:43:42 +0100 Subject: [PATCH 44/54] Removed import --- .../views/main_menu/match_view/match_detail_view.dart | 1 - 1 file changed, 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 451a08b..b4ef952 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 @@ -6,7 +6,6 @@ import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/dto/match.dart'; -import 'package:tallee/data/dto/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; From 0b118800e4fb0f0f7f1910d019d3c9969e5f657e Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 21:33:51 +0100 Subject: [PATCH 45/54] Fixed issue with group replacing solo players --- .../main_menu/match_view/create_match/create_match_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 3205883..84087e2 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -196,7 +196,7 @@ class _CreateMatchViewState extends State { setState(() { if (selectedGroup != null) { setState(() { - selectedPlayers = [...selectedGroup!.members]; + selectedPlayers += [...selectedGroup!.members]; }); } }); From 0822039a5f031574e77c6d1568be82906c55414a Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 21:50:25 +0100 Subject: [PATCH 46/54] Fix: Not updated match view after updating matches players --- .../views/main_menu/match_view/match_view.dart | 2 +- lib/presentation/widgets/tiles/match_tile.dart | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index e5a4e29..8cca4a0 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -144,7 +144,7 @@ class _MatchViewState extends State { if (mounted) { setState(() { final loadedMatches = results[0] as List; - matches = loadedMatches + matches = [...loadedMatches] ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); isLoading = false; }); diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 2f990cb..7231787 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -5,7 +5,6 @@ import 'package:intl/intl.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/data/dto/match.dart'; -import 'package:tallee/data/dto/player.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; @@ -41,20 +40,13 @@ class MatchTile extends StatefulWidget { } class _MatchTileState extends State { - late List _allPlayers; - - @override - void initState() { - super.initState(); - _allPlayers = [...widget.match.players]; - _allPlayers.sort((a, b) => a.name.compareTo(b.name)); - } - @override Widget build(BuildContext context) { final match = widget.match; final group = widget.match.group; final winner = widget.match.winner; + final players = [...widget.match.players] + ..sort((a, b) => a.name.compareTo(b.name)); final loc = AppLocalizations.of(context); return GestureDetector( @@ -197,7 +189,7 @@ class _MatchTileState extends State { const SizedBox(height: 12), ], - if (_allPlayers.isNotEmpty && widget.compact == false) ...[ + if (players.isNotEmpty && widget.compact == false) ...[ Text( loc.players, style: const TextStyle( @@ -210,7 +202,7 @@ class _MatchTileState extends State { Wrap( spacing: 6, runSpacing: 6, - children: _allPlayers.map((player) { + children: players.map((player) { return TextIconTile(text: player.name, iconEnabled: false); }).toList(), ), From 27c6d8b29385f7fc7da4a4f98bc76f4d7853fc40 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 21:57:51 +0100 Subject: [PATCH 47/54] Small corrections --- .../views/main_menu/match_view/match_view.dart | 10 +++++----- lib/presentation/widgets/tiles/match_tile.dart | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 8cca4a0..a090b46 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -58,7 +58,7 @@ class _MatchViewState extends State { void initState() { super.initState(); db = Provider.of(context, listen: false); - loadGames(); + loadMatches(); } @override @@ -100,7 +100,7 @@ class _MatchViewState extends State { adaptivePageRoute( builder: (context) => MatchDetailView( match: matches[index], - onMatchUpdate: loadGames, + onMatchUpdate: loadMatches, ), ), ); @@ -123,7 +123,7 @@ class _MatchViewState extends State { context, adaptivePageRoute( builder: (context) => - CreateMatchView(onWinnerChanged: loadGames), + CreateMatchView(onWinnerChanged: loadMatches), ), ); }, @@ -134,8 +134,8 @@ class _MatchViewState extends State { ); } - /// Loads the games from the database and sorts them by creation date. - void loadGames() { + /// Loads the matches from the database and sorts them by creation date. + void loadMatches() { isLoading = true; Future.wait([ db.matchDao.getAllMatches(), diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 7231787..3c36587 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -43,9 +43,9 @@ class _MatchTileState extends State { @override Widget build(BuildContext context) { final match = widget.match; - final group = widget.match.group; - final winner = widget.match.winner; - final players = [...widget.match.players] + final group = match.group; + final winner = match.winner; + final players = [...match.players] ..sort((a, b) => a.name.compareTo(b.name)); final loc = AppLocalizations.of(context); From 7810443a002ac178ba6acecf1702d0d02041337b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:11:51 +0100 Subject: [PATCH 48/54] Fix: SetState Error --- .../widgets/player_selection.dart | 88 ++++++++++--------- 1 file changed, 47 insertions(+), 41 deletions(-) diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index cc3a68c..b51cadc 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -110,7 +110,9 @@ class _PlayerSelectionState extends State { final bool nameMatches = player.name.toLowerCase().contains( value.toLowerCase(), ); - final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id); + final bool isNotSelected = !selectedPlayers.any( + (p) => p.id == player.id, + ); return nameMatches && isNotSelected; }).toList(); } @@ -224,49 +226,53 @@ class _PlayerSelectionState extends State { db.playerDao.getAllPlayers(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), ]).then((results) => results[0] as List); - if (mounted) { - _allPlayersFuture.then((loadedPlayers) { - setState(() { - // If a list of available players is provided (even if empty), use that list. - if (widget.availablePlayers != null) { - widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name)); - allPlayers = [...widget.availablePlayers!]; - suggestedPlayers = [...allPlayers]; - if (widget.initialSelectedPlayers != null) { - // Ensures that only players available for selection are pre-selected. - selectedPlayers = widget.initialSelectedPlayers! - .where( - (p) => widget.availablePlayers!.any( - (available) => available.id == p.id, - ), - ) - .toList(); - } - } else { - // Otherwise, use the loaded players from the database. - loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); - allPlayers = [...loadedPlayers]; - if (widget.initialSelectedPlayers != null) { - // Excludes already selected players from the suggested players list. - suggestedPlayers = loadedPlayers.where((p) => !widget.initialSelectedPlayers!.any((ip) => ip.id == p.id)).toList(); - // Ensures that only players available for selection are pre-selected. - selectedPlayers = widget.initialSelectedPlayers! - .where( - (p) => allPlayers.any( - (available) => available.id == p.id, - ), - ) - .toList(); - } else { - // If no initial selection, all loaded players are suggested. - suggestedPlayers = [...loadedPlayers]; - } + _allPlayersFuture.then((loadedPlayers) { + if (!mounted) return; + setState(() { + // If a list of available players is provided (even if empty), use that list. + if (widget.availablePlayers != null) { + widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name)); + allPlayers = [...widget.availablePlayers!]; + suggestedPlayers = [...allPlayers]; + + if (widget.initialSelectedPlayers != null) { + // Ensures that only players available for selection are pre-selected. + selectedPlayers = widget.initialSelectedPlayers! + .where( + (p) => widget.availablePlayers!.any( + (available) => available.id == p.id, + ), + ) + .toList(); } - isLoading = false; - }); + } else { + // Otherwise, use the loaded players from the database. + loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); + allPlayers = [...loadedPlayers]; + if (widget.initialSelectedPlayers != null) { + // Excludes already selected players from the suggested players list. + suggestedPlayers = loadedPlayers + .where( + (p) => !widget.initialSelectedPlayers!.any( + (ip) => ip.id == p.id, + ), + ) + .toList(); + // Ensures that only players available for selection are pre-selected. + selectedPlayers = widget.initialSelectedPlayers! + .where( + (p) => allPlayers.any((available) => available.id == p.id), + ) + .toList(); + } else { + // If no initial selection, all loaded players are suggested. + suggestedPlayers = [...loadedPlayers]; + } + } + isLoading = false; }); - } + }); } /// Adds a new player to the database from the search bar input. From 16aecffdbe38768ec5abda402fb181ec3ac3c536 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:43:24 +0100 Subject: [PATCH 49/54] Change var name --- .../create_match/create_match_view.dart | 42 +++++++++---------- .../match_view/match_detail_view.dart | 2 +- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 84087e2..3ae8d05 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -24,7 +24,7 @@ class CreateMatchView extends StatefulWidget { const CreateMatchView({ super.key, this.onWinnerChanged, - this.match, + this.matchToEdit, this.onMatchUpdated, }); @@ -35,7 +35,7 @@ class CreateMatchView extends StatefulWidget { final void Function(Match)? onMatchUpdated; /// An optional match to prefill the fields - final Match? match; + final Match? matchToEdit; @override State createState() => _CreateMatchViewState(); @@ -92,7 +92,7 @@ class _CreateMatchViewState extends State { playerList = result[1] as List; // If a match is provided, prefill the fields - if (widget.match != null) { + if (widget.matchToEdit != null) { prefillMatchDetails(); } }); @@ -119,10 +119,10 @@ class _CreateMatchViewState extends State { @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); - final buttonText = widget.match != null + final buttonText = widget.matchToEdit != null ? loc.save_changes : loc.create_match; - final viewTitle = widget.match != null + final viewTitle = widget.matchToEdit != null ? loc.edit_match : loc.create_new_match; @@ -245,7 +245,7 @@ class _CreateMatchViewState extends State { // If no match was provided, it creates a new match in the database and // navigates to the MatchResultView for the newly created match. void buttonNavigation(BuildContext context) async { - if (widget.match != null) { + if (widget.matchToEdit != null) { await updateMatch(); if (context.mounted) { Navigator.pop(context); @@ -275,51 +275,51 @@ class _CreateMatchViewState extends State { final tempGame = await getTemporaryGame(); final updatedMatch = Match( - id: widget.match!.id, + id: widget.matchToEdit!.id, name: _matchNameController.text.isEmpty ? (hintText ?? '') : _matchNameController.text.trim(), group: selectedGroup, players: selectedPlayers, game: tempGame, - winner: widget.match!.winner, - createdAt: widget.match!.createdAt, - endedAt: widget.match!.endedAt, - notes: widget.match!.notes, + winner: widget.matchToEdit!.winner, + createdAt: widget.matchToEdit!.createdAt, + endedAt: widget.matchToEdit!.endedAt, + notes: widget.matchToEdit!.notes, ); - if (widget.match!.name != updatedMatch.name) { + if (widget.matchToEdit!.name != updatedMatch.name) { await db.matchDao.updateMatchName( - matchId: widget.match!.id, + matchId: widget.matchToEdit!.id, newName: updatedMatch.name, ); } - if (widget.match!.group?.id != updatedMatch.group?.id) { + if (widget.matchToEdit!.group?.id != updatedMatch.group?.id) { await db.matchDao.updateMatchGroup( - matchId: widget.match!.id, + matchId: widget.matchToEdit!.id, newGroupId: updatedMatch.group?.id, ); } // Add players who are in updatedMatch but not in the original match for (var player in updatedMatch.players) { - if (!widget.match!.players.any((p) => p.id == player.id)) { + if (!widget.matchToEdit!.players.any((p) => p.id == player.id)) { await db.playerMatchDao.addPlayerToMatch( - matchId: widget.match!.id, + matchId: widget.matchToEdit!.id, playerId: player.id, ); } } // Remove players who are in the original match but not in updatedMatch - for (var player in widget.match!.players) { + for (var player in widget.matchToEdit!.players) { if (!updatedMatch.players.any((p) => p.id == player.id)) { await db.playerMatchDao.removePlayerFromMatch( - matchId: widget.match!.id, + matchId: widget.matchToEdit!.id, playerId: player.id, ); - if (widget.match!.winner?.id == player.id) { + if (widget.matchToEdit!.winner?.id == player.id) { updatedMatch.winner = null; } } @@ -379,7 +379,7 @@ class _CreateMatchViewState extends State { // If a match was provided to the view, this method prefills the input fields void prefillMatchDetails() { - final match = widget.match!; + final match = widget.matchToEdit!; _matchNameController.text = match.name; selectedPlayers = match.players; 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 b4ef952..c6e30d2 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 @@ -222,7 +222,7 @@ class _MatchDetailViewState extends State { adaptivePageRoute( fullscreenDialog: true, builder: (context) => CreateMatchView( - match: match, + matchToEdit: match, onMatchUpdated: onMatchUpdated, ), ), From a846e4d7eaf72772e1da97da3b78551222c18238 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:44:49 +0100 Subject: [PATCH 50/54] Removed dead code --- .../match_view/create_match/create_match_view.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 3ae8d05..4f1f796 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -56,12 +56,6 @@ class _CreateMatchViewState extends State { /// List of all players from the database List playerList = []; - /// List of players filtered based on the selected group - /// If a group is selected, this list contains all players from [playerList] - /// who are not members of the selected group. If no group is selected, - /// this list is identical to [playerList]. - /*List filteredPlayerList = [];*/ - /// The currently selected group Group? selectedGroup; From 0597b21ac191c6bd8cbd20d6f9ca05e715a575d1 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:45:55 +0100 Subject: [PATCH 51/54] Added condition --- .../main_menu/match_view/create_match/create_match_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 4f1f796..21045b0 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -231,7 +231,8 @@ class _CreateMatchViewState extends State { /// - A ruleset is selected AND /// - Either a group is selected OR at least 2 players are selected bool _enableCreateGameButton() { - return (selectedGroup != null || (selectedPlayers.length > 1)); + return (selectedGroup != null || + (selectedPlayers.length > 1) && selectedGameIndex != -1); } // If a match was provied to the view, it updates the match in the database From 588b5053e864c25e96c26819cf11277bab914fc2 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:46:21 +0100 Subject: [PATCH 52/54] typo --- .../main_menu/match_view/create_match/create_match_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 21045b0..d3f756b 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -235,7 +235,7 @@ class _CreateMatchViewState extends State { (selectedPlayers.length > 1) && selectedGameIndex != -1); } - // If a match was provied to the view, it updates the match in the database + // If a match was provided to the view, it updates the match in the database // and navigates back to the previous screen. // If no match was provided, it creates a new match in the database and // navigates to the MatchResultView for the newly created match. From 9d2b6a0286404091057d65136894d5cfae13e05b Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:47:28 +0100 Subject: [PATCH 53/54] Adapted getTemporaryGame method --- .../create_match/create_match_view.dart | 32 ++++++------------- 1 file changed, 9 insertions(+), 23 deletions(-) diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index d3f756b..1bf732c 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -345,29 +345,15 @@ class _CreateMatchViewState extends State { Future getTemporaryGame() async { Game? game; - // No game is selected - if (selectedGameIndex == -1) { - // Use the first game as default if none selected - final selectedGame = games[0]; - game = Game( - name: selectedGame.$1, - description: selectedGame.$2, - ruleset: selectedGame.$3, - color: GameColor.blue, - icon: '', - ); - } else { - // Use the selected game from the list - final selectedGame = games[selectedGameIndex]; - game = Game( - name: selectedGame.$1, - description: selectedGame.$2, - ruleset: selectedGame.$3, - color: GameColor.blue, - icon: '', - ); - } - // Add the game to the database if it doesn't exist + final selectedGame = games[selectedGameIndex]; + game = Game( + name: selectedGame.$1, + description: selectedGame.$2, + ruleset: selectedGame.$3, + color: GameColor.blue, + icon: '', + ); + await db.gameDao.addGame(game: game); return game; } From 73c8865eb541af25f98467960fb8a52123d83b66 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sat, 7 Mar 2026 22:48:57 +0100 Subject: [PATCH 54/54] Removed todo --- .../views/main_menu/match_view/match_detail_view.dart | 1 - 1 file changed, 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 c6e30d2..1deba18 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 @@ -142,7 +142,6 @@ class _MatchDetailViewState extends State { const Icon(Icons.group), const SizedBox(width: 8), Text( - // TODO: Update after DB changes '${match.group!.name}${getExtraPlayerCount(match)}', style: const TextStyle(fontWeight: FontWeight.bold), ),