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": { "winner": {
"type": ["string","null"] "type": ["object","null"]
}, },
"required": [ "required": [
"id", "id",
"createdAt", "createdAt",
"name", "name"
"winner"
] ]
} }
] ]

View File

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

View File

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

View File

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

View File

@@ -69,9 +69,9 @@ class _GroupsViewState extends State<GroupsView> {
} }
final bool isLoading = final bool isLoading =
snapshot.connectionState == ConnectionState.waiting; snapshot.connectionState == ConnectionState.waiting;
final List<Group> groups = isLoading final List<Group> groups =
? skeletonData isLoading ? skeletonData : (snapshot.data ?? [])
: (snapshot.data ?? []); ..sort((a, b) => b.createdAt.compareTo(a.createdAt));
return Skeletonizer( return Skeletonizer(
effect: PulseEffect( effect: PulseEffect(
from: Colors.grey[800]!, from: Colors.grey[800]!,
@@ -93,7 +93,9 @@ class _GroupsViewState extends State<GroupsView> {
itemCount: groups.length + 1, itemCount: groups.length + 1,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
if (index == groups.length) { if (index == groups.length) {
return const SizedBox(height: 60); return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 20,
);
} }
return GroupTile(group: groups[index]); 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/game_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_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/tiles/quick_info_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
@@ -35,8 +34,7 @@ class _HomeViewState extends State<HomeView> {
Player(name: 'Skeleton Player 2'), Player(name: 'Skeleton Player 2'),
], ],
), ),
winner: winner: Player(name: 'Skeleton Player 1'),
"Winner ID", //TODO: Should be player object, but isnt yet, waiting for pr
), ),
); );
@@ -73,7 +71,7 @@ class _HomeViewState extends State<HomeView> {
enabled: isLoading, enabled: isLoading,
enableSwitchAnimation: true, enableSwitchAnimation: true,
switchAnimationConfig: SwitchAnimationConfig( switchAnimationConfig: SwitchAnimationConfig(
duration: Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
switchInCurve: Curves.linear, switchInCurve: Curves.linear,
switchOutCurve: Curves.linear, switchOutCurve: Curves.linear,
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder, transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
@@ -130,71 +128,80 @@ class _HomeViewState extends State<HomeView> {
), ),
], ],
), ),
FutureBuilder( Padding(
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: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.symmetric(vertical: 16.0),
child: InfoTile( child: InfoTile(
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
title: 'Recent Games', title: 'Recent Games',
icon: Icons.timer, icon: Icons.timer,
content: Padding( content: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.0), padding: const EdgeInsets.symmetric(horizontal: 40.0),
child: Column( 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, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
GameTile( GameTile(
gameTitle: games[0].name, gameTitle: games[0].name,
gameType: "Gametype", gameType: 'Winner',
ruleset: 'Ruleset', ruleset: 'Ruleset',
players: _getPlayerText(games[0]), players: _getPlayerText(games[0]),
winner: winner: games[0].winner == null
'Leonard', //TODO: Replace Winner with real Winner ? 'No winner set.'
: games[0].winner!.name,
), ),
Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0), padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(), child: Divider(),
), ),
if (games.length >= 2) ...[
GameTile( GameTile(
gameTitle: games[1].name, gameTitle: games[1].name,
gameType: 'Gametype', gameType: 'Winner',
ruleset: 'Ruleset', ruleset: 'Ruleset',
players: _getPlayerText(games[1]), players: _getPlayerText(games[1]),
winner: winner: games[1].winner == null
'Lina', //TODO: Replace Winner with real Winner ? '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( InfoTile(
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
title: 'Quick Create', title: 'Quick Create',
@@ -254,7 +261,8 @@ class _HomeViewState extends State<HomeView> {
String _getPlayerText(Game game) { String _getPlayerText(Game game) {
if (game.group == null) { 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) { if (game.players == null || game.players!.isEmpty) {
return game.group!.name; 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}); 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 @override
Widget build(BuildContext context) { 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', name: 'First Test Game',
group: testGroup1, group: testGroup1,
players: [testPlayer4, testPlayer5], players: [testPlayer4, testPlayer5],
winner: testPlayer4,
); );
testGame2 = Game( testGame2 = Game(
name: 'Second Test Game', name: 'Second Test Game',
group: testGroup2, group: testGroup2,
players: [testPlayer1, testPlayer2, testPlayer3], players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer2,
); );
testGameOnlyPlayers = Game( testGameOnlyPlayers = Game(
name: 'Test Game with Players', name: 'Test Game with Players',
players: [testPlayer1, testPlayer2, testPlayer3], players: [testPlayer1, testPlayer2, testPlayer3],
winner: testPlayer3,
); );
testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2); testGameOnlyGroup = Game(name: 'Test Game with Group', group: testGroup2);
}); });
@@ -75,9 +78,16 @@ void main() {
expect(result.id, testGame1.id); expect(result.id, testGame1.id);
expect(result.name, testGame1.name); expect(result.name, testGame1.name);
expect(result.winner, testGame1.winner);
expect(result.createdAt, testGame1.createdAt); 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) { if (result.group != null) {
expect(result.group!.members.length, testGroup1.members.length); expect(result.group!.members.length, testGroup1.members.length);
@@ -123,7 +133,13 @@ void main() {
expect(game.id, testGame.id); expect(game.id, testGame.id);
expect(game.name, testGame.name); expect(game.name, testGame.name);
expect(game.createdAt, testGame.createdAt); 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); expect(game.winner, testGame.winner);
}
// Group-Checks // Group-Checks
if (testGame.group != null) { if (testGame.group != null) {