From 45a419cae7010c55393e2df0fd95accc3b9b0dbb Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 10 Jan 2026 20:14:37 +0100 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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 3addaa0f9d5bf227a1391135462d0332ac46391e Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sat, 17 Jan 2026 14:22:45 +0100 Subject: [PATCH 10/13] Disable resizing when using keyboard --- .../views/main_menu/group_view/create_group_view.dart | 1 + .../main_menu/match_view/create_match/choose_game_view.dart | 1 + .../main_menu/match_view/create_match/choose_group_view.dart | 1 + .../main_menu/match_view/create_match/create_match_view.dart | 1 + pubspec.yaml | 2 +- 5 files changed, 5 insertions(+), 1 deletion(-) 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 719b47d..b342448 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 @@ -47,6 +47,7 @@ class _CreateGroupViewState extends State { final loc = AppLocalizations.of(context); return ScaffoldMessenger( child: Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_new_group)), body: SafeArea( 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 32868e4..3ff6e79 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 @@ -43,6 +43,7 @@ class _ChooseGameViewState extends State { final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, + resizeToAvoidBottomInset: false, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios), diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index 00a0276..592d765 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -43,6 +43,7 @@ class _ChooseGroupViewState extends State { final loc = AppLocalizations.of(context); return Scaffold( backgroundColor: CustomTheme.backgroundColor, + resizeToAvoidBottomInset: false, appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios), 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 694a82d..5509911 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 @@ -125,6 +125,7 @@ class _CreateMatchViewState extends State { final loc = AppLocalizations.of(context); return ScaffoldMessenger( child: Scaffold( + resizeToAvoidBottomInset: false, backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_new_match)), body: SafeArea( diff --git a/pubspec.yaml b/pubspec.yaml index e9fd894..4d5b4bc 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.8+213 environment: sdk: ^3.8.1 From f1df0678243312d7f49862d7dc179dc39f2c6c65 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 18 Jan 2026 10:51:53 +0100 Subject: [PATCH 11/13] 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 12/13] 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 13/13] 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(); }, ), );