Compare commits
27 Commits
f658a88849
...
def37aa640
| Author | SHA1 | Date | |
|---|---|---|---|
| def37aa640 | |||
| b744b04648 | |||
| 17e882986d | |||
| 7cda25a380 | |||
| e9b041e43a | |||
| c38c731b41 | |||
| d411f58134 | |||
| fee5c57207 | |||
| de60c942ea | |||
| acc5b0a3e9 | |||
| 24babe06d2 | |||
| 50dd05ecc5 | |||
| 4ff131770e | |||
| 82b344a145 | |||
| 338f4294dc | |||
| cfed05595c | |||
| fa841e328e | |||
| feb5fa0615 | |||
| fba35521cb | |||
| e60961730f | |||
| 59c041699d | |||
| c170aa1775 | |||
| 692b412fe2 | |||
| cc04e05557 | |||
| 546a3e3717 | |||
| b2036e4e68 | |||
| 310b9aa43b |
@@ -88,13 +88,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"winner": {
|
"winner": {
|
||||||
"type": ["string","null"]
|
"type": ["object","null"]
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id",
|
"id",
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"name",
|
"name"
|
||||||
"winner"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,19 +66,19 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SafeArea(
|
return Scaffold(
|
||||||
child: Scaffold(
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
backgroundColor: CustomTheme.backgroundColor,
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
appBar: AppBar(
|
scrolledUnderElevation: 0,
|
||||||
backgroundColor: CustomTheme.backgroundColor,
|
title: const Text(
|
||||||
scrolledUnderElevation: 0,
|
'Create new group',
|
||||||
title: const Text(
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
'Create new group',
|
|
||||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
),
|
),
|
||||||
body: Column(
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
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,48 +333,47 @@ 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,
|
if (!context.mounted) return;
|
||||||
required TextEditingController searchBarController,
|
if (success) {
|
||||||
required AppDatabase db,
|
selectedPlayers.add(createdPlayer);
|
||||||
required Function loadPlayerList,
|
allPlayers.add(createdPlayer);
|
||||||
}) async {
|
setState(() {
|
||||||
String playerName = searchBarController.text.trim();
|
_searchBarController.clear();
|
||||||
bool success = await db.playerDao.addPlayer(player: Player(name: playerName));
|
suggestedPlayers = allPlayers.where((player) {
|
||||||
if (!context.mounted) return;
|
return !selectedPlayers.contains(player);
|
||||||
if (success) {
|
}).toList();
|
||||||
loadPlayerList();
|
});
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
backgroundColor: CustomTheme.boxColor,
|
backgroundColor: CustomTheme.boxColor,
|
||||||
content: Center(
|
content: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Successfully added player $playerName.',
|
'Successfully added player $playerName.',
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
} else {
|
||||||
searchBarController.clear();
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
} else {
|
SnackBar(
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
backgroundColor: CustomTheme.boxColor,
|
||||||
SnackBar(
|
content: Center(
|
||||||
backgroundColor: CustomTheme.boxColor,
|
child: Text(
|
||||||
content: Center(
|
'Could not add player $playerName.',
|
||||||
child: Text(
|
style: const TextStyle(color: Colors.white),
|
||||||
'Could not add player $playerName.',
|
),
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,70 +128,79 @@ class _HomeViewState extends State<HomeView> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
FutureBuilder(
|
Padding(
|
||||||
future: _recentGamesFuture,
|
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||||
builder: (context, snapshot) {
|
child: InfoTile(
|
||||||
if (snapshot.hasError) {
|
width: constraints.maxWidth * 0.95,
|
||||||
return const Center(
|
title: 'Recent Games',
|
||||||
child: TopCenteredMessage(
|
icon: Icons.timer,
|
||||||
icon: Icons.report,
|
content: Padding(
|
||||||
title: 'Error',
|
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||||
message: 'Group data couldn\'t\nbe loaded.',
|
child: FutureBuilder(
|
||||||
),
|
future: _recentGamesFuture,
|
||||||
);
|
builder: (context, snapshot) {
|
||||||
}
|
if (snapshot.hasError) {
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
return const Center(
|
||||||
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
heightFactor: 4,
|
||||||
return const Center(
|
child: Text('Error while loading recent games.'),
|
||||||
child: TopCenteredMessage(
|
);
|
||||||
icon: Icons.info,
|
}
|
||||||
title: 'Info',
|
if (snapshot.connectionState ==
|
||||||
message: 'No games created yet.',
|
ConnectionState.done &&
|
||||||
),
|
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
||||||
);
|
return const Center(
|
||||||
}
|
heightFactor: 4,
|
||||||
final List<Game> games =
|
child: Text('No recent games available.'),
|
||||||
isLoading ? skeletonData : (snapshot.data ?? [])
|
);
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
}
|
||||||
return Padding(
|
final List<Game> games =
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
(isLoading ? skeletonData : (snapshot.data ?? [])
|
||||||
child: InfoTile(
|
..sort(
|
||||||
width: constraints.maxWidth * 0.95,
|
(a, b) =>
|
||||||
title: 'Recent Games',
|
b.createdAt.compareTo(a.createdAt),
|
||||||
icon: Icons.timer,
|
))
|
||||||
content: Padding(
|
.take(2)
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
.toList();
|
||||||
child: Column(
|
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(),
|
||||||
),
|
),
|
||||||
GameTile(
|
if (games.length >= 2) ...[
|
||||||
gameTitle: games[1].name,
|
GameTile(
|
||||||
gameType: 'Gametype',
|
gameTitle: games[1].name,
|
||||||
ruleset: 'Ruleset',
|
gameType: 'Winner',
|
||||||
players: _getPlayerText(games[1]),
|
ruleset: 'Ruleset',
|
||||||
winner:
|
players: _getPlayerText(games[1]),
|
||||||
'Lina', //TODO: Replace Winner with real Winner
|
winner: games[1].winner == null
|
||||||
),
|
? 'No winner set.'
|
||||||
SizedBox(height: 8),
|
: games[1].winner!.name,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
] else ...[
|
||||||
|
const Center(
|
||||||
|
heightFactor: 4,
|
||||||
|
child: Text('No second game available.'),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
);
|
),
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
InfoTile(
|
InfoTile(
|
||||||
width: constraints.maxWidth * 0.95,
|
width: constraints.maxWidth * 0.95,
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
102
lib/presentation/widgets/tiles/statistics_tile.dart
Normal file
102
lib/presentation/widgets/tiles/statistics_tile.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
expect(game.winner, testGame.winner);
|
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
|
// Group-Checks
|
||||||
if (testGame.group != null) {
|
if (testGame.group != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user