27 Commits

Author SHA1 Message Date
def37aa640 Refactor Recent Games tile in HomeView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m7s
- Move FutureBuilder inside InfoTile content
- Replace hardcoded winner and game type strings with actual game data
- Limit displayed recent games to 2 items and handle cases with fewer games
- Update player text generation to show player count instead of names
- Remove TopCenteredMessage usage and replace with simple text for empty/error states
- Update skeleton data to use Player object for winner
2025-11-23 14:56:53 +01:00
b744b04648 Merge remote-tracking branch 'origin/development' into enhancement/46-homeview-mockdaten-entfernen 2025-11-23 14:02:55 +01:00
17e882986d Merge pull request 'StatisticsView erstellen' (#30) from feature/6-statisticsview-erstellen into development
Reviewed-on: #30
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-23 12:59:40 +00:00
7cda25a380 Changed item count back to normal
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m19s
Pull Request Pipeline / lint (pull_request) Successful in 2m23s
2025-11-23 12:34:42 +01:00
e9b041e43a Changed double depiction
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m20s
Pull Request Pipeline / lint (pull_request) Successful in 2m23s
2025-11-23 12:33:13 +01:00
c38c731b41 Merge branch 'development' into feature/6-statisticsview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m25s
Pull Request Pipeline / lint (pull_request) Successful in 2m25s
# Conflicts:
#	lib/presentation/views/main_menu/statistics_view.dart
2025-11-23 12:19:03 +01:00
d411f58134 Changed icon for second statistics tile
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m21s
Pull Request Pipeline / lint (pull_request) Successful in 2m22s
2025-11-23 12:18:05 +01:00
fee5c57207 Added comments for return value -1 2025-11-23 12:13:30 +01:00
de60c942ea Merge pull request 'Winner als Player-Objekte' (#52) from feature/50-winner-soll-player-objekt-sein into development
Reviewed-on: #52
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
2025-11-23 10:48:45 +00:00
acc5b0a3e9 Merge branch 'development' into feature/6-statisticsview-erstellen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m33s
2025-11-23 00:36:33 +01:00
24babe06d2 Merge branch 'development' into feature/50-winner-soll-player-objekt-sein
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m34s
2025-11-23 00:36:08 +01:00
50dd05ecc5 Merge pull request 'Erstelle Spieler direkt zu ausgewählten Spielern, Gruppen Sortierung nach Timestamp + Bugfixes' (#49) from feature/47-spieler-zur-gruppe-und-gruppen-sortierung into development
Reviewed-on: #49
Reviewed-by: Felix Kirchner <felix.kirchner.fk@gmail.com>
2025-11-22 23:34:47 +00:00
4ff131770e Adjust tests
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m6s
2025-11-23 00:22:53 +01:00
82b344a145 Changed winner access in statistics view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m9s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2025-11-23 00:17:48 +01:00
338f4294dc Updated json schema 2025-11-23 00:17:31 +01:00
cfed05595c Updated methods in gameDao 2025-11-23 00:17:27 +01:00
fa841e328e Altered game class 2025-11-23 00:17:11 +01:00
feb5fa0615 Docs, small changes
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m13s
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
2025-11-22 23:30:24 +01:00
fba35521cb changed skeletonizer transition duration back to normal 2025-11-22 23:23:02 +01:00
e60961730f Added new metric & changed layout builder of Skeletonizer 2025-11-22 23:20:58 +01:00
59c041699d Changed values attribute & maxBarWidth 2025-11-22 23:20:31 +01:00
c170aa1775 sort groups by creation date in GroupsView
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m12s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2025-11-22 22:55:19 +01:00
692b412fe2 Fix black bars on the screens bottom and top by not wrapping scaffold in safearea, but scaffolds body children 2025-11-22 22:52:09 +01:00
cc04e05557 Adjust bottom padding in GroupsView list based on media query padding 2025-11-22 22:49:28 +01:00
546a3e3717 implemented feature to automatically add newly created player to selected players 2025-11-22 22:49:17 +01:00
b2036e4e68 Implemented first version of statistics view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m4s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2025-11-22 22:10:16 +01:00
310b9aa43b Implemented StatisticsWidget tile 2025-11-22 22:10:02 +01:00
9 changed files with 512 additions and 133 deletions

View File

@@ -88,13 +88,12 @@
]
},
"winner": {
"type": ["string","null"]
"type": ["object","null"]
},
"required": [
"id",
"createdAt",
"name",
"winner"
"name"
]
}
]

