From 2e3b46253388f10bdd63e84d2c859971d19bce45 Mon Sep 17 00:00:00 2001 From: Felix Kirchner Date: Sun, 24 May 2026 15:11:56 +0200 Subject: [PATCH] feat: added statistic tile factory --- .../statistic_tile_factory.dart | 318 ++++++++++++++++++ .../statistics_view/statistics_view.dart | 285 +++------------- .../widgets/tiles/statistics_tile.dart | 223 ++++++++---- 3 files changed, 529 insertions(+), 297 deletions(-) create mode 100644 lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart 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 new file mode 100644 index 0000000..c482a7d --- /dev/null +++ b/lib/presentation/views/main_menu/statistics_view/statistic_tile_factory.dart @@ -0,0 +1,318 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:tallee/core/enums.dart'; +import 'package:tallee/data/models/match.dart'; +import 'package:tallee/data/models/player.dart'; +import 'package:tallee/data/models/statistic.dart'; +import 'package:tallee/presentation/views/main_menu/statistics_view/create_statistic_view.dart' + show translateStatisticTypeToString; +import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart'; + +/// Build the [StatisticsTile] for a given [Statistic]. + +Widget buildStatisticTile({ + required Statistic statistic, + required List matches, + required List players, + required BuildContext context, + double? width, + int itemCount = 5, +}) { + final filteredMatches = _getFilterMatches(statistic, matches); + final filteredPlayers = _getFilteredPlayers( + statistic, + players, + filteredMatches, + ); + + print('Building tile for statistic: $statistic'); + print('Filtered matches count: ${filteredMatches.length}'); + print('Filtered players count: ${filteredPlayers.length}'); + + final values = _computeValuesForType( + type: statistic.type, + matches: filteredMatches, + players: filteredPlayers, + ); + print(values); + + return StatisticsTile( + icon: _getStatisticIcon(type: statistic.type), + title: translateStatisticTypeToString(statistic.type, context), + width: width ?? MediaQuery.sizeOf(context).width * 0.95, + values: values, + itemCount: itemCount, + barColor: _getStatisticColor(statistic), + statistic: statistic, + ); +} + +List _getFilterMatches(Statistic statistic, List matches) { + List filteredMatches = matches; + + // Filter timeframe + if (statistic.scopes.contains(StatisticScope.timeframe) && + statistic.timeframe != null) { + final minDate = _getMinimumDate(timeframe: statistic.timeframe!); + print( + 'Filtering matches by timeframe: ${statistic.timeframe}, minDate: $minDate', + ); + if (minDate != null) { + filteredMatches = matches + .where((m) => m.endedAt != null && m.endedAt!.isAfter(minDate)) + .toList(); + } + } + + // Filter games + if (statistic.scopes.contains(StatisticScope.selectedGames) && + (statistic.selectedGames?.isNotEmpty ?? false)) { + final gameIds = statistic.selectedGames!.map((g) => g.id).toSet(); + filteredMatches = filteredMatches + .where((match) => gameIds.contains(match.game.id)) + .toList(); + } + + // Filter groups + if (statistic.scopes.contains(StatisticScope.selectedGroups) && + (statistic.selectedGroups?.isNotEmpty ?? false)) { + final groupIds = statistic.selectedGroups!.map((g) => g.id).toSet(); + filteredMatches = filteredMatches + .where((m) => m.group != null && groupIds.contains(m.group!.id)) + .toList(); + } + + return filteredMatches; +} + +/// Returns a [Player] List with the selected players depending on +List _getFilteredPlayers( + Statistic statistic, + List allPlayers, + List filteredMatches, +) { + // allPlayers + if (statistic.scopes.contains(StatisticScope.allPlayers)) { + return allPlayers; + } + + // selectedGroups -> only members + if (statistic.scopes.contains(StatisticScope.selectedGroups) && + (statistic.selectedGroups?.isNotEmpty ?? false)) { + final Set ids = { + for (final g in statistic.selectedGroups!) + for (final p in g.members) p.id, + }; + return allPlayers.where((p) => ids.contains(p.id)).toList(); + } + + // Else -> all players from filtered matches + final Set ids = { + for (final m in filteredMatches) + for (final p in m.players) p.id, + }; + return allPlayers.where((p) => ids.contains(p.id)).toList(); +} + +/// Returns a [DateTime] with the minimum time and date the [timeframe] allows +DateTime? _getMinimumDate({required Timeframe timeframe}) { + final now = DateTime.now(); + switch (timeframe) { + case Timeframe.last7Days: + return now.subtract(const Duration(days: 7)); + case Timeframe.last30Days: + return now.subtract(const Duration(days: 30)); + case Timeframe.last90Days: + return now.subtract(const Duration(days: 90)); + case Timeframe.last180Days: + return now.subtract(const Duration(days: 180)); + case Timeframe.lastYear: + return now.subtract(const Duration(days: 365)); + case Timeframe.allTime: + return null; + } +} + +/// Computes the statistic values for each player based on the statistic type +/// and returns a list of (Player, value) tuples sorted descending by value. +List<(Player, num)> _computeValuesForType({ + required StatisticType type, + required List matches, + required List players, +}) { + switch (type) { + case StatisticType.totalMatches: + return _sortDesc( + players.map((p) => (p, _matchesPlayed(p, matches) as num)).toList(), + ); + + case StatisticType.totalWins: + return _sortDesc( + players.map((p) => (p, _wins(p, matches) as num)).toList(), + ); + + case StatisticType.totalLosses: + return _sortDesc( + players + .map( + (p) => + (p, (_matchesPlayed(p, matches) - _wins(p, matches)) as num), + ) + .toList(), + ); + + case StatisticType.totalScore: + return _sortDesc( + players.map((p) => (p, _totalScore(p, matches) as num)).toList(), + ); + + case StatisticType.averageScore: + return _sortDesc( + players.map((p) { + final scores = _scoresOf(p, matches); + final avg = scores.isEmpty + ? 0.0 + : double.parse( + (scores.reduce((a, b) => a + b) / scores.length) + .toStringAsFixed(2), + ); + return (p, avg as num); + }).toList(), + ); + + case StatisticType.bestScore: + return _sortDesc( + players.map((p) { + final scores = _scoresOf(p, matches); + final best = scores.isEmpty ? 0 : scores.reduce(max); + return (p, best as num); + }).toList(), + ); + + case StatisticType.worstScore: + // Ascending here is more meaningful for "worst", but keep the + // existing tile semantics (largest bar = top entry) by sorting + // descending on the inverse — i.e. show smallest score on top. + final entries = players.map((p) { + final scores = _scoresOf(p, matches); + final worst = scores.isEmpty ? 0 : scores.reduce(min); + return (p, worst as num); + }).toList(); + entries.sort((a, b) => a.$2.compareTo(b.$2)); + return entries; + + case StatisticType.winrate: + return _sortDesc( + players.map((p) { + final played = _matchesPlayed(p, matches); + final wins = _wins(p, matches); + final rate = played == 0 + ? 0.0 + : double.parse((wins / played).toStringAsFixed(2)); + return (p, rate as num); + }).toList(), + ); + } +} + +/* Helper functions for different statistic types */ + +/// Detemerines how many matches the player has played in the given list of matches. +int _matchesPlayed(Player p, List matches) => + matches.where((m) => m.players.any((mp) => mp.id == p.id)).length; + +/// Determines how many matches the player has won in the given list of matches. +int _wins(Player p, List matches) => + matches.where((m) => m.mvp.any((mp) => mp.id == p.id)).length; + +/// Determines the total score of the player in the given list of matches. +int _totalScore(Player p, List matches) { + var total = 0; + for (final m in matches) { + final s = m.scores[p.id]; + if (s != null) total += s.score; + } + return total; +} + +/// Returns a list of all scores the player has achieved in the given list of matches. +List _scoresOf(Player p, List matches) => [ + for (final m in matches) + if (m.scores[p.id] != null) m.scores[p.id]!.score, +]; + +/// Returns the list of entries sorted descending by the statistic value. +List<(Player, num)> _sortDesc(List<(Player, num)> entries) { + entries.sort((a, b) => b.$2.compareTo(a.$2)); + return entries; +} + +/* Icon and color */ + +/// Returns the icon for the given statistic type. +IconData _getStatisticIcon({required StatisticType type}) { + switch (type) { + case StatisticType.totalMatches: + return Icons.casino; + case StatisticType.totalWins: + return Icons.emoji_events; + case StatisticType.totalLosses: + return Icons.sentiment_dissatisfied; + case StatisticType.totalScore: + return Icons.scoreboard; + case StatisticType.averageScore: + return Icons.show_chart; + case StatisticType.bestScore: + return Icons.trending_up; + case StatisticType.worstScore: + return Icons.trending_down; + case StatisticType.winrate: + return Icons.percent; + } +} + +/// The bar colors for the statistic tiles +const List _palette = [ + Color(0xFF4CAF50), // green + Color(0xFF2196F3), // blue + Color(0xFFFF9800), // orange + Color(0xFFE91E63), // pink + Color(0xFF9C27B0), // purple + Color(0xFF00BCD4), // cyan + Color(0xFFFFC107), // amber + Color(0xFF3F51B5), // indigo + Color(0xFF8BC34A), // light green + Color(0xFFFF5722), // deep orange +]; + +/// Returns a color from the palette based on the statistic's ID as random seed. +Color _getStatisticColor(Statistic stat) { + final seed = stat.id.hashCode; + return _palette[seed.abs() % _palette.length]; +} + +/* Skeleton data */ + +/// A placeholder tile with mock data for the loading state. +Widget buildSkeletonStatisticTile({required BuildContext context}) { + final count = 4 + Random().nextInt(5); // 4..8 + final values = <(Player, num)>[ + for (var i = 0; i < count; i++) + (Player(name: 'Player ${i + 1}'), count - i), + ]; + + return StatisticsTile( + icon: Icons.bar_chart, + title: 'Skeleton title', + width: MediaQuery.sizeOf(context).width * 0.95, + values: values, + itemCount: count, + barColor: _palette[Random().nextInt(_palette.length)], + statistic: Statistic( + type: StatisticType.totalMatches, + scopes: [StatisticScope.allPlayers], + timeframe: Timeframe.last7Days, + ), + ); +} 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 f551a8b..8c3ac16 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,9 @@ 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_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/tiles/info_tile.dart'; class StatisticsView extends StatefulWidget { /// A view that displays player statistics @@ -21,36 +21,19 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { - int matchCount = 0; - int groupCount = 0; - - List<(Player, int)> winCounts = List.filled(6, ( - Player(name: 'Skeleton Player'), - 1, - )); - List<(Player, int)> matchCounts = List.filled(6, ( - Player(name: 'Skeleton Player'), - 1, - )); - List<(Player, double)> winRates = List.filled(6, ( - Player(name: 'Skeleton Player'), - 1, - )); bool isLoading = true; - List statisticTiles = List.generate( - 4, - (_) => const InfoTile( - icon: Icons.sports_score, - title: 'Skeleton Statistic', - width: double.infinity, - content: Text('Skeleton content'), - ), - ); + List _allMatches = const []; + List _allPlayers = const []; + List statisticTiles = []; @override void initState() { super.initState(); - getStatisticTiles(context); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + getStatisticTiles(context); + }); } @override @@ -66,13 +49,14 @@ class _StatisticsViewState extends State { child: AppSkeleton( enabled: isLoading, fixLayoutBuilder: true, - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: constraints.maxWidth), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [...statisticTiles], - ), + child: Column( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...statisticTiles, + SizedBox(height: MediaQuery.paddingOf(context).bottom + 80), + ], ), ), ), @@ -86,17 +70,16 @@ class _StatisticsViewState extends State { context, adaptivePageRoute( builder: (context) => CreateStatisticView( - onStatisticCreated: loadStatisticData, + onStatisticCreated: () => getStatisticTiles(context), ), ), ); - final newTile = InfoTile( - icon: Icons.sports_score, - title: newStatistic.type.name, - width: MediaQuery.sizeOf(context).width * 0.95, - content: Text( - '${newStatistic.id}\n${newStatistic.scopes}\n${newStatistic.type}\n${newStatistic.timeframe}\n${newStatistic.selectedGroups}\n${newStatistic.selectedGames}\n', - ), + if (!context.mounted) return; + final newTile = buildStatisticTile( + statistic: newStatistic, + matches: _allMatches, + players: _allPlayers, + context: context, ); setState(() { @@ -111,205 +94,47 @@ class _StatisticsViewState extends State { ); } - /// Loads matches and players from the database - /// and calculates statistics for each player - void loadStatisticData() { + Future getStatisticTiles(BuildContext context) async { + setState(() { + isLoading = true; + statisticTiles = List.generate( + 4, + (index) => Column( + children: [ + buildSkeletonStatisticTile(context: context), + const SizedBox(height: 12), + ], + ), + ); + }); + final db = Provider.of(context, listen: false); - Future.wait([ + final results = await Future.wait([ + db.statisticDao.getAllStatistics(), db.matchDao.getAllMatches(), db.playerDao.getAllPlayers(), - db.matchDao.getMatchCount(), - db.groupDao.getGroupCount(), Future.delayed(Constants.MINIMUM_SKELETON_DURATION), - ]).then((results) async { - if (!mounted) return; + ]); - final matches = results[0] as List; - final players = results[1] as List; - matchCount = results[2] as int; - groupCount = results[3] as int; + if (!mounted) return; - winCounts = _calculateWinsForAllPlayers( - matches: matches, - players: players, - context: context, - ); - matchCounts = _calculateMatchAmountsForAllPlayers( - matches: matches, - players: players, - context: context, - ); - winRates = computeWinRatePercent( - winCounts: winCounts, - matchCounts: matchCounts, - ); - - setState(() { - isLoading = false; - }); - }); - } - - Future getStatisticTiles(BuildContext context) async { - isLoading = true; - final db = Provider.of(context, listen: false); - final statistics = await db.statisticDao.getAllStatistics(); + final statistics = results[0] as List; + _allMatches = results[1] as List; + _allPlayers = results[2] as List; setState(() { - statisticTiles = []; - for (var statistic in statistics) { - statisticTiles.add( - InfoTile( - icon: Icons.sports_score, - title: statistic.type.name, - width: MediaQuery.sizeOf(context).width * 0.95, - content: Text(statistic.toString()), + statisticTiles = [ + for (final statistic in statistics) ...[ + buildStatisticTile( + statistic: statistic, + matches: _allMatches, + players: _allPlayers, + context: context, ), - ); - statisticTiles.add( - SizedBox(height: MediaQuery.sizeOf(context).height * 0.02), - ); - } + ], + ]; + isLoading = false; }); - isLoading = false; - } - - /// Calculates the number of wins for each player - /// and returns a sorted list of tuples (playerName, winCount) - List<(Player, int)> _calculateWinsForAllPlayers({ - required List matches, - required List players, - required BuildContext context, - }) { - List<(Player, int)> winCounts = []; - final loc = AppLocalizations.of(context); - - // Getting the winners - for (var match in matches) { - final mvps = match.mvp; - for (var winner in mvps) { - final index = winCounts.indexWhere((entry) => entry.$1.id == winner.id); - // -1 means winner not found in winCounts - if (index != -1) { - final current = winCounts[index].$2; - winCounts[index] = (winner, current + 1); - } else { - winCounts.add((winner, 1)); - } - } - } - - // Adding all players with zero wins - for (var player in players) { - final index = winCounts.indexWhere((entry) => entry.$1.id == player.id); - // -1 means player not found in winCounts - if (index == -1) { - winCounts.add((player, 0)); - } - } - - // Replace player IDs with names - for (int i = 0; i < winCounts.length; i++) { - final playerId = winCounts[i].$1.id; - final player = players.firstWhere( - (p) => p.id == playerId, - orElse: () => Player(id: playerId, name: loc.not_available), - ); - winCounts[i] = (player, winCounts[i].$2); - } - - winCounts.sort((a, b) => b.$2.compareTo(a.$2)); - - return winCounts; - } - - /// Calculates the number of matches played for each player - /// and returns a sorted list of tuples (playerName, matchCount) - List<(Player, int)> _calculateMatchAmountsForAllPlayers({ - required List matches, - required List players, - required BuildContext context, - }) { - List<(Player, int)> matchCounts = []; - final loc = AppLocalizations.of(context); - - // Counting matches for each player - for (var match in matches) { - for (Player player in match.players) { - // Check if the player is already in matchCounts - final index = matchCounts.indexWhere( - (entry) => entry.$1.id == player.id, - ); - - // -1 -> not found - if (index == -1) { - // Add new entry - matchCounts.add((player, 1)); - } else { - // Update existing entry - final currentMatchAmount = matchCounts[index].$2; - matchCounts[index] = (player, currentMatchAmount + 1); - } - } - } - - // Adding all players with zero matches - for (var player in players) { - final index = matchCounts.indexWhere((entry) => entry.$1.id == player.id); - // -1 means player not found in matchCounts - if (index == -1) { - matchCounts.add((player, 0)); - } - } - - // Replace player IDs with names - for (int i = 0; i < matchCounts.length; i++) { - final playerId = matchCounts[i].$1.id; - final player = players.firstWhere( - (p) => p.id == playerId, - orElse: () => Player(id: playerId, name: loc.not_available), - ); - matchCounts[i] = (player, matchCounts[i].$2); - } - - matchCounts.sort((a, b) => b.$2.compareTo(a.$2)); - - return matchCounts; - } - - List<(Player, double)> computeWinRatePercent({ - required List<(Player, int)> winCounts, - required List<(Player, int)> matchCounts, - }) { - final Map winsMap = {for (var e in winCounts) e.$1: e.$2}; - final Map matchesMap = {for (var e in matchCounts) e.$1: e.$2}; - - // Get all unique player names - final player = {...matchesMap.keys}; - - // Calculate win rates - final result = player.map((name) { - final int w = winsMap[name] ?? 0; - final int m = matchesMap[name] ?? 0; - // Calculate percentage and round to 2 decimal places - // Avoid division by zero - final double percent = (m > 0) - ? double.parse(((w / m)).toStringAsFixed(2)) - : 0; - return (name, percent); - }).toList(); - - // Sort the result: first by winrate descending, - // then by wins descending in case of a tie - result.sort((a, b) { - final cmp = b.$2.compareTo(a.$2); - if (cmp != 0) return cmp; - final wa = winsMap[a.$1] ?? 0; - final wb = winsMap[b.$1] ?? 0; - return wb.compareTo(wa); - }); - - return result; } } diff --git a/lib/presentation/widgets/tiles/statistics_tile.dart b/lib/presentation/widgets/tiles/statistics_tile.dart index ea9cb49..b740eb5 100644 --- a/lib/presentation/widgets/tiles/statistics_tile.dart +++ b/lib/presentation/widgets/tiles/statistics_tile.dart @@ -1,9 +1,13 @@ import 'dart:math'; import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:tallee/core/common.dart'; import 'package:tallee/core/custom_theme.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'; @@ -23,6 +27,7 @@ class StatisticsTile extends StatelessWidget { required this.values, required this.itemCount, required this.barColor, + required this.statistic, }); /// The icon displayed next to the title. @@ -43,6 +48,8 @@ class StatisticsTile extends StatelessWidget { /// The color of the bars representing the values. final Color barColor; + final Statistic statistic; + @override Widget build(BuildContext context) { final loc = AppLocalizations.of(context); @@ -52,7 +59,7 @@ class StatisticsTile extends StatelessWidget { title: title, icon: icon, content: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Visibility( visible: values.isNotEmpty, replacement: Center( @@ -63,80 +70,146 @@ class StatisticsTile extends StatelessWidget { builder: (context, constraints) { final maxBarWidth = constraints.maxWidth * 0.65; return Column( - children: List.generate(min(values.length, itemCount), (index) { - /// The maximum wins among all players - final maxMatches = values.isNotEmpty ? values[0].$2 : 0; + children: [ + // Bar chart + ...List.generate(min(values.length, itemCount), (index) { + /// The maximum wins among all players + final maxMatches = values.isNotEmpty ? values[0].$2 : 0; - /// Fraction of wins - final double fraction = (maxMatches > 0) - ? (values[index].$2 / maxMatches) - : 0.0; + /// Fraction of wins + final double fraction = (maxMatches > 0) + ? (values[index].$2 / maxMatches) + : 0.0; - /// Calculated width for current the bar - final double barWidth = maxBarWidth * fraction; + /// Calculated width for current the bar + final double barWidth = maxBarWidth * fraction; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Stack( - children: [ - Container( - height: 24, - width: barWidth, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - color: barColor, - ), - ), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: RichText( - overflow: TextOverflow.ellipsis, - text: TextSpan( - style: DefaultTextStyle.of(context).style, - children: [ - TextSpan( - text: values[index].$1.name, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - TextSpan( - text: getNameCountText(values[index].$1), - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: CustomTheme.textColor.withAlpha( - 150, - ), - ), - ), - ], + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Stack( + children: [ + // Bar + Container( + height: 24, + width: barWidth, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: barColor, ), ), - ), - ], - ), - const Spacer(), - Center( - child: Text( - values[index].$2 <= 1 && values[index].$2 is double - ? values[index].$2.toStringAsFixed(2) - : values[index].$2.toString(), - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + + // Player + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: RichText( + overflow: TextOverflow.ellipsis, + text: TextSpan( + style: DefaultTextStyle.of(context).style, + children: [ + TextSpan( + text: values[index].$1.name, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: getNameCountText( + values[index].$1, + ), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.textColor + .withAlpha(150), + ), + ), + ], + ), + ), + ), + ], + ), + const Spacer(), + + // Value + Center( + child: Text( + values[index].$2 <= 1 && + values[index].$2 is double + ? values[index].$2.toStringAsFixed(2) + : values[index].$2.toString(), + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ), - ), - ], + ], + ), + ); + }), + + // Group & Game info + if (statistic.selectedGames != null || + statistic.selectedGroups != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + // Game + if (statistic.selectedGames != null && + statistic.selectedGames!.isNotEmpty) + Row( + spacing: 8, + children: [ + const Icon( + RpgAwesome.clovers_card, + color: CustomTheme.hintColor, + size: 20, + ), + Text( + getGameText(statistic.selectedGames!), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.hintColor, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + + // Group + if (statistic.selectedGroups != null && + statistic.selectedGroups!.isNotEmpty) + Row( + spacing: 8, + children: [ + const Icon( + Icons.groups, + color: CustomTheme.hintColor, + ), + Text( + getGroupText(statistic.selectedGroups!), + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: CustomTheme.hintColor, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), ), - ); - }), + ], ); }, ), @@ -144,4 +217,20 @@ class StatisticsTile extends StatelessWidget { ), ); } + + String getGroupText(List groups) { + var text = groups[0].name; + if (groups.length > 1) { + return '$text + ${groups.length - 1}'; + } + return text; + } + + String getGameText(List games) { + var text = games[0].name; + if (games.length > 1) { + return '$text + ${games.length - 1}'; + } + return text; + } }