Merge remote-tracking branch 'origin/development' into feature/2-gamehistoryview-anpassen
This commit is contained in:
@@ -49,13 +49,16 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
@override
|
||||
void dispose() {
|
||||
_groupNameController.dispose();
|
||||
_searchBarController
|
||||
.dispose(); // Listener entfernen und Controller aufräumen
|
||||
_searchBarController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadPlayerList() {
|
||||
_allPlayersFuture = db.playerDao.getAllPlayers();
|
||||
_allPlayersFuture = Future.delayed(
|
||||
const Duration(milliseconds: 250),
|
||||
() => db.playerDao.getAllPlayers(),
|
||||
);
|
||||
suggestedPlayers = skeletonData;
|
||||
_allPlayersFuture.then((loadedPlayers) {
|
||||
setState(() {
|
||||
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
@@ -67,19 +70,19 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
child: Scaffold(
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
scrolledUnderElevation: 0,
|
||||
title: const Text(
|
||||
'Create new group',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
centerTitle: true,
|
||||
scrolledUnderElevation: 0,
|
||||
title: const Text(
|
||||
'Create new group',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
),
|
||||
body: Column(
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
@@ -123,12 +126,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,48 +337,47 @@ 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));
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
loadPlayerList();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Successfully added player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
/// 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.
|
||||
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) {
|
||||
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,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Successfully added player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
searchBarController.clear();
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Could not add player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Could not add player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,7 @@ class CustomNavigationBar extends StatefulWidget {
|
||||
class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
with SingleTickerProviderStateMixin {
|
||||
int currentIndex = 0;
|
||||
final List<Widget> tabs = [
|
||||
const HomeView(),
|
||||
const GameHistoryView(),
|
||||
const GroupsView(),
|
||||
const StatisticsView(),
|
||||
];
|
||||
int tabKeyCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -31,6 +26,22 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Pretty ugly but works
|
||||
final List<Widget> tabs = [
|
||||
KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('games_$tabKeyCount'),
|
||||
child: const GameHistoryView(),
|
||||
),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('groups_$tabKeyCount'),
|
||||
child: const GroupsView(),
|
||||
),
|
||||
KeyedSubtree(
|
||||
key: ValueKey('stats_$tabKeyCount'),
|
||||
child: const StatisticsView(),
|
||||
),
|
||||
];
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
centerTitle: true,
|
||||
@@ -42,10 +53,15 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
scrolledUnderElevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SettingsView()),
|
||||
),
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (_) => const SettingsView()),
|
||||
);
|
||||
setState(() {
|
||||
tabKeyCount++;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.settings),
|
||||
),
|
||||
],
|
||||
@@ -54,12 +70,14 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
body: tabs[currentIndex],
|
||||
extendBody: true,
|
||||
bottomNavigationBar: Padding(
|
||||
padding: const EdgeInsets.only(left: 12.0, right: 12.0, bottom: 8.0),
|
||||
child: Material(
|
||||
elevation: 10,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: CustomTheme.primaryColor,
|
||||
bottomNavigationBar: SafeArea(
|
||||
minimum: const EdgeInsets.only(bottom: 30),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: CustomTheme.primaryColor,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
child: SizedBox(
|
||||
|
||||
@@ -34,7 +34,10 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_allGroupsFuture = db.groupDao.getAllGroups();
|
||||
_allGroupsFuture = Future.delayed(
|
||||
const Duration(milliseconds: 250),
|
||||
() => db.groupDao.getAllGroups(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -69,9 +72,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 +96,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]);
|
||||
},
|
||||
@@ -103,7 +108,7 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: 80,
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: CustomWidthButton(
|
||||
text: 'Create Group',
|
||||
sizeRelativeToWidth: 0.90,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
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/group.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
|
||||
@@ -17,23 +20,42 @@ class HomeView extends StatefulWidget {
|
||||
class _HomeViewState extends State<HomeView> {
|
||||
late Future<int> _gameCountFuture;
|
||||
late Future<int> _groupCountFuture;
|
||||
late Future<List<Game>> _recentGamesFuture;
|
||||
bool isLoading = true;
|
||||
|
||||
late final List<Game> skeletonData = List.filled(
|
||||
2,
|
||||
Game(
|
||||
name: 'Skeleton Game',
|
||||
group: Group(
|
||||
name: 'Skeleton Group',
|
||||
members: [
|
||||
Player(name: 'Skeleton Player 1'),
|
||||
Player(name: 'Skeleton Player 2'),
|
||||
],
|
||||
),
|
||||
winner: Player(name: 'Skeleton Player 1'),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
final db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_gameCountFuture = db.gameDao.getGameCount();
|
||||
_groupCountFuture = db.groupDao.getGroupCount();
|
||||
_recentGamesFuture = db.gameDao.getAllGames();
|
||||
|
||||
Future.wait([_gameCountFuture, _groupCountFuture]).then((_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
Future.wait([_gameCountFuture, _groupCountFuture, _recentGamesFuture]).then(
|
||||
(_) async {
|
||||
await Future.delayed(const Duration(milliseconds: 250));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -48,12 +70,21 @@ class _HomeViewState extends State<HomeView> {
|
||||
),
|
||||
enabled: isLoading,
|
||||
enableSwitchAnimation: true,
|
||||
switchAnimationConfig: const SwitchAnimationConfig(
|
||||
duration: Duration(milliseconds: 200),
|
||||
switchAnimationConfig: SwitchAnimationConfig(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.linear,
|
||||
switchOutCurve: Curves.linear,
|
||||
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
|
||||
layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder,
|
||||
layoutBuilder:
|
||||
(Widget? currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -103,32 +134,91 @@ class _HomeViewState extends State<HomeView> {
|
||||
width: constraints.maxWidth * 0.95,
|
||||
title: 'Recent Games',
|
||||
icon: Icons.timer,
|
||||
content: const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GameTile(
|
||||
gameTitle: 'Gamenight',
|
||||
gameType: 'Cabo',
|
||||
ruleset: 'Lowest Points',
|
||||
players: '5 Players',
|
||||
winner: 'Leonard',
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Divider(),
|
||||
),
|
||||
GameTile(
|
||||
gameTitle: 'Schoolbreak',
|
||||
gameType: 'Uno',
|
||||
ruleset: 'Highest Points',
|
||||
players: 'The Gang',
|
||||
winner: 'Lina',
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
],
|
||||
content: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 40.0),
|
||||
child: FutureBuilder(
|
||||
future: _recentGamesFuture,
|
||||
builder:
|
||||
(
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<Game>> 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();
|
||||
if (games.isNotEmpty) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GameTile(
|
||||
gameTitle: games[0].name,
|
||||
gameType: 'Winner',
|
||||
ruleset: 'Ruleset',
|
||||
players: _getPlayerText(games[0]),
|
||||
winner: games[0].winner == null
|
||||
? 'Game in progress...'
|
||||
: games[0].winner!.name,
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Divider(),
|
||||
),
|
||||
if (games.length > 1) ...[
|
||||
GameTile(
|
||||
gameTitle: games[1].name,
|
||||
gameType: 'Winner',
|
||||
ruleset: 'Ruleset',
|
||||
players: _getPlayerText(games[1]),
|
||||
winner: games[1].winner == null
|
||||
? 'Game in progress...'
|
||||
: games[1].winner!.name,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
] else ...[
|
||||
const Center(
|
||||
heightFactor: 4,
|
||||
child: Text(
|
||||
'No second game available.',
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
heightFactor: 4,
|
||||
child: Text('No recent games available.'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -189,4 +279,15 @@ class _HomeViewState extends State<HomeView> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _getPlayerText(Game game) {
|
||||
if (game.group == null) {
|
||||
final playerCount = game.players?.length ?? 0;
|
||||
return '$playerCount Players';
|
||||
}
|
||||
if (game.players == null || game.players!.isEmpty) {
|
||||
return game.group!.name;
|
||||
}
|
||||
return '${game.group!.name} + ${game.players!.length}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,191 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:game_tracker/core/enums.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart';
|
||||
import 'package:game_tracker/services/data_transfer_service.dart';
|
||||
|
||||
class SettingsView extends StatelessWidget {
|
||||
class SettingsView extends StatefulWidget {
|
||||
const SettingsView({super.key});
|
||||
|
||||
@override
|
||||
State<SettingsView> createState() => _SettingsViewState();
|
||||
}
|
||||
|
||||
class _SettingsViewState extends State<SettingsView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Einstellungen')),
|
||||
body: const Center(child: Text('Settings View')),
|
||||
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
body: LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) =>
|
||||
SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 0, 24, 10),
|
||||
child: Text(
|
||||
textAlign: TextAlign.start,
|
||||
'Menu',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
|
||||
child: Text(
|
||||
textAlign: TextAlign.start,
|
||||
'Settings',
|
||||
style: TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SettingsListTile(
|
||||
title: 'Export data',
|
||||
icon: Icons.upload_outlined,
|
||||
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onPressed: () async {
|
||||
final String json =
|
||||
await DataTransferService.getAppDataAsJson(context);
|
||||
final result = await DataTransferService.exportData(
|
||||
json,
|
||||
'game_tracker-data',
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
showExportSnackBar(context: context, result: result);
|
||||
},
|
||||
),
|
||||
SettingsListTile(
|
||||
title: 'Import data',
|
||||
icon: Icons.download_outlined,
|
||||
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onPressed: () async {
|
||||
final result = await DataTransferService.importData(
|
||||
context,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
showImportSnackBar(context: context, result: result);
|
||||
},
|
||||
),
|
||||
SettingsListTile(
|
||||
title: 'Delete all data',
|
||||
icon: Icons.download_outlined,
|
||||
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onPressed: () {
|
||||
showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete all data?'),
|
||||
content: const Text('This can\'t be undone'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Abbrechen'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Löschen'),
|
||||
),
|
||||
],
|
||||
),
|
||||
).then((confirmed) {
|
||||
if (confirmed == true && context.mounted) {
|
||||
DataTransferService.deleteAllData(context);
|
||||
showSnackbar(
|
||||
context: context,
|
||||
message: 'Daten erfolgreich gelöscht',
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Displays a snackbar based on the import result.
|
||||
///
|
||||
/// [context] The BuildContext to show the snackbar in.
|
||||
/// [result] The result of the import operation.
|
||||
void showImportSnackBar({
|
||||
required BuildContext context,
|
||||
required ImportResult result,
|
||||
}) {
|
||||
switch (result) {
|
||||
case ImportResult.success:
|
||||
showSnackbar(context: context, message: 'Data successfully imported');
|
||||
case ImportResult.invalidSchema:
|
||||
showSnackbar(context: context, message: 'Invalid Schema');
|
||||
case ImportResult.fileReadError:
|
||||
showSnackbar(context: context, message: 'Error reading file');
|
||||
case ImportResult.canceled:
|
||||
showSnackbar(context: context, message: 'Import canceled');
|
||||
case ImportResult.formatException:
|
||||
showSnackbar(
|
||||
context: context,
|
||||
message: 'Format Exception (see console)',
|
||||
);
|
||||
case ImportResult.unknownException:
|
||||
showSnackbar(
|
||||
context: context,
|
||||
message: 'Unknown Exception (see console)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a snackbar based on the export result.
|
||||
///
|
||||
/// [context] The BuildContext to show the snackbar in.
|
||||
/// [result] The result of the export operation.
|
||||
void showExportSnackBar({
|
||||
required BuildContext context,
|
||||
required ExportResult result,
|
||||
}) {
|
||||
switch (result) {
|
||||
case ExportResult.success:
|
||||
showSnackbar(context: context, message: 'Data successfully exported');
|
||||
case ExportResult.canceled:
|
||||
showSnackbar(context: context, message: 'Export canceled');
|
||||
case ExportResult.unknownException:
|
||||
showSnackbar(
|
||||
context: context,
|
||||
message: 'Unknown Exception (see console)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Displays a snackbar with the given message and optional action.
|
||||
///
|
||||
/// [context] The BuildContext to show the snackbar in.
|
||||
/// [message] The message to display in the snackbar.
|
||||
/// [duration] The duration for which the snackbar is displayed.
|
||||
/// [action] An optional callback function to execute when the action button is pressed.
|
||||
void showSnackbar({
|
||||
required BuildContext context,
|
||||
required String message,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
VoidCallback? action,
|
||||
}) {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
messenger.hideCurrentSnackBar();
|
||||
messenger.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(message, style: const TextStyle(color: Colors.white)),
|
||||
backgroundColor: CustomTheme.onBoxColor,
|
||||
duration: duration,
|
||||
action: action != null
|
||||
? SnackBarAction(label: 'Rückgängig', onPressed: action)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: 250));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user