264 lines
9.0 KiB
Dart
264 lines
9.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:game_tracker/core/constants.dart';
|
|
import 'package:game_tracker/data/db/database.dart';
|
|
import 'package:game_tracker/data/dto/match.dart';
|
|
import 'package:game_tracker/data/dto/player.dart';
|
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
|
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
|
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
|
import 'package:provider/provider.dart';
|
|
|
|
class StatisticsView extends StatefulWidget {
|
|
const StatisticsView({super.key});
|
|
|
|
@override
|
|
State<StatisticsView> createState() => _StatisticsViewState();
|
|
}
|
|
|
|
class _StatisticsViewState extends State<StatisticsView> {
|
|
List<(String, int)> winCounts = List.filled(6, ('Skeleton Player', 1));
|
|
List<(String, int)> matchCounts = 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);
|
|
|
|
Future.wait([
|
|
db.matchDao.getAllMatches(),
|
|
db.playerDao.getAllPlayers(),
|
|
Future.delayed(minimumSkeletonDuration),
|
|
]).then((results) async {
|
|
final matches = results[0] as List<Match>;
|
|
final players = results[1] as List<Player>;
|
|
winCounts = _calculateWinsForAllPlayers(matches, players);
|
|
matchCounts = _calculateMatchAmountsForAllPlayers(matches, players);
|
|
winRates = computeWinRatePercent(wins: winCounts, matches: matchCounts);
|
|
if (mounted) {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (BuildContext context, BoxConstraints constraints) {
|
|
return SingleChildScrollView(
|
|
child: AppSkeleton(
|
|
enabled: isLoading,
|
|
fixLayoutBuilder: true,
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
children: [
|
|
SizedBox(height: constraints.maxHeight * 0.01),
|
|
Visibility(
|
|
visible:
|
|
winCounts.isEmpty &&
|
|
matchCounts.isEmpty &&
|
|
winRates.isEmpty,
|
|
replacement: Column(
|
|
children: [
|
|
if (winCounts.isNotEmpty) ...[
|
|
StatisticsTile(
|
|
icon: Icons.sports_score,
|
|
title: 'Wins',
|
|
width: constraints.maxWidth * 0.95,
|
|
values: winCounts,
|
|
itemCount: 3,
|
|
barColor: Colors.blue,
|
|
),
|
|
SizedBox(height: constraints.maxHeight * 0.02),
|
|
],
|
|
if (winRates.isNotEmpty) ...[
|
|
StatisticsTile(
|
|
icon: Icons.percent,
|
|
title: 'Winrate',
|
|
width: constraints.maxWidth * 0.95,
|
|
values: winRates,
|
|
itemCount: 5,
|
|
barColor: Colors.orange[700]!,
|
|
),
|
|
SizedBox(height: constraints.maxHeight * 0.02),
|
|
],
|
|
if (matchCounts.isNotEmpty) ...[
|
|
StatisticsTile(
|
|
icon: Icons.casino,
|
|
title: 'Amount of Matches',
|
|
width: constraints.maxWidth * 0.95,
|
|
values: matchCounts,
|
|
itemCount: 10,
|
|
barColor: Colors.green,
|
|
),
|
|
],
|
|
],
|
|
),
|
|
child: const TopCenteredMessage(
|
|
icon: Icons.info,
|
|
title: 'Info',
|
|
message: 'No statistics available',
|
|
),
|
|
),
|
|
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<Match> matches,
|
|
List<Player> players,
|
|
) {
|
|
List<(String, int)> winCounts = [];
|
|
|
|
// Getting the winners
|
|
for (var match in matches) {
|
|
final winner = match.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 matches played for each player
|
|
/// and returns a sorted list of tuples (playerName, matchCount)
|
|
List<(String, int)> _calculateMatchAmountsForAllPlayers(
|
|
List<Match> matches,
|
|
List<Player> players,
|
|
) {
|
|
List<(String, int)> matchCounts = [];
|
|
|
|
// Counting matches for each player
|
|
for (var match in matches) {
|
|
if (match.group != null) {
|
|
final members = match.group!.members.map((p) => p.id).toList();
|
|
for (var playerId in members) {
|
|
final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
|
|
// -1 means player not found in matchCounts
|
|
if (index != -1) {
|
|
final current = matchCounts[index].$2;
|
|
matchCounts[index] = (playerId, current + 1);
|
|
} else {
|
|
matchCounts.add((playerId, 1));
|
|
}
|
|
}
|
|
}
|
|
if (match.players != null) {
|
|
final members = match.players!.map((p) => p.id).toList();
|
|
for (var playerId in members) {
|
|
final index = matchCounts.indexWhere((entry) => entry.$1 == playerId);
|
|
// -1 means player not found in matchCounts
|
|
if (index != -1) {
|
|
final current = matchCounts[index].$2;
|
|
matchCounts[index] = (playerId, current + 1);
|
|
} else {
|
|
matchCounts.add((playerId, 1));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adding all players with zero matches
|
|
for (var player in players) {
|
|
final index = matchCounts.indexWhere((entry) => entry.$1 == player.id);
|
|
// -1 means player not found in matchCounts
|
|
if (index == -1) {
|
|
matchCounts.add((player.id, 0));
|
|
}
|
|
}
|
|
|
|
// Replace player IDs with names
|
|
for (int i = 0; i < matchCounts.length; i++) {
|
|
final playerId = matchCounts[i].$1;
|
|
final player = players.firstWhere(
|
|
(p) => p.id == playerId,
|
|
orElse: () => Player(id: playerId, name: 'N.a.'),
|
|
);
|
|
matchCounts[i] = (player.name, matchCounts[i].$2);
|
|
}
|
|
|
|
matchCounts.sort((a, b) => b.$2.compareTo(a.$2));
|
|
|
|
return matchCounts;
|
|
}
|
|
|
|
// dart
|
|
List<(String, double)> computeWinRatePercent({
|
|
required List<(String, int)> wins,
|
|
required List<(String, int)> matches,
|
|
}) {
|
|
final Map<String, int> winsMap = {for (var e in wins) e.$1: e.$2};
|
|
final Map<String, int> matchesMap = {for (var e in matches) e.$1: e.$2};
|
|
|
|
// Get all unique player names
|
|
final names = {...winsMap.keys, ...matchesMap.keys};
|
|
|
|
// Calculate win rates
|
|
final result = names.map((name) {
|
|
final int w = winsMap[name] ?? 0;
|
|
final int g = matchesMap[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;
|
|
}
|
|
}
|