View File

@@ -20,13 +20,16 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
result.map((row) async {
final group = await db.groupGameDao.getGroupOfGame(gameId: row.id);
final players = await db.playerGameDao.getPlayersOfGame(gameId: row.id);
final winner = row.winnerId != null
? await db.playerDao.getPlayerById(playerId: row.winnerId!)
: null;
return Game(
id: row.id,
name: row.name,
group: group,
players: players,
createdAt: row.createdAt,
winner: row.winnerId,
winner: winner,
);
}),
);
@@ -45,13 +48,17 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
if (await db.groupGameDao.gameHasGroup(gameId: gameId)) {
group = await db.groupGameDao.getGroupOfGame(gameId: gameId);
}
Player? winner;
if (result.winnerId != null) {
winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
}
return Game(
id: result.id,
name: result.name,
players: players,
group: group,
winner: result.winnerId,
winner: winner,
createdAt: result.createdAt,
);
}
@@ -64,7 +71,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
GameTableCompanion.insert(
id: game.id,
name: game.name,
winnerId: Value(game.winner),
winnerId: Value(game.winner?.id),
createdAt: game.createdAt,
),
mode: InsertMode.insertOrReplace,
@@ -100,7 +107,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
id: game.id,
name: game.name,
createdAt: game.createdAt,
winnerId: Value(game.winner),
winnerId: Value(game.winner?.id),
),
)
.toList(),

View File

