From e4c3bc1c5e2498597469c9bd073a633840ef4f41 Mon Sep 17 00:00:00 2001 From: Mathis Kirchner Date: Sun, 18 Jan 2026 11:41:50 +0100 Subject: [PATCH] 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