diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 12ac4a8..2c18073 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -5,14 +5,32 @@ class CustomTheme { CustomTheme._(); // Private constructor to prevent instantiation // ==================== Colors ==================== + + /// Primary color of the app theme static Color primaryColor = const Color(0xFF7505E4); + + /// Secondary color of the app theme static Color secondaryColor = const Color(0xFFAFA2FF); + + /// Background color of the app theme static Color backgroundColor = const Color(0xFF0B0B0B); + + /// Default color for boxes and containers static Color boxColor = const Color(0xFF101010); - static Color onBoxColor = const Color(0xFF181818); + + /// Default border color for boxes and containers static Color boxBorder = const Color(0xFF272727); + + /// Color for boxes and containers displayed on boxes + static Color onBoxColor = const Color(0xFF181818); + + /// Text color used throughout the app static const Color textColor = Colors.white; + + /// Selected color for the [NavbarItem] static Color navBarItemSelectedColor = primaryColor.withGreen(100); + + /// Unselected color for the [NavbarItem] static Color navBarItemUnselectedColor = Colors.grey.shade400; // ==================== Border Radius ==================== diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 4347fdf..0c85634 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -4,6 +4,7 @@ "all_players_selected": "Alle Spieler:innen ausgewählt", "amount_of_matches": "Anzahl der Spiele", "app_name": "Game Tracker", + "best_player": "Beste:r Spieler:in", "cancel": "Abbrechen", "choose_game": "Spielvorlage wählen", "choose_group": "Gruppe wählen", @@ -13,6 +14,7 @@ "create_match": "Spiel erstellen", "create_new_group": "Neue Gruppe erstellen", "create_new_match": "Neues Spiel erstellen", + "created_on": "Erstellt am", "data": "Daten", "data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_exported": "Daten erfolgreich exportiert", @@ -22,6 +24,8 @@ "delete_all_data": "Alle Daten löschen", "delete_group": "Diese Gruppe löschen", "edit_group": "Gruppe bearbeiten", + "delete_group": "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", @@ -33,6 +37,7 @@ "game_name": "Spielvorlagenname", "group": "Gruppe", "group_name": "Gruppenname", + "group_profile": "Gruppenprofil", "groups": "Gruppen", "home": "Startseite", "import_canceled": "Import abgebrochen", @@ -46,6 +51,7 @@ "match_in_progress": "Spiel läuft...", "match_name": "Spieltitel", "matches": "Spiele", + "members": "Mitglieder", "most_points": "Höchste Punkte", "no_data_available": "Keine Daten verfügbar", "no_groups_created_yet": "Noch keine Gruppen erstellt", @@ -61,6 +67,7 @@ "none": "Kein", "none_group": "Keine", "not_available": "Nicht verfügbar", + "played_matches": "Gespielte Spiele", "player_name": "Spieler:innenname", "players": "Spieler:innen", "players_count": "{count} Spieler", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 2642dcc..6756ec5 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -12,6 +12,9 @@ "@app_name": { "description": "The name of the App" }, + "@best_player": { + "description": "Label for best player statistic" + }, "@cancel": { "description": "Cancel button text" }, @@ -39,6 +42,9 @@ "@create_new_match": { "description": "Appbar text to create a new match" }, + "@created_on": { + "description": "Label for creation date" + }, "@data": { "description": "Data label" }, @@ -104,6 +110,9 @@ "@group_name": { "description": "Placeholder for group name input" }, + "@group_profile": { + "description": "Title for group profile view" + }, "@groups": { "description": "Label for groups" }, @@ -143,6 +152,9 @@ "@matches": { "description": "Label for matches" }, + "@members": { + "description": "Label for group members" + }, "@most_points": { "description": "Title for most points ruleset" }, @@ -188,6 +200,9 @@ "@not_available": { "description": "Abbreviation for not available" }, + "@played_matches": { + "description": "Label for played matches statistic" + }, "@player_name": { "description": "Placeholder for player name input" }, @@ -293,6 +308,7 @@ "all_players_selected": "All players selected", "amount_of_matches": "Amount of Matches", "app_name": "Game Tracker", + "best_player": "Best Player", "cancel": "Cancel", "choose_game": "Choose Game", "choose_group": "Choose Group", @@ -301,6 +317,7 @@ "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", + "created_on": "Created on", "create_new_match": "Create new match", "data": "Data", "data_successfully_deleted": "Data successfully deleted", @@ -309,7 +326,7 @@ "days_ago": "{count} days ago", "delete": "Delete", "delete_all_data": "Delete all data", - "delete_group": "Delete this group", + "delete_group": "Delete 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", @@ -322,6 +339,7 @@ "game_name": "Game Name", "group": "Group", "group_name": "Group name", + "group_profile": "Group Profile", "groups": "Groups", "home": "Home", "import_canceled": "Import canceled", @@ -335,6 +353,7 @@ "match_in_progress": "Match in progress...", "match_name": "Match name", "matches": "Matches", + "members": "Members", "most_points": "Most Points", "no_data_available": "No data available", "no_groups_created_yet": "No groups created yet", @@ -350,6 +369,7 @@ "none": "None", "none_group": "None", "not_available": "Not available", + "played_matches": "Played Matches", "player_name": "Player name", "players": "Players", "players_count": "{count} Players", 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 new file mode 100644 index 0000000..e366834 --- /dev/null +++ b/lib/presentation/views/main_menu/group_view/group_profile_view.dart @@ -0,0 +1,271 @@ +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 dbd4d82..61dbe51 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -6,6 +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/group_profile_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/main_menu_button.dart'; @@ -74,19 +75,22 @@ class _GroupsViewState extends State { height: MediaQuery.paddingOf(context).bottom - 20, ); } - return GroupTile(group: groups[index], onTap: () async { - await Navigator.push( - context, - adaptivePageRoute( - builder: (context) { - return GroupDetailView(groupToEdit: groups[index]); - }, - ), - ); - setState(() { - loadGroups(); - }); - }); + return GroupTile( + group: groups[index], + onTap: () async { + await Navigator.push( + context, + adaptivePageRoute( + builder: (context) { + return GroupProfileView( + group: groups[index], + callback: loadGroups, + ); + }, + ), + ); + }, + ); }, ), ), @@ -117,6 +121,9 @@ class _GroupsViewState extends State { } void loadGroups() { + setState(() { + isLoading = true; + }); Future.wait([ db.groupDao.getAllGroups(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart deleted file mode 100644 index 3b1f37b..0000000 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart +++ /dev/null @@ -1,99 +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/l10n/generated/app_localizations.dart'; -import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; - -class ChooseRulesetView extends StatefulWidget { - /// A view that allows the user to choose a ruleset from a list of available rulesets - /// - [rulesets]: A list of tuples containing the ruleset and its description - /// - [initialRulesetIndex]: The index of the initially selected ruleset - const ChooseRulesetView({ - super.key, - required this.rulesets, - required this.initialRulesetIndex, - }); - - /// A list of tuples containing the ruleset and its description - final List<(Ruleset, String)> rulesets; - - /// The index of the initially selected ruleset - final int initialRulesetIndex; - - @override - State createState() => _ChooseRulesetViewState(); -} - -class _ChooseRulesetViewState extends State { - /// Currently selected ruleset index - late int selectedRulesetIndex; - - @override - void initState() { - selectedRulesetIndex = widget.initialRulesetIndex; - super.initState(); - } - - @override - Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - return DefaultTabController( - length: 2, - initialIndex: 0, - child: Scaffold( - backgroundColor: CustomTheme.backgroundColor, - appBar: AppBar( - leading: IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: () { - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - ), - title: Text(loc.choose_ruleset), - ), - body: PopScope( - // This fixes that the Android Back Gesture didn't return the - // selectedRulesetIndex and therefore the selected Ruleset wasn't saved - canPop: false, - onPopInvokedWithResult: (bool didPop, Object? result) { - if (didPop) { - return; - } - Navigator.of(context).pop( - selectedRulesetIndex == -1 - ? null - : widget.rulesets[selectedRulesetIndex].$1, - ); - }, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: widget.rulesets.length, - itemBuilder: (BuildContext context, int index) { - return TitleDescriptionListTile( - onPressed: () async { - setState(() { - if (selectedRulesetIndex == index) { - selectedRulesetIndex = -1; - } else { - selectedRulesetIndex = index; - } - }); - }, - title: translateRulesetToString( - widget.rulesets[index].$1, - context, - ), - description: widget.rulesets[index].$2, - isHighlighted: selectedRulesetIndex == index, - ); - }, - ), - ), - ), - ); - } -} 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 0e52d03..a3d5cf1 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 @@ -10,7 +10,6 @@ import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart'; -import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; import 'package:game_tracker/presentation/widgets/player_selection.dart'; @@ -58,13 +57,6 @@ class _CreateMatchViewState extends State { /// the [ChooseGroupView] String selectedGroupId = ''; - /// The currently selected ruleset - Ruleset? selectedRuleset; - - /// The index of the currently selected ruleset in [rulesets] to mark it in - /// the [ChooseRulesetView] - int selectedRulesetIndex = -1; - /// The index of the currently selected game in [games] to mark it in /// the [ChooseGameView] int selectedGameIndex = -1; @@ -72,12 +64,6 @@ class _CreateMatchViewState extends State { /// The currently selected players List? selectedPlayers; - /// 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(); @@ -110,15 +96,8 @@ class _CreateMatchViewState extends State { super.didChangeDependencies(); final loc = AppLocalizations.of(context); hintText ??= loc.match_name; - _rulesets = [ - (Ruleset.singleWinner, loc.ruleset_single_winner), - (Ruleset.singleLoser, loc.ruleset_single_loser), - (Ruleset.mostPoints, loc.ruleset_most_points), - (Ruleset.leastPoints, loc.ruleset_least_points), - ]; } - // TODO: Replace when games are implemented List<(String, String, Ruleset)> games = [ ('Example Game 1', 'This is a description', Ruleset.leastPoints), ('Example Game 2', '', Ruleset.singleWinner), @@ -161,39 +140,12 @@ class _CreateMatchViewState extends State { setState(() { if (selectedGameIndex != -1) { hintText = games[selectedGameIndex].$1; - selectedRuleset = games[selectedGameIndex].$3; - selectedRulesetIndex = _rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); } else { hintText = loc.match_name; - selectedRuleset = null; } }); }, ), - ChooseTile( - title: loc.ruleset, - trailingText: selectedRuleset == null - ? loc.none - : translateRulesetToString(selectedRuleset!, context), - onPressed: () async { - selectedRuleset = await Navigator.of(context).push( - adaptivePageRoute( - builder: (context) => ChooseRulesetView( - rulesets: _rulesets, - initialRulesetIndex: selectedRulesetIndex, - ), - ), - ); - if (!mounted) return; - selectedRulesetIndex = _rulesets.indexWhere( - (r) => r.$1 == selectedRuleset, - ); - selectedGameIndex = -1; - setState(() {}); - }, - ), ChooseTile( title: loc.group, trailingText: selectedGroup == null @@ -278,7 +230,6 @@ class _CreateMatchViewState extends State { /// - Either a group is selected OR at least 2 players are selected bool _enableCreateGameButton() { return (selectedGroup != null || - (selectedPlayers != null && selectedPlayers!.length > 1)) && - selectedRuleset != null; + (selectedPlayers != null && selectedPlayers!.length > 1)); } } diff --git a/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart b/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart index e46fc31..54ff34e 100644 --- a/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart +++ b/lib/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.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/settings_view/licenses/oss_licenses.dart'; +import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; import 'package:url_launcher/url_launcher.dart'; class LicenseDetailView extends StatelessWidget { @@ -29,19 +30,11 @@ class LicenseDetailView extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - margin: const EdgeInsetsGeometry.only(right: 15), - width: 60, - height: 60, - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(40), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.description, - color: CustomTheme.primaryColor, - size: 30, - ), + const ColoredIconContainer( + icon: Icons.description, + margin: EdgeInsetsGeometry.only(right: 15), + containerSize: 60, + iconSize: 30, ), Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/presentation/widgets/colored_icon_container.dart b/lib/presentation/widgets/colored_icon_container.dart new file mode 100644 index 0000000..be51cd2 --- /dev/null +++ b/lib/presentation/widgets/colored_icon_container.dart @@ -0,0 +1,57 @@ +import 'package:flutter/cupertino.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +class ColoredIconContainer extends StatelessWidget { + /// A customizable container widget that displays an icon with a colored background. + /// - [icon]: The icon to be displayed inside the container. + /// - [containerSize]: The size of the container (width and height). + /// - [iconSize]: The size of the icon inside the container. + /// - [margin]: Optional margin around the container. + /// - [padding]: Optional padding inside the container. + const ColoredIconContainer({ + super.key, + required this.icon, + this.containerSize = 44, + this.iconSize = 28, + this.margin, + this.padding, + }); + + /// The icon to be displayed inside the container. + final IconData icon; + + /// The size of the container (width and height). + final double containerSize; + + /// The size of the icon inside the container. + final double iconSize; + + /// Optional margin around the container. + final EdgeInsetsGeometry? margin; + + /// Optional padding inside the container. + final EdgeInsetsGeometry? padding; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Container( + width: containerSize, + height: containerSize, + margin: margin, + padding: padding, + decoration: BoxDecoration( + color: CustomTheme.primaryColor.withAlpha(40), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + icon, + size: iconSize, + color: CustomTheme.primaryColor.withGreen(40), + ), + ), + ], + ); + } +} diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart index 05dbd23..c035a04 100644 --- a/lib/presentation/widgets/tiles/group_tile.dart +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -3,12 +3,17 @@ import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; -class GroupTile extends StatelessWidget { +class GroupTile extends StatefulWidget { /// 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. - const GroupTile({super.key, required this.group, this.isHighlighted = false, this.onTap}); + /// - [onTap]: Callback function to be executed when the tile is tapped. + const GroupTile({ + super.key, + required this.group, + this.isHighlighted = false, + this.onTap, + }); /// The group data to be displayed. final Group group; @@ -16,17 +21,22 @@ class GroupTile extends StatelessWidget { /// Whether the tile should be highlighted. final bool isHighlighted; - /// Callback function to handle tap events. + /// Callback function to be executed when the tile is tapped. final VoidCallback? onTap; + @override + State createState() => _GroupTileState(); +} + +class _GroupTileState extends State { @override Widget build(BuildContext context) { return GestureDetector( - onTap: onTap, + onTap: widget.onTap, child: AnimatedContainer( margin: CustomTheme.standardMargin, padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), - decoration: isHighlighted + decoration: widget.isHighlighted ? CustomTheme.highlightedBoxDecoration : CustomTheme.standardBoxDecoration, duration: const Duration(milliseconds: 150), @@ -38,7 +48,7 @@ class GroupTile extends StatelessWidget { children: [ Flexible( child: Text( - group.name, + widget.group.name, overflow: TextOverflow.ellipsis, style: const TextStyle( fontWeight: FontWeight.bold, @@ -49,7 +59,7 @@ class GroupTile extends StatelessWidget { Row( children: [ Text( - '${group.members.length}', + '${widget.group.members.length}', style: const TextStyle( fontWeight: FontWeight.w900, fontSize: 18, @@ -69,7 +79,7 @@ class GroupTile extends StatelessWidget { runSpacing: 8.0, children: [ for (var member in [ - ...group.members, + ...widget.group.members, ]..sort((a, b) => a.name.compareTo(b.name))) TextIconTile(text: member.name, iconEnabled: false), ], diff --git a/lib/presentation/widgets/tiles/info_tile.dart b/lib/presentation/widgets/tiles/info_tile.dart index 280c7d7..78d7f28 100644 --- a/lib/presentation/widgets/tiles/info_tile.dart +++ b/lib/presentation/widgets/tiles/info_tile.dart @@ -17,6 +17,7 @@ class InfoTile extends StatefulWidget { this.padding, this.height, this.width, + this.horizontalAlignment = CrossAxisAlignment.center, }); /// The title text displayed on the tile. @@ -37,6 +38,9 @@ class InfoTile extends StatefulWidget { /// Optional width for the tile. final double? width; + /// The main axis alignment for the content. + final CrossAxisAlignment horizontalAlignment; + @override State createState() => _InfoTileState(); } @@ -51,7 +55,7 @@ class _InfoTileState extends State { decoration: CustomTheme.standardBoxDecoration, child: Column( mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: widget.horizontalAlignment, children: [ Row( children: [ diff --git a/lib/presentation/widgets/tiles/license_tile.dart b/lib/presentation/widgets/tiles/license_tile.dart index 14ee2bf..33e5a45 100644 --- a/lib/presentation/widgets/tiles/license_tile.dart +++ b/lib/presentation/widgets/tiles/license_tile.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; +import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; class LicenseTile extends StatelessWidget { /// A tile widget that displays information about a software package license. @@ -29,18 +30,10 @@ class LicenseTile extends StatelessWidget { ), child: Row( children: [ - Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(40), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - Icons.description, - color: CustomTheme.primaryColor, - size: 32, - ), + const ColoredIconContainer( + icon: Icons.description, + containerSize: 50, + iconSize: 32, ), const SizedBox(width: 16), Expanded( diff --git a/lib/presentation/widgets/tiles/match_tile.dart b/lib/presentation/widgets/tiles/match_tile.dart index 11cdea0..e1365c1 100644 --- a/lib/presentation/widgets/tiles/match_tile.dart +++ b/lib/presentation/widgets/tiles/match_tile.dart @@ -230,7 +230,7 @@ class _MatchTileState extends State { } else if (difference.inDays < 7) { return loc.days_ago(difference.inDays); } else { - return DateFormat('MMM d, yyyy').format(dateTime); + return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}'; } } diff --git a/lib/presentation/widgets/tiles/settings_list_tile.dart b/lib/presentation/widgets/tiles/settings_list_tile.dart index 53fb041..d4bc6dc 100644 --- a/lib/presentation/widgets/tiles/settings_list_tile.dart +++ b/lib/presentation/widgets/tiles/settings_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/presentation/widgets/colored_icon_container.dart'; class SettingsListTile extends StatelessWidget { /// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget. @@ -46,18 +47,10 @@ class SettingsListTile extends StatelessWidget { Row( mainAxisSize: MainAxisSize.min, children: [ - Container( - width: 44, - height: 44, - decoration: BoxDecoration( - color: CustomTheme.primaryColor.withAlpha(40), - borderRadius: BorderRadius.circular(10), - ), - child: Icon( - icon, - size: 28, - color: CustomTheme.primaryColor.withGreen(40), - ), + ColoredIconContainer( + icon: icon, + containerSize: 44, + iconSize: 28, ), const SizedBox(width: 16), Text(title, style: const TextStyle(fontSize: 18)), diff --git a/lib/presentation/widgets/tiles/title_description_list_tile.dart b/lib/presentation/widgets/tiles/title_description_list_tile.dart index 3141cbe..a963d16 100644 --- a/lib/presentation/widgets/tiles/title_description_list_tile.dart +++ b/lib/presentation/widgets/tiles/title_description_list_tile.dart @@ -73,7 +73,6 @@ class TitleDescriptionListTile extends StatelessWidget { const Spacer(), Container( constraints: const BoxConstraints(maxWidth: 115), - margin: const EdgeInsets.only(top: 4), padding: const EdgeInsets.symmetric( vertical: 2, horizontal: 6, diff --git a/pubspec.yaml b/pubspec.yaml index 5e4432d..aa253b3 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+216 +version: 0.0.7+239 environment: sdk: ^3.8.1