diff --git a/lib/data/dao/statistic_dao.dart b/lib/data/dao/statistic_dao.dart index 7bdebde..092ceb0 100644 --- a/lib/data/dao/statistic_dao.dart +++ b/lib/data/dao/statistic_dao.dart @@ -80,12 +80,13 @@ class StatisticDao extends DatabaseAccessor result.map((row) async { final groups = await db.statisticGroupDao.getGroupsForStatistic(row.id); final games = await db.statisticGameDao.getGamesForStatistic(row.id); + final scopes = await db.statisticScopeDao.getScopeForStatistic(row.id); return Statistic( type: StatisticType.values.firstWhere( (type) => type.name == row.type, ), - scopes: [], + scopes: scopes, timeframe: Timeframe.values.firstWhereOrNull( (t) => t.name == row.timeframe, ), diff --git a/lib/data/dao/statistic_game_dao.dart b/lib/data/dao/statistic_game_dao.dart index 76e3429..d546ce0 100644 --- a/lib/data/dao/statistic_game_dao.dart +++ b/lib/data/dao/statistic_game_dao.dart @@ -12,12 +12,13 @@ class StatisticGameDao extends DatabaseAccessor StatisticGameDao(super.db); /// Retrieves a list of games associated with a specific statistic. - Future> getGamesForStatistic(String statisticId) async { + Future?> getGamesForStatistic(String statisticId) async { final query = select(statisticGameTable).join([ innerJoin(gameTable, gameTable.id.equalsExp(statisticGameTable.gameId)), ])..where(statisticGameTable.statisticId.equals(statisticId)); final results = await query.map((row) => row.readTable(gameTable)).get(); + if (results.isEmpty) return null; return results .map( (row) => Game( diff --git a/lib/data/dao/statistic_group_dao.dart b/lib/data/dao/statistic_group_dao.dart index 9eb9397..449b6a8 100644 --- a/lib/data/dao/statistic_group_dao.dart +++ b/lib/data/dao/statistic_group_dao.dart @@ -12,7 +12,7 @@ class StatisticGroupDao extends DatabaseAccessor StatisticGroupDao(super.db); /// Retrieves a list of groups associated with a specific statistic. - Future> getGroupsForStatistic(String statisticId) async { + Future?> getGroupsForStatistic(String statisticId) async { final query = select(statisticGroupTable).join([ innerJoin( groupTable, @@ -21,6 +21,7 @@ class StatisticGroupDao extends DatabaseAccessor ])..where(statisticGroupTable.statisticId.equals(statisticId)); final results = await query.map((row) => row.readTable(groupTable)).get(); + if (results.isEmpty) return null; final groups = await Future.wait( results.map((result) async { final groupMembers = await db.playerGroupDao.getPlayersOfGroup( diff --git a/lib/data/dao/statistic_scope_dao.dart b/lib/data/dao/statistic_scope_dao.dart index d2fc315..eb286af 100644 --- a/lib/data/dao/statistic_scope_dao.dart +++ b/lib/data/dao/statistic_scope_dao.dart @@ -15,8 +15,8 @@ class StatisticScopeDao extends DatabaseAccessor final query = select(statisticScopeTable) ..where((tbl) => tbl.statisticId.equals(statisticId)); - final results = await query.get(); - return results + final result = await query.get(); + return result .map( (row) => StatisticScope.values.firstWhere( (e) => e.name == row.scope, diff --git a/lib/l10n/arb/app_de.arb b/lib/l10n/arb/app_de.arb index 8f92cb0..b6ae56b 100644 --- a/lib/l10n/arb/app_de.arb +++ b/lib/l10n/arb/app_de.arb @@ -21,6 +21,13 @@ "color_yellow": "Gelb", "confirm": "Bestätigen", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", + "@could_not_add_player": { + "placeholders": { + "playerName": { + "type": "String" + } + } + }, "create_game": "Spielvorlage erstellen", "create_group": "Gruppe erstellen", "create_match": "Spiel erstellen", diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index b7548bd..baf5006 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -20,23 +20,29 @@ "color_teal": "Teal", "color_yellow": "Yellow", "confirm": "Confirm", - "could_not_add_player": "Could not add player", + "could_not_add_player": "Could not add player {playerName}", + "@could_not_add_player": { + "placeholders": { + "playerName": { + "type": "String" + } + } + }, "create_game": "Create Game", "create_group": "Create Group", "create_match": "Create match", "create_new_group": "Create new group", "create_new_match": "Create new match", "create_statistic": "Create statistic", - "create_statistic_classifier_subtitle": "Select which key metric you want to display", - "create_statistic_classifier_title": "Classifier", - "create_statistic_games_subtitle": "Select the filtered games", - "create_statistic_games_title": "Games", - "create_statistic_groups_subtitle": "Select the filtered groups", - "create_statistic_groups_title": "Groups", - "create_statistic_scope_subtitle": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", - "create_statistic_scope_title": "Scope", - "create_statistic_timeframe_subtitle": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", - "create_statistic_timeframe_title": "Timeframe", + "which_key_metric": "Select which key metric you want to display", + "classifier": "Classifier", + "select_the_filtered_games": "Select the filtered games", + "games": "Games", + "select_the_filtered_groups": "Select the filtered groups", + "select_main_filter": "Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.", + "scope": "Scope", + "select_a_timeframe_for_which_data_will_be_filtered": "Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.", + "timeframe": "Timeframe", "created_on": "Created on", "data": "Data", "data_successfully_deleted": "Data successfully deleted", @@ -54,6 +60,7 @@ } } }, + "filter": "Filter", "delete_group": "Delete Group", "delete_match": "Delete Match", "delete_player": "Delete player?", @@ -120,6 +127,7 @@ "no_results_entered_yet": "No results entered yet", "no_second_match_available": "No second match available", "no_statistics_available": "No statistics available", + "no_statistics_created_yet": "No statistics created yet", "none": "None", "none_group": "None", "not_available": "Not available", diff --git a/lib/l10n/generated/app_localizations.dart b/lib/l10n/generated/app_localizations.dart index e8af2e8..20067d4 100644 --- a/lib/l10n/generated/app_localizations.dart +++ b/lib/l10n/generated/app_localizations.dart @@ -221,8 +221,8 @@ abstract class AppLocalizations { /// No description provided for @could_not_add_player. /// /// In en, this message translates to: - /// **'Could not add player'** - String could_not_add_player(Object playerName); + /// **'Could not add player {playerName}'** + String could_not_add_player(String playerName); /// No description provided for @create_game. /// @@ -260,65 +260,59 @@ abstract class AppLocalizations { /// **'Create statistic'** String get create_statistic; - /// No description provided for @create_statistic_classifier_subtitle. + /// No description provided for @which_key_metric. /// /// In en, this message translates to: /// **'Select which key metric you want to display'** - String get create_statistic_classifier_subtitle; + String get which_key_metric; - /// No description provided for @create_statistic_classifier_title. + /// No description provided for @classifier. /// /// In en, this message translates to: /// **'Classifier'** - String get create_statistic_classifier_title; + String get classifier; - /// No description provided for @create_statistic_games_subtitle. + /// No description provided for @select_the_filtered_games. /// /// In en, this message translates to: /// **'Select the filtered games'** - String get create_statistic_games_subtitle; + String get select_the_filtered_games; - /// No description provided for @create_statistic_games_title. + /// No description provided for @games. /// /// In en, this message translates to: /// **'Games'** - String get create_statistic_games_title; + String get games; - /// No description provided for @create_statistic_groups_subtitle. + /// No description provided for @select_the_filtered_groups. /// /// In en, this message translates to: /// **'Select the filtered groups'** - String get create_statistic_groups_subtitle; + String get select_the_filtered_groups; - /// No description provided for @create_statistic_groups_title. - /// - /// In en, this message translates to: - /// **'Groups'** - String get create_statistic_groups_title; - - /// No description provided for @create_statistic_scope_subtitle. + /// No description provided for @select_main_filter. /// /// In en, this message translates to: /// **'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'** - String get create_statistic_scope_subtitle; + String get select_main_filter; - /// No description provided for @create_statistic_scope_title. + /// No description provided for @scope. /// /// In en, this message translates to: /// **'Scope'** - String get create_statistic_scope_title; + String get scope; - /// No description provided for @create_statistic_timeframe_subtitle. + /// No description provided for @select_a_timeframe_for_which_data_will_be_filtered. /// /// In en, this message translates to: /// **'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'** - String get create_statistic_timeframe_subtitle; + String get select_a_timeframe_for_which_data_will_be_filtered; - /// No description provided for @create_statistic_timeframe_title. + /// No description provided for @timeframe. /// /// In en, this message translates to: /// **'Timeframe'** - String get create_statistic_timeframe_title; + String get timeframe; /// No description provided for @created_on. /// @@ -380,6 +374,12 @@ abstract class AppLocalizations { /// **'If you delete this game template, {count, plural, =1{1 match} other{{count} matches}} using this game template will also be deleted.'** String delete_game_with_matches_warning(int count); + /// No description provided for @filter. + /// + /// In en, this message translates to: + /// **'Filter'** + String get filter; + /// No description provided for @delete_group. /// /// In en, this message translates to: @@ -776,6 +776,12 @@ abstract class AppLocalizations { /// **'No statistics available'** String get no_statistics_available; + /// No description provided for @no_statistics_created_yet. + /// + /// In en, this message translates to: + /// **'No statistics created yet'** + String get no_statistics_created_yet; + /// No description provided for @none. /// /// 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 4ff81bb..8152185 100644 --- a/lib/l10n/generated/app_localizations_de.dart +++ b/lib/l10n/generated/app_localizations_de.dart @@ -69,7 +69,7 @@ class AppLocalizationsDe extends AppLocalizations { String get confirm => 'Bestätigen'; @override - String could_not_add_player(Object playerName) { + String could_not_add_player(String playerName) { return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; } @@ -92,39 +92,33 @@ class AppLocalizationsDe extends AppLocalizations { String get create_statistic => 'Statistik erstellen'; @override - String get create_statistic_classifier_subtitle => - 'Wähle die anzuzeigende Hauptmetrik aus'; + String get which_key_metric => 'Select which key metric you want to display'; @override - String get create_statistic_classifier_title => 'Klassifikator'; + String get classifier => 'Classifier'; @override - String get create_statistic_games_subtitle => - 'Wähle die gefilterten Spielvorlagen'; + String get select_the_filtered_games => 'Select the filtered games'; @override - String get create_statistic_games_title => 'Spielvorlagen'; + String get games => 'Games'; @override - String get create_statistic_groups_subtitle => - 'Wähle die gefilterten Gruppen'; + String get select_the_filtered_groups => 'Select the filtered groups'; @override - String get create_statistic_groups_title => 'Gruppen'; + String get select_main_filter => + 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; @override - String get create_statistic_scope_subtitle => - 'Wähle den Hauptfilter für deine Statistik. Er bestimmt, welche Daten zur Berechnung des Klassifikators verwendet werden.'; + String get scope => 'Scope'; @override - String get create_statistic_scope_title => 'Bereich'; + String get select_a_timeframe_for_which_data_will_be_filtered => + 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; @override - String get create_statistic_timeframe_subtitle => - 'Wähle einen Zeitraum, nach dem die Daten gefiltert werden. Nur Spiele, die innerhalb des Zeitraums beendet wurden, fließen in die Statistik ein.'; - - @override - String get create_statistic_timeframe_title => 'Zeitraum'; + String get timeframe => 'Timeframe'; @override String get created_on => 'Erstellt am'; @@ -166,6 +160,9 @@ class AppLocalizationsDe extends AppLocalizations { return 'Wenn du diese Spielvorlage löschst, $_temp0 mit dieser Spielvorlage ebenfalls gelöscht.'; } + @override + String get filter => 'Filter'; + @override String get delete_group => 'Gruppe löschen'; @@ -369,6 +366,9 @@ class AppLocalizationsDe extends AppLocalizations { @override String get no_statistics_available => 'Keine Statistiken verfügbar'; + @override + String get no_statistics_created_yet => 'No statistics created yet'; + @override String get none => 'Kein'; diff --git a/lib/l10n/generated/app_localizations_en.dart b/lib/l10n/generated/app_localizations_en.dart index 4bc1dd7..2366ce2 100644 --- a/lib/l10n/generated/app_localizations_en.dart +++ b/lib/l10n/generated/app_localizations_en.dart @@ -69,8 +69,8 @@ class AppLocalizationsEn extends AppLocalizations { String get confirm => 'Confirm'; @override - String could_not_add_player(Object playerName) { - return 'Could not add player'; + String could_not_add_player(String playerName) { + return 'Could not add player $playerName'; } @override @@ -92,37 +92,33 @@ class AppLocalizationsEn extends AppLocalizations { String get create_statistic => 'Create statistic'; @override - String get create_statistic_classifier_subtitle => - 'Select which key metric you want to display'; + String get which_key_metric => 'Select which key metric you want to display'; @override - String get create_statistic_classifier_title => 'Classifier'; + String get classifier => 'Classifier'; @override - String get create_statistic_games_subtitle => 'Select the filtered games'; + String get select_the_filtered_games => 'Select the filtered games'; @override - String get create_statistic_games_title => 'Games'; + String get games => 'Games'; @override - String get create_statistic_groups_subtitle => 'Select the filtered groups'; + String get select_the_filtered_groups => 'Select the filtered groups'; @override - String get create_statistic_groups_title => 'Groups'; - - @override - String get create_statistic_scope_subtitle => + String get select_main_filter => 'Select the main filter for your statistic. This will determine which data is used to calculate the selected classifier.'; @override - String get create_statistic_scope_title => 'Scope'; + String get scope => 'Scope'; @override - String get create_statistic_timeframe_subtitle => + String get select_a_timeframe_for_which_data_will_be_filtered => 'Select a timeframe for which the data will be filtered. Only matches that ended within the selected timeframe will be included in the statistic.'; @override - String get create_statistic_timeframe_title => 'Timeframe'; + String get timeframe => 'Timeframe'; @override String get created_on => 'Created on'; @@ -164,6 +160,9 @@ class AppLocalizationsEn extends AppLocalizations { return 'If you delete this game template, $_temp0 using this game template will also be deleted.'; } + @override + String get filter => 'Filter'; + @override String get delete_group => 'Delete Group'; @@ -367,6 +366,9 @@ class AppLocalizationsEn extends AppLocalizations { @override String get no_statistics_available => 'No statistics available'; + @override + String get no_statistics_created_yet => 'No statistics created yet'; + @override String get none => 'None'; diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 3c01f45..1d30afb 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -79,7 +79,7 @@ class _MatchViewState extends State { visible: matches.isNotEmpty, replacement: Center( child: TopCenteredMessage( - icon: Icons.report, + icon: Icons.info, title: loc.info, message: loc.no_matches_created_yet, ), diff --git a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart index 9ac03aa..92a01bb 100644 --- a/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/create_statistic_view.dart @@ -68,7 +68,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_classifier_title, + loc.classifier, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -77,7 +77,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_classifier_subtitle, + loc.select_a_classifier, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -139,7 +139,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_scope_title, + loc.scope, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -148,7 +148,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_scope_subtitle, + loc.select_a_scope, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -214,7 +214,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_games_title, + loc.games, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -223,7 +223,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_games_subtitle, + loc.select_the_filtered_games, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -310,7 +310,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_groups_title, + loc.groups, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -319,7 +319,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_groups_subtitle, + loc.select_the_filtered_groups, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -396,7 +396,7 @@ class _CreateStatisticViewState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - loc.create_statistic_timeframe_title, + loc.timeframe, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, @@ -405,7 +405,7 @@ class _CreateStatisticViewState extends State { ), ), Text( - loc.create_statistic_timeframe_subtitle, + loc.select_a_timeframe_for_which_data_will_be_filtered, textAlign: TextAlign.start, style: const TextStyle( color: CustomTheme.textColor, diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart new file mode 100644 index 0000000..4053cb3 --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view/statistic_detail_view.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; +import 'package:tallee/l10n/generated/app_localizations.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart' + show + translateScopeToString, + translateStatisticTypeToString, + translateTimeframeToString; +import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart'; +import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; +import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; + +class StatisticDetailView extends StatefulWidget { + const StatisticDetailView({ + super.key, + required this.statistic, + required this.values, + required this.icon, + required this.barColor, + }); + + final Statistic statistic; + final List<(Player, num)> values; + final IconData icon; + final Color barColor; + + @override + State createState() => _StatisticDetailViewState(); +} + +class _StatisticDetailViewState extends State { + int displayCount = 0; + + @override + void initState() { + super.initState(); + displayCount = widget.statistic.displayCount; + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + final title = translateStatisticTypeToString( + widget.statistic.type, + context, + ); + const style = TextStyle(fontWeight: FontWeight.bold); + + return Scaffold( + appBar: AppBar(title: Text(title)), + body: SingleChildScrollView( + padding: const EdgeInsets.all(12.0), + child: Column( + children: [ + StatisticsTile( + icon: widget.icon, + title: title, + width: MediaQuery.sizeOf(context).width * 0.95, + values: widget.values, + barColor: widget.barColor, + selectedGroups: widget.statistic.selectedGroups, + selectedGames: widget.statistic.selectedGames, + ), + const SizedBox(height: 12), + + InfoTile( + icon: Icons.filter_alt, + title: loc.filter, + content: Column( + spacing: 12, + + children: [ + // Scopes + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.scope, style: style), + Text( + widget.statistic.scopes + .map( + (scope) => translateScopeToString(scope, context), + ) + .join('\n'), + textAlign: TextAlign.end, + ), + ], + ), + + // Timeframe + if (widget.statistic.timeframe != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.timeframe, style: style), + Text( + translateTimeframeToString( + widget.statistic.timeframe!, + context, + ), + textAlign: TextAlign.end, + ), + ], + ), + + // Groups + if (widget.statistic.selectedGroups != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.groups, style: style), + Text( + widget.statistic.selectedGroups! + .map((group) => group.name) + .join('\n'), + textAlign: TextAlign.end, + ), + ], + ), + + // Games + if (widget.statistic.selectedGames != null) + Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(loc.games, style: style), + Text( + widget.statistic.selectedGames! + .map((game) => game.name) + .join('\n'), + textAlign: TextAlign.end, + ), + ], + ), + + if (widget.values.isNotEmpty) + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Display count', style: style), + Row( + children: [ + HapticIconButton( + icon: const Icon(Icons.remove), + onPressed: displayCount <= 1 + ? null + : () => setState(() => displayCount -= 1), + ), + SizedBox( + width: 30, + child: Text( + '$displayCount', + textAlign: TextAlign.center, + ), + ), + HapticIconButton( + icon: const Icon(Icons.add), + onPressed: displayCount >= widget.values.length + ? null + : () => setState(() => displayCount += 1), + ), + ], + ), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart index fd43390..807358e 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart @@ -3,6 +3,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/game.dart'; +import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/statistic.dart'; @@ -14,13 +16,18 @@ List _colorPalette = AppColor.values .map((c) => getColorFromAppColor(c)) .toList(); -/// Build the [StatisticsTile] for a given [Statistic]. -Widget buildStatisticTile({ +/// Returns the icon for the given statistic type. +IconData getStatisticIconForType(StatisticType type) => + _getStatisticIcon(type: type); + +/// Returns a color from the palette based on the statistic's ID. +Color getStatisticColorForStatistic(Statistic stat) => _getStatisticColor(stat); + +/// Computes the statistic values for a given [Statistic]. +List<(Player, num)> computeStatisticValues({ required Statistic statistic, required List matches, required List players, - required BuildContext context, - double? width, }) { final filteredMatches = _getFilterMatches(statistic, matches); final filteredPlayers = _getFilteredPlayers( @@ -29,16 +36,26 @@ Widget buildStatisticTile({ filteredMatches, ); - print('Building tile for statistic: $statistic'); - print('Filtered matches count: ${filteredMatches.length}'); - print('Filtered players count: ${filteredPlayers.length}'); - - final values = _computeValuesForType( + return _computeValuesForType( type: statistic.type, matches: filteredMatches, players: filteredPlayers, ); - print(values); +} + +/// Build the [StatisticsTile] for a given [Statistic]. +Widget buildStatisticTile({ + required Statistic statistic, + required List matches, + required List players, + required BuildContext context, + double? width, +}) { + final values = computeStatisticValues( + statistic: statistic, + matches: matches, + players: players, + ); return StatisticsTile( icon: _getStatisticIcon(type: statistic.type), @@ -46,7 +63,9 @@ Widget buildStatisticTile({ width: width ?? MediaQuery.sizeOf(context).width * 0.95, values: values, barColor: _getStatisticColor(statistic), - statistic: statistic, + displayCount: statistic.displayCount, + selectedGroups: statistic.selectedGroups, + selectedGames: statistic.selectedGames, ); } @@ -296,10 +315,8 @@ Widget buildSkeletonStatisticTile({required BuildContext context}) { width: MediaQuery.sizeOf(context).width * 0.95, values: values, barColor: _colorPalette[Random().nextInt(_colorPalette.length)], - statistic: Statistic( - type: StatisticType.totalMatches, - scopes: [StatisticScope.allPlayers], - timeframe: Timeframe.last7Days, - ), + selectedGames: [Game(name: 'Game 1', ruleset: Ruleset.highestScore)], + selectedGroups: [Group(name: 'Group 1', members: [])], + displayCount: 5, ); } diff --git a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart index 8c3ac16..d981b0a 100644 --- a/lib/presentation/views/main_menu/statistics_view/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view/statistics_view.dart @@ -8,9 +8,11 @@ import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_detail_view.dart'; import 'package:tallee/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; +import 'package:tallee/presentation/widgets/top_centered_message.dart'; class StatisticsView extends StatefulWidget { /// A view that displays player statistics @@ -45,18 +47,30 @@ class _StatisticsViewState extends State { alignment: AlignmentDirectional.bottomCenter, fit: StackFit.expand, children: [ - SingleChildScrollView( - child: AppSkeleton( - enabled: isLoading, - fixLayoutBuilder: true, - child: Column( - spacing: 12, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...statisticTiles, - SizedBox(height: MediaQuery.paddingOf(context).bottom + 80), - ], + Visibility( + visible: statisticTiles.isNotEmpty, + replacement: Center( + child: TopCenteredMessage( + icon: Icons.info, + title: loc.info, + message: loc.no_statistics_created_yet, + ), + ), + child: SingleChildScrollView( + child: AppSkeleton( + enabled: isLoading, + fixLayoutBuilder: true, + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...statisticTiles, + SizedBox( + height: MediaQuery.paddingOf(context).bottom + 80, + ), + ], + ), ), ), ), @@ -75,12 +89,7 @@ class _StatisticsViewState extends State { ), ); if (!context.mounted) return; - final newTile = buildStatisticTile( - statistic: newStatistic, - matches: _allMatches, - players: _allPlayers, - context: context, - ); + final newTile = _buildStatisticTile(context, newStatistic); setState(() { statisticTiles.add(newTile); @@ -126,15 +135,40 @@ class _StatisticsViewState extends State { setState(() { statisticTiles = [ for (final statistic in statistics) ...[ - buildStatisticTile( - statistic: statistic, - matches: _allMatches, - players: _allPlayers, - context: context, - ), + _buildStatisticTile(context, statistic), ], ]; isLoading = false; }); } + + Widget _buildStatisticTile(BuildContext context, Statistic statistic) { + return GestureDetector( + onTap: () { + final values = computeStatisticValues( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + ); + + Navigator.push( + context, + adaptivePageRoute( + builder: (context) => StatisticDetailView( + statistic: statistic, + values: values, + icon: getStatisticIconForType(statistic.type), + barColor: getStatisticColorForStatistic(statistic), + ), + ), + ); + }, + child: buildStatisticTile( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + context: context, + ), + ); + } } diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index 24d51fc..f6db2a1 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -8,7 +8,6 @@ import 'package:tallee/core/enums.dart'; import 'package:tallee/data/models/game.dart'; import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/player.dart'; -import 'package:tallee/data/models/statistic.dart'; import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart'; @@ -27,7 +26,9 @@ class StatisticsTile extends StatelessWidget { required this.width, required this.values, required this.barColor, - required this.statistic, + this.displayCount, + this.selectedGroups, + this.selectedGames, }); /// The icon displayed next to the title. @@ -45,7 +46,10 @@ class StatisticsTile extends StatelessWidget { /// The color of the bars representing the values. final Color barColor; - final Statistic statistic; + final int? displayCount; + + final List? selectedGroups; + final List? selectedGames; @override Widget build(BuildContext context) { @@ -70,8 +74,12 @@ class StatisticsTile extends StatelessWidget { child: LayoutBuilder( builder: (context, constraints) { final maxBarWidth = constraints.maxWidth * 0.8; - final displayCount = min(values.length, statistic.displayCount); - final displayValues = values.take(displayCount).toList(); + + // If displayCount wasnt provided, take all values + final valuesShown = displayCount == null + ? values.length + : min(values.length, displayCount!); + final displayValues = values.take(valuesShown).toList(); final maxVal = displayValues.isNotEmpty ? displayValues.fold( 0, @@ -83,7 +91,7 @@ class StatisticsTile extends StatelessWidget { return Column( children: [ // Bars - ...List.generate(displayCount, (index) { + ...List.generate(valuesShown, (index) { /// Fraction of wins final double fraction = (maxVal > 0) ? (displayValues[index].$2 / maxVal) @@ -187,12 +195,14 @@ class StatisticsTile extends StatelessWidget { }), // Group & Game info - if (statistic.selectedGames != null || - statistic.selectedGroups != null) + if (hasGame || hasGroup) Padding( padding: const EdgeInsets.only(top: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + child: Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 4, + runSpacing: 4, children: [ // Game if (hasGroup) @@ -205,7 +215,7 @@ class StatisticsTile extends StatelessWidget { size: 20, ), Text( - getGameText(statistic.selectedGames!), + getGameText(selectedGames!), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -227,7 +237,7 @@ class StatisticsTile extends StatelessWidget { color: CustomTheme.hintColor, ), Text( - getGroupText(statistic.selectedGroups!), + getGroupText(selectedGroups!), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, @@ -265,9 +275,7 @@ class StatisticsTile extends StatelessWidget { return text; } - bool get hasGroup => - statistic.selectedGroups != null && statistic.selectedGroups!.isNotEmpty; + bool get hasGroup => selectedGroups != null && selectedGroups!.isNotEmpty; - bool get hasGame => - statistic.selectedGames != null && statistic.selectedGames!.isNotEmpty; + bool get hasGame => selectedGames != null && selectedGames!.isNotEmpty; } diff --git a/pubspec.yaml b/pubspec.yaml index d6fd994..b8b0aa8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tallee description: "Tracking App for Card Games" publish_to: 'none' -version: 0.0.33+276 +version: 0.0.33+280 environment: sdk: ^3.8.1