@@ -9,7 +9,7 @@ class Game {
final String name;
final List<Player>? players;
final Group? group;
final String? winner;
final Player? winner;
Game({
String? id,
@@ -17,7 +17,7 @@ class Game {
required this.name,
this.players,
this.group,
this.winner = '',
this.winner,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
@@ -37,7 +37,7 @@ class Game {
.toList()
: null,
group = json['group'] != null ? Group.fromJson(json['group']) : null,
winner = json['winner'] ?? '';
winner = json['winner'] != null ? Player.fromJson(json['winner']) : null;
/// Converts the Game instance to a JSON object.
Map<String, dynamic> toJson() => {
@@ -46,6 +46,6 @@ class Game {
'name': name,
'players': players?.map((player) => player.toJson()).toList(),
'group': group?.toJson(),
'winner': winner,
'winner': winner?.toJson(),
};
}

View File

@@ -49,8 +49,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
@override
void dispose() {
_groupNameController.dispose();
_searchBarController
.dispose(); // Listener entfernen und Controller aufräumen
_searchBarController.dispose();
super.dispose();
}
@@ -67,8 +66,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
@override
Widget build(BuildContext context) {
return SafeArea(
child: Scaffold(
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
@@ -79,7 +77,8 @@ class _CreateGroupViewState extends State<CreateGroupView> {
),
centerTitle: true,
),
body: Column(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
@@ -123,12 +122,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
.trim()
.isNotEmpty,
onTrailingButtonPressed: () async {
addNewPlayerFromSearch(
context: context,
searchBarController: _searchBarController,
db: db,
loadPlayerList: loadPlayerList,
);
addNewPlayerFromSearch(context: context);
},
onChanged: (value) {
setState(() {
@@ -339,25 +333,24 @@ class _CreateGroupViewState extends State<CreateGroupView> {
),
);
}
}
/// Adds a new player to the database from the search bar input.
/// Shows a snackbar indicating success or failure.
/// [context] - BuildContext to show the snackbar.
/// [searchBarController] - TextEditingController of the search bar.
/// [db] - AppDatabase instance to interact with the database.
/// [loadPlayerList] - Function to reload the player list after adding.
void addNewPlayerFromSearch({
required BuildContext context,
required TextEditingController searchBarController,
required AppDatabase db,
required Function loadPlayerList,
}) async {
String playerName = searchBarController.text.trim();
bool success = await db.playerDao.addPlayer(player: Player(name: playerName));
void addNewPlayerFromSearch({required BuildContext context}) async {
String playerName = _searchBarController.text.trim();
Player createdPlayer = Player(name: playerName);
bool success = await db.playerDao.addPlayer(player: createdPlayer);
if (!context.mounted) return;
if (success) {
loadPlayerList();
selectedPlayers.add(createdPlayer);
allPlayers.add(createdPlayer);
setState(() {
_searchBarController.clear();
suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player);
}).toList();
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
@@ -369,7 +362,6 @@ void addNewPlayerFromSearch({
),
),
);
searchBarController.clear();
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@@ -384,3 +376,4 @@ void addNewPlayerFromSearch({
);
}
}
}

View File

@@ -69,9 +69,9 @@ class _GroupsViewState extends State<GroupsView> {
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Group> groups = isLoading
? skeletonData
: (snapshot.data ?? []);
final List<Group> groups =
isLoading ? skeletonData : (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return Skeletonizer(
effect: PulseEffect(
from: Colors.grey[800]!,
@@ -93,7 +93,9 @@ class _GroupsViewState extends State<GroupsView> {
itemCount: groups.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == groups.length) {
return const SizedBox(height: 60);
return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 20,
);
}
return GroupTile(group: groups[index]);
},

View File

@@ -7,7 +7,6 @@ import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.da
import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
@@ -35,8 +34,7 @@ class _HomeViewState extends State<HomeView> {
Player(name: 'Skeleton Player 2'),
],
),
winner:
"Winner ID", //TODO: Should be player object, but isnt yet, waiting for pr
winner: Player(name: 'Skeleton Player 1'),
),
);
@@ -73,7 +71,7 @@ class _HomeViewState extends State<HomeView> {
enabled: isLoading,
enableSwitchAnimation: true,
switchAnimationConfig: SwitchAnimationConfig(
duration: Duration(milliseconds: 200),
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.linear,
switchOutCurve: Curves.linear,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
@@ -130,71 +128,80 @@ class _HomeViewState extends State<HomeView> {
),
],
),
FutureBuilder(
future: _recentGamesFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
icon: Icons.report,
title: 'Error',
message: 'Group data couldn\'t\nbe loaded.',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
child: TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'No games created yet.',
),
);
}
final List<Game> games =
isLoading ? skeletonData : (snapshot.data ?? [])
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return Padding(
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: InfoTile(
width: constraints.maxWidth * 0.95,
title: 'Recent Games',
icon: Icons.timer,
content: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0),
child: Column(
padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: FutureBuilder(
future: _recentGamesFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(
heightFactor: 4,
child: Text('Error while loading recent games.'),
);
}
if (snapshot.connectionState ==
ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
heightFactor: 4,
child: Text('No recent games available.'),
);
}
final List<Game> games =
(isLoading ? skeletonData : (snapshot.data ?? [])
..sort(
(a, b) =>
b.createdAt.compareTo(a.createdAt),
))
.take(2)
.toList();
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GameTile(
gameTitle: games[0].name,
gameType: "Gametype",
gameType: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(games[0]),
winner:
'Leonard', //TODO: Replace Winner with real Winner
winner: games[0].winner == null
? 'No winner set.'
: games[0].winner!.name,
),
Padding(
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(),
),
if (games.length >= 2) ...[
GameTile(
gameTitle: games[1].name,
gameType: 'Gametype',
gameType: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(games[1]),
winner:
'Lina', //TODO: Replace Winner with real Winner
winner: games[1].winner == null
? 'No winner set.'
: games[1].winner!.name,
),
const SizedBox(height: 8),
] else ...[
const Center(
heightFactor: 4,
child: Text('No second game available.'),
),
SizedBox(height: 8),
],
),
),
),
],
);
},
),
),
),
),
InfoTile(
width: constraints.maxWidth * 0.95,
title: 'Quick Create',
@@ -254,7 +261,8 @@ class _HomeViewState extends State<HomeView> {
String _getPlayerText(Game game) {
if (game.group == null) {
return game.players?.map((p) => p.name).join(', ') ?? 'No Players';
final playerCount = game.players?.length ?? 0;
return '$playerCount Player(s)';
}
if (game.players == null || game.players!.isEmpty) {
return game.group!.name;

View File

@@ -1,10 +1,262 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
class StatisticsView extends StatelessWidget {
class StatisticsView extends StatefulWidget {
const StatisticsView({super.key});
@override
State<StatisticsView> createState() => _StatisticsViewState();
}
class _StatisticsViewState extends State<StatisticsView> {
late Future<List<Game>> _gamesFuture;
late Future<List<Player>> _playersFuture;
List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1));
List<(String, int)> gameCounts = List.filled(6, ('Skeleton Player', 1));
List<(String, double)> winRates = List.filled(6, ('Skeleton Player', 1));
bool isLoading = true;
@override
void initState() {
super.initState();
final db = Provider.of<AppDatabase>(context, listen: false);
_gamesFuture = db.gameDao.getAllGames();
_playersFuture = db.playerDao.getAllPlayers();
Future.wait([_gamesFuture, _playersFuture]).then((results) async {
await Future.delayed(const Duration(milliseconds: 200));
final games = results[0] as List<Game>;
final players = results[1] as List<Player>;
winCounts = _calculateWinsForAllPlayers(games, players);
gameCounts = _calculateGameAmountsForAllPlayers(games, players);
winRates = computeWinRatePercent(wins: winCounts, games: gameCounts);
if (mounted) {
setState(() {
isLoading = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return const Center(child: Text('Statistics View'));
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView(
child: Skeletonizer(
effect: PulseEffect(
from: Colors.grey[800]!,
to: Colors.grey[600]!,
duration: const Duration(milliseconds: 800),
),
enabled: isLoading,
enableSwitchAnimation: true,
switchAnimationConfig: SwitchAnimationConfig(
duration: const Duration(milliseconds: 200),
switchInCurve: Curves.linear,
switchOutCurve: Curves.linear,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
layoutBuilder:
(Widget? currentChild, List<Widget> previousChildren) {
return Stack(
alignment: Alignment.topCenter,
children: [
...previousChildren,
if (currentChild != null) currentChild,
],
);
},
),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: constraints.maxWidth),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: constraints.maxHeight * 0.01),
StatisticsTile(
icon: Icons.sports_score,
title: 'Wins per Player',
width: constraints.maxWidth * 0.95,
values: winCounts,
itemCount: 3,
barColor: Colors.blue,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
icon: Icons.percent,
title: 'Winrate per Player',
width: constraints.maxWidth * 0.95,
values: winRates,
itemCount: 5,
barColor: Colors.orange[700]!,
),
SizedBox(height: constraints.maxHeight * 0.02),
StatisticsTile(
icon: Icons.casino,
title: 'Games per Player',
width: constraints.maxWidth * 0.95,
values: gameCounts,
itemCount: 10,
barColor: Colors.green,
),
SizedBox(height: MediaQuery.paddingOf(context).bottom),
],
),
),
),
);
},
);
}
/// Calculates the number of wins for each player
/// and returns a sorted list of tuples (playerName, winCount)
List<(String, int)> _calculateWinsForAllPlayers(
List<Game> games,
List<Player> players,
) {
List<(String, int)> winCounts = [];
// Getting the winners
for (var game in games) {
final winner = game.winner;
if (winner != null) {
final index = winCounts.indexWhere((entry) => entry.$1 == winner.id);
// -1 means winner not found in winCounts
if (index != -1) {
final current = winCounts[index].$2;
winCounts[index] = (winner.id, current + 1);
} else {
winCounts.add((winner.id, 1));
}
}
}
// Adding all players with zero wins
for (var player in players) {
final index = winCounts.indexWhere((entry) => entry.$1 == player.id);
// -1 means player not found in winCounts
if (index == -1) {
winCounts.add((player.id, 0));
}
}
// Replace player IDs with names
for (int i = 0; i < winCounts.length; i++) {
final playerId = winCounts[i].$1;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () => Player(id: playerId, name: 'N.a.'),
);
winCounts[i] = (player.name, winCounts[i].$2);
}
winCounts.sort((a, b) => b.$2.compareTo(a.$2));
return winCounts;
}
/// Calculates the number of games played for each player
/// and returns a sorted list of tuples (playerName, gameCount)
List<(String, int)> _calculateGameAmountsForAllPlayers(
List<Game> games,
List<Player> players,
) {
List<(String, int)> gameCounts = [];
// Counting games for each player
for (var game in games) {
if (game.group != null) {
final members = game.group!.members.map((p) => p.id).toList();
for (var playerId in members) {
final index = gameCounts.indexWhere((entry) => entry.$1 == playerId);
// -1 means player not found in gameCounts
if (index != -1) {
final current = gameCounts[index].$2;
gameCounts[index] = (playerId, current + 1);
} else {
gameCounts.add((playerId, 1));
}
}
}
if (game.players != null) {
final members = game.players!.map((p) => p.id).toList();
for (var playerId in members) {
final index = gameCounts.indexWhere((entry) => entry.$1 == playerId);
// -1 means player not found in gameCounts
if (index != -1) {
final current = gameCounts[index].$2;
gameCounts[index] = (playerId, current + 1);
} else {
gameCounts.add((playerId, 1));
}
}
}
}
// Adding all players with zero games
for (var player in players) {
final index = gameCounts.indexWhere((entry) => entry.$1 == player.id);
// -1 means player not found in gameCounts
if (index == -1) {
gameCounts.add((player.id, 0));
}
}
// Replace player IDs with names
for (int i = 0; i < gameCounts.length; i++) {
final playerId = gameCounts[i].$1;
final player = players.firstWhere(
(p) => p.id == playerId,
orElse: () => Player(id: playerId, name: 'N.a.'),
);
gameCounts[i] = (player.name, gameCounts[i].$2);
}
gameCounts.sort((a, b) => b.$2.compareTo(a.$2));
return gameCounts;
}
// dart
List<(String, double)> computeWinRatePercent({
required List<(String, int)> wins,
required List<(String, int)> games,
}) {
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
final Map<String, int> gamesMap = {for (var e in games) e.$1: e.$2};
// Get all unique player names
final names = {...winsMap.keys, ...gamesMap.keys};
// Calculate win rates
final result = names.map((name) {
final int w = winsMap[name] ?? 0;
final int g = gamesMap[name] ?? 0;
// Calculate percentage and round to 2 decimal places
// Avoid division by zero
final double percent = (g > 0)
? double.parse(((w / g)).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;
}
}

View File

@@ -0,0 +1,102 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
class StatisticsTile extends StatelessWidget {
const StatisticsTile({
super.key,
required this.icon,
required this.title,
required this.width,
required this.values,
required this.itemCount,
required this.barColor,
});
final IconData icon;
final String title;
final double width;
final List<(String, num)> values;
final int itemCount;
final Color barColor;
@override
Widget build(BuildContext context) {
final maxBarWidth = MediaQuery.of(context).size.width * 0.65;
return InfoTile(
width: width,
title: title,
icon: icon,
content: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Visibility(
visible: values.isNotEmpty,
replacement: const Center(
heightFactor: 4,
child: Text('No data available.'),
),
child: Column(
children: List.generate(min(values.length, itemCount), (index) {
/// The maximum wins among all players
final maxGames = values.isNotEmpty ? values[0].$2 : 0;
/// Fraction of wins
final double fraction = (maxGames > 0)
? (values[index].$2 / maxGames)
: 0.0;
/// 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: Text(
values[index].$1,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
const Spacer(),
Center(
child: Text(
values[index].$2 <= 1
? values[index].$2.toStringAsFixed(2)
: values[index].$2.toString(),
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
],
),
);
}),
),
),
),
);
}
}

View File

@@ -50,15 +50,18 @@ void main() {
name: 'First Test Game',
group: testGroup1,
players: [testPlayer4, testPlayer5],
winner: testPlayer4,
);
testGame2 = Game(
name: 'Second Test Game',
group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer2,
);
testGameOnlyPlayers = Game(
name: 'Test Game with Players',
players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer3,
);
testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2);
});
@@ -75,9 +78,16 @@ void main() {
expect(result.id, testGame1.id);
expect(result.name, testGame1.name);
expect(result.winner, testGame1.winner);
expect(result.createdAt, testGame1.createdAt);
if (result.winner != null && testGame1.winner != null) {
expect(result.winner!.id, testGame1.winner!.id);
expect(result.winner!.name, testGame1.winner!.name);
expect(result.winner!.createdAt, testGame1.winner!.createdAt);
} else {
expect(result.winner, testGame1.winner);
}
if (result.group != null) {
expect(result.group!.members.length, testGroup1.members.length);
@@ -123,7 +133,13 @@ void main() {
expect(game.id, testGame.id);
expect(game.name, testGame.name);
expect(game.createdAt, testGame.createdAt);
if (game.winner != null && testGame.winner != null) {
expect(game.winner!.id, testGame.winner!.id);
expect(game.winner!.name, testGame.winner!.name);
expect(game.winner!.createdAt, testGame.winner!.createdAt);
} else {
expect(game.winner, testGame.winner);
}
// Group-Checks
if (testGame.group != null) {