diff --git a/lib/core/constants.dart b/lib/core/constants.dart new file mode 100644 index 0000000..075b1ab --- /dev/null +++ b/lib/core/constants.dart @@ -0,0 +1,2 @@ +/// Minimum duration of all app skeletons +Duration minimumSkeletonDuration = const Duration(milliseconds: 250); diff --git a/lib/data/dao/group_match_dao.dart b/lib/data/dao/group_match_dao.dart index 3c16c83..d428fb5 100644 --- a/lib/data/dao/group_match_dao.dart +++ b/lib/data/dao/group_match_dao.dart @@ -21,7 +21,7 @@ class GroupMatchDao extends DatabaseAccessor } await into(groupMatchTable).insert( GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } diff --git a/lib/data/dao/match_dao.dart b/lib/data/dao/match_dao.dart index 32bd323..b56a38b 100644 --- a/lib/data/dao/match_dao.dart +++ b/lib/data/dao/match_dao.dart @@ -65,8 +65,9 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); } - /// Adds a new [Match] to the database. - /// Also adds associated players and group if they exist. + /// Adds a new [Match] to the database. Also adds players and group + /// associations. This method assumes that the players and groups added to + /// this match are already present in the database. Future addMatch({required Match match}) async { await db.transaction(() async { await into(matchTable).insert( @@ -80,7 +81,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { ); if (match.players != null) { - await db.playerDao.addPlayersAsList(players: match.players!); for (final p in match.players ?? []) { await db.playerMatchDao.addPlayerToMatch( matchId: match.id, @@ -90,7 +90,6 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { } if (match.group != null) { - await db.groupDao.addGroup(group: match.group!); await db.groupMatchDao.addGroupToMatch( matchId: match.id, groupId: match.group!.id, @@ -102,6 +101,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { /// Adds multiple [Match]s to the database in a batch operation. /// Also adds associated players and groups if they exist. /// If the [matches] list is empty, the method returns immediately. + /// This Method should only be used to import matches from a different device. Future addMatchAsList({required List matches}) async { if (matches.isEmpty) return; await db.transaction(() async { @@ -186,7 +186,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: match.id, playerId: p.id, ), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } } @@ -204,7 +204,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { playerId: m.id, groupId: match.group!.id, ), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } } @@ -221,7 +221,7 @@ class MatchDao extends DatabaseAccessor with _$MatchDaoMixin { matchId: match.id, groupId: match.group!.id, ), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } } diff --git a/lib/data/dao/player_match_dao.dart b/lib/data/dao/player_match_dao.dart index 9e242b7..7ebaee6 100644 --- a/lib/data/dao/player_match_dao.dart +++ b/lib/data/dao/player_match_dao.dart @@ -18,7 +18,7 @@ class PlayerMatchDao extends DatabaseAccessor }) async { await into(playerMatchTable).insert( PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId), - mode: InsertMode.insertOrReplace, + mode: InsertMode.insertOrIgnore, ); } @@ -121,7 +121,7 @@ class PlayerMatchDao extends DatabaseAccessor inserts.map( (c) => into( playerMatchTable, - ).insert(c, mode: InsertMode.insertOrReplace), + ).insert(c, mode: InsertMode.insertOrIgnore), ), ); } diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 5fd5e4b..b2243bc 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; @@ -18,15 +19,15 @@ class GroupsView extends StatefulWidget { } class _GroupsViewState extends State { - late Future> _allGroupsFuture; late final AppDatabase db; + late List loadedGroups; + bool isLoading = true; - final player = Player(name: 'Skeleton Player'); - late final List skeletonData = List.filled( + List groups = List.filled( 7, Group( - name: 'Skeleton Match', - members: [player, player, player, player, player, player], + name: 'Skeleton Group', + members: List.filled(6, Player(name: 'Skeleton Player')), ), ); @@ -34,10 +35,7 @@ class _GroupsViewState extends State { void initState() { super.initState(); db = Provider.of(context, listen: false); - _allGroupsFuture = Future.delayed( - const Duration(milliseconds: 250), - () => db.groupDao.getAllGroups(), - ); + loadGroups(); } @override @@ -47,50 +45,30 @@ class _GroupsViewState extends State { body: Stack( alignment: Alignment.center, children: [ - FutureBuilder>( - future: _allGroupsFuture, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return const Center( - child: TopCenteredMessage( - icon: Icons.report, - title: 'Error', - message: 'Group data couldn\'t\nbe loaded', - ), + AppSkeleton( + enabled: isLoading, + child: Visibility( + visible: groups.isNotEmpty, + replacement: const Center( + child: TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: 'No groups created yet', + ), + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: groups.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == groups.length) { + return SizedBox( + height: MediaQuery.paddingOf(context).bottom - 20, ); } - if (snapshot.connectionState == ConnectionState.done && - (!snapshot.hasData || snapshot.data!.isEmpty)) { - return const Center( - child: TopCenteredMessage( - icon: Icons.info, - title: 'Info', - message: 'No groups created yet', - ), - ); - } - final bool isLoading = - snapshot.connectionState == ConnectionState.waiting; - final List groups = - isLoading ? skeletonData : (snapshot.data ?? []) - ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); - return AppSkeleton( - enabled: isLoading, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: groups.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == groups.length) { - return SizedBox( - height: MediaQuery.paddingOf(context).bottom - 20, - ); - } - return GroupTile(group: groups[index]); - }, - ), - ); + return GroupTile(group: groups[index]); }, + ), + ), ), Positioned( bottom: MediaQuery.paddingOf(context).bottom, @@ -107,7 +85,7 @@ class _GroupsViewState extends State { ), ); setState(() { - _allGroupsFuture = db.groupDao.getAllGroups(); + loadGroups(); }); }, ), @@ -116,4 +94,22 @@ class _GroupsViewState extends State { ), ); } + + void loadGroups() { + Future.wait([ + db.groupDao.getAllGroups(), + Future.delayed(minimumSkeletonDuration), + ]).then((results) { + loadedGroups = results[0] as List; + setState(() { + groups = loadedGroups + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + }); + if (mounted) { + setState(() { + isLoading = false; + }); + } + }); + } } diff --git a/lib/presentation/views/main_menu/home_view.dart b/lib/presentation/views/main_menu/home_view.dart index 49bfa8f..3e221e7 100644 --- a/lib/presentation/views/main_menu/home_view.dart +++ b/lib/presentation/views/main_menu/home_view.dart @@ -1,4 +1,5 @@ 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/group.dart'; import 'package:game_tracker/data/dto/match.dart'; @@ -18,12 +19,11 @@ class HomeView extends StatefulWidget { } class _HomeViewState extends State { - late Future _matchCountFuture; - late Future _groupCountFuture; - late Future> _recentMatchesFuture; bool isLoading = true; - - late final List skeletonData = List.filled( + int matchCount = 0; + int groupCount = 0; + List loadedRecentMatches = []; + List recentMatches = List.filled( 2, Match( name: 'Skeleton Match', @@ -39,19 +39,28 @@ class _HomeViewState extends State { ); @override - initState() { + void initState() { super.initState(); final db = Provider.of(context, listen: false); - _matchCountFuture = db.matchDao.getMatchCount(); - _groupCountFuture = db.groupDao.getGroupCount(); - _recentMatchesFuture = db.matchDao.getAllMatches(); - Future.wait([ - _matchCountFuture, - _groupCountFuture, - _recentMatchesFuture, - ]).then((_) async { - await Future.delayed(const Duration(milliseconds: 250)); + db.matchDao.getMatchCount(), + db.groupDao.getGroupCount(), + db.matchDao.getAllMatches(), + Future.delayed(minimumSkeletonDuration), + ]).then((results) { + matchCount = results[0] as int; + groupCount = results[1] as int; + loadedRecentMatches = results[2] as List; + recentMatches = + (loadedRecentMatches + ..sort((a, b) => b.createdAt.compareTo(a.createdAt))) + .take(2) + .toList(); + if (loadedRecentMatches.length < 2) { + recentMatches.add( + Match(name: 'Dummy Match', winner: null, group: null, players: null), + ); + } if (mounted) { setState(() { isLoading = false; @@ -65,6 +74,7 @@ class _HomeViewState extends State { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return AppSkeleton( + fixLayoutBuilder: true, enabled: isLoading, child: SingleChildScrollView( child: Column( @@ -73,38 +83,20 @@ class _HomeViewState extends State { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - FutureBuilder( - future: _matchCountFuture, - builder: (context, snapshot) { - final int count = (snapshot.hasData) - ? snapshot.data! - : 0; - return QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, - title: 'Matches', - icon: Icons.groups_rounded, - value: count, - ); - }, + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: 'Matches', + icon: Icons.groups_rounded, + value: matchCount, ), SizedBox(width: constraints.maxWidth * 0.05), - FutureBuilder( - future: _groupCountFuture, - builder: (context, snapshot) { - final int count = - (snapshot.connectionState == ConnectionState.done && - snapshot.hasData) - ? snapshot.data! - : 0; - return QuickInfoTile( - width: constraints.maxWidth * 0.45, - height: constraints.maxHeight * 0.15, - title: 'Groups', - icon: Icons.groups_rounded, - value: count, - ); - }, + QuickInfoTile( + width: constraints.maxWidth * 0.45, + height: constraints.maxHeight * 0.15, + title: 'Groups', + icon: Icons.groups_rounded, + value: groupCount, ), ], ), @@ -116,80 +108,48 @@ class _HomeViewState extends State { icon: Icons.timer, content: Padding( padding: const EdgeInsets.symmetric(horizontal: 40.0), - child: FutureBuilder( - future: _recentMatchesFuture, - builder: - ( - BuildContext context, - AsyncSnapshot> snapshot, - ) { - if (snapshot.hasError) { - return const Center( - heightFactor: 4, - child: Text( - 'Error while loading recent matches.', - ), - ); - } - final List matches = - (isLoading - ? skeletonData - : (snapshot.data ?? []) - ..sort( - (a, b) => b.createdAt.compareTo( - a.createdAt, - ), - )) - .take(2) - .toList(); - if (matches.isNotEmpty) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MatchTile( - matchTitle: matches[0].name, - game: 'Winner', - ruleset: 'Ruleset', - players: _getPlayerText(matches[0]), - winner: matches[0].winner == null - ? 'Match in progress...' - : matches[0].winner!.name, - ), - const Padding( - padding: EdgeInsets.symmetric( - vertical: 8.0, - ), - child: Divider(), - ), - if (matches.length > 1) ...[ - MatchTile( - matchTitle: matches[1].name, - game: 'Winner', - ruleset: 'Ruleset', - players: _getPlayerText(matches[1]), - winner: matches[1].winner == null - ? 'Game in progress...' - : matches[1].winner!.name, - ), - const SizedBox(height: 8), - ] else ...[ - const Center( - heightFactor: 4, - child: Text( - 'No second game available.', - ), - ), - ], - ], - ); - } else { - return const Center( - heightFactor: 12, - child: Text('No recent games available.'), - ); - } - }, + child: Visibility( + visible: !isLoading && loadedRecentMatches.isNotEmpty, + replacement: const Center( + heightFactor: 12, + child: Text('No recent games available'), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MatchTile( + matchTitle: recentMatches[0].name, + game: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText(recentMatches[0]), + winner: recentMatches[0].winner == null + ? 'Match in progress...' + : recentMatches[0].winner!.name, + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: Divider(), + ), + if (loadedRecentMatches.length > 1) ...[ + MatchTile( + matchTitle: recentMatches[1].name, + game: 'Winner', + ruleset: 'Ruleset', + players: _getPlayerText(recentMatches[1]), + winner: recentMatches[1].winner == null + ? 'Game in progress...' + : recentMatches[1].winner!.name, + ), + const SizedBox(height: 8), + ] else ...[ + const Center( + heightFactor: 5.35, + child: Text('No second game available'), + ), + ], + ], + ), ), ), ), @@ -199,7 +159,6 @@ class _HomeViewState extends State { title: 'Quick Create', icon: Icons.add_box_rounded, content: Column( - spacing: 8, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart index c30d6ef..37096b9 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/choose_group_view.dart @@ -74,10 +74,18 @@ class _ChooseGroupViewState extends State { Expanded( child: Visibility( visible: filteredGroups.isNotEmpty, - replacement: const TopCenteredMessage( - icon: Icons.info, - title: 'Info', - message: 'There is no group matching your search', + replacement: Visibility( + visible: widget.groups.isNotEmpty, + replacement: const TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: 'You have no groups created yet', + ), + child: const TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: 'There is no group matching your search', + ), ), child: ListView.builder( padding: const EdgeInsets.only(bottom: 85), diff --git a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart index 03c081c..787d200 100644 --- a/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart +++ b/lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart @@ -28,12 +28,6 @@ class _CreateMatchViewState extends State { /// Reference to the app database late final AppDatabase db; - /// Futures to load all groups and players from the database - late Future> _allGroupsFuture; - - /// Future to load all players from the database - late Future> _allPlayersFuture; - /// Controller for the game name input field final TextEditingController _gameNameController = TextEditingController(); @@ -107,14 +101,13 @@ class _CreateMatchViewState extends State { db = Provider.of(context, listen: false); - _allGroupsFuture = db.groupDao.getAllGroups(); - _allPlayersFuture = db.playerDao.getAllPlayers(); - - Future.wait([_allGroupsFuture, _allPlayersFuture]).then((result) async { + Future.wait([ + db.groupDao.getAllGroups(), + db.playerDao.getAllPlayers(), + ]).then((result) async { groupsList = result[0] as List; playerList = result[1] as List; }); - filteredPlayerList = List.from(playerList); } diff --git a/lib/presentation/views/main_menu/match_view/match_result_view.dart b/lib/presentation/views/main_menu/match_view/match_result_view.dart index 58ff9ce..c6c3dae 100644 --- a/lib/presentation/views/main_menu/match_view/match_result_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_result_view.dart @@ -38,6 +38,13 @@ class _GameResultViewState extends State { return Scaffold( backgroundColor: CustomTheme.backgroundColor, appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + widget.onWinnerChanged?.call(); + Navigator.of(context).pop(); + }, + ), backgroundColor: CustomTheme.backgroundColor, scrolledUnderElevation: 0, title: Text( diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index 92fd268..462e1b5 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -2,6 +2,7 @@ import 'dart:core' hide Match; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/group.dart'; @@ -23,16 +24,16 @@ class MatchView extends StatefulWidget { } class _MatchViewState extends State { - late Future> _gameListFuture; late final AppDatabase db; + bool isLoading = true; - late final List skeletonData = List.filled( + List matches = List.filled( 4, Match( name: 'Skeleton Gamename', group: Group( name: 'Groupname', - members: List.generate(5, (index) => Player(name: 'Player')), + members: List.filled(5, Player(name: 'Player')), ), winner: Player(name: 'Player'), players: [Player(name: 'Player')], @@ -43,10 +44,7 @@ class _MatchViewState extends State { void initState() { super.initState(); db = Provider.of(context, listen: false); - _gameListFuture = Future.delayed( - const Duration(milliseconds: 250), - () => db.matchDao.getAllMatches(), - ); + loadGames(); } @override @@ -56,67 +54,44 @@ class _MatchViewState extends State { body: Stack( alignment: Alignment.center, children: [ - FutureBuilder>( - future: _gameListFuture, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return const Center( - child: TopCenteredMessage( - icon: Icons.report, - title: 'Error', - message: 'Game data could not be loaded', - ), + AppSkeleton( + enabled: isLoading, + child: Visibility( + visible: matches.isNotEmpty, + replacement: const Center( + child: TopCenteredMessage( + icon: Icons.report, + title: 'Info', + message: 'No games created yet', + ), + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: matches.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == matches.length) { + return SizedBox( + height: MediaQuery.paddingOf(context).bottom - 80, ); } - if (snapshot.connectionState == ConnectionState.done && - (!snapshot.hasData || snapshot.data!.isEmpty)) { - return const Center( - child: TopCenteredMessage( - icon: Icons.report, - title: 'Info', - message: 'No games created yet', - ), - ); - } - final bool isLoading = - snapshot.connectionState == ConnectionState.waiting; - final List matches = - (isLoading ? skeletonData : (snapshot.data ?? []) - ..sort( - (a, b) => b.createdAt.compareTo(a.createdAt), - )) - .toList(); - return AppSkeleton( - enabled: isLoading, - child: ListView.builder( - padding: const EdgeInsets.only(bottom: 85), - itemCount: matches.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == matches.length) { - return SizedBox( - height: MediaQuery.paddingOf(context).bottom - 80, - ); - } - return GameHistoryTile( - onTap: () async { - Navigator.push( - context, - CupertinoPageRoute( - fullscreenDialog: true, - builder: (context) => GameResultView( - match: matches[index], - onWinnerChanged: refreshGameList, - ), - ), - ); - }, - match: matches[index], - ); - }, - ), + return GameHistoryTile( + onTap: () async { + Navigator.push( + context, + CupertinoPageRoute( + fullscreenDialog: true, + builder: (context) => GameResultView( + match: matches[index], + onWinnerChanged: loadGames, + ), + ), + ); + }, + match: matches[index], ); }, + ), + ), ), Positioned( bottom: MediaQuery.paddingOf(context).bottom, @@ -128,7 +103,7 @@ class _MatchViewState extends State { context, MaterialPageRoute( builder: (context) => - CreateMatchView(onWinnerChanged: refreshGameList), + CreateMatchView(onWinnerChanged: loadGames), ), ); }, @@ -139,9 +114,19 @@ class _MatchViewState extends State { ); } - void refreshGameList() { - setState(() { - _gameListFuture = db.matchDao.getAllMatches(); + void loadGames() { + Future.wait([ + db.matchDao.getAllMatches(), + Future.delayed(minimumSkeletonDuration), + ]).then((results) { + if (mounted) { + setState(() { + final loadedMatches = results[0] as List; + matches = loadedMatches + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + isLoading = false; + }); + } }); } } diff --git a/lib/presentation/views/main_menu/statistics_view.dart b/lib/presentation/views/main_menu/statistics_view.dart index 6104e39..e94f2b6 100644 --- a/lib/presentation/views/main_menu/statistics_view.dart +++ b/lib/presentation/views/main_menu/statistics_view.dart @@ -1,4 +1,5 @@ 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'; @@ -14,8 +15,6 @@ class StatisticsView extends StatefulWidget { } class _StatisticsViewState extends State { - late Future> _matchesFuture; - late Future> _playersFuture; 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)); @@ -25,11 +24,12 @@ class _StatisticsViewState extends State { void initState() { super.initState(); final db = Provider.of(context, listen: false); - _matchesFuture = db.matchDao.getAllMatches(); - _playersFuture = db.playerDao.getAllPlayers(); - Future.wait([_matchesFuture, _playersFuture]).then((results) async { - await Future.delayed(const Duration(milliseconds: 250)); + Future.wait([ + db.matchDao.getAllMatches(), + db.playerDao.getAllPlayers(), + Future.delayed(minimumSkeletonDuration), + ]).then((results) async { final matches = results[0] as List; final players = results[1] as List; winCounts = _calculateWinsForAllPlayers(matches, players); @@ -60,7 +60,7 @@ class _StatisticsViewState extends State { SizedBox(height: constraints.maxHeight * 0.01), StatisticsTile( icon: Icons.sports_score, - title: 'Wins per Player', + title: 'Wins', width: constraints.maxWidth * 0.95, values: winCounts, itemCount: 3, @@ -69,7 +69,7 @@ class _StatisticsViewState extends State { SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( icon: Icons.percent, - title: 'Winrate per Player', + title: 'Winrate', width: constraints.maxWidth * 0.95, values: winRates, itemCount: 5, @@ -78,7 +78,7 @@ class _StatisticsViewState extends State { SizedBox(height: constraints.maxHeight * 0.02), StatisticsTile( icon: Icons.casino, - title: 'Match per Player', + title: 'Amount of Matches', width: constraints.maxWidth * 0.95, values: matchCounts, itemCount: 10, diff --git a/lib/presentation/widgets/player_selection.dart b/lib/presentation/widgets/player_selection.dart index 88841e6..007a95f 100644 --- a/lib/presentation/widgets/player_selection.dart +++ b/lib/presentation/widgets/player_selection.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/dto/player.dart'; @@ -29,6 +30,7 @@ class _PlayerSelectionState extends State { List selectedPlayers = []; List suggestedPlayers = []; List allPlayers = []; + bool isLoading = true; late final TextEditingController _searchBarController = TextEditingController(); late final AppDatabase db; @@ -42,41 +44,44 @@ class _PlayerSelectionState extends State { void initState() { super.initState(); db = Provider.of(context, listen: false); + suggestedPlayers = skeletonData; loadPlayerList(); } void loadPlayerList() { - _allPlayersFuture = Future.delayed( - const Duration(milliseconds: 250), - () => db.playerDao.getAllPlayers(), - ); - suggestedPlayers = skeletonData; - _allPlayersFuture.then((loadedPlayers) { - setState(() { - // If a list of available players is provided, use that list. - if (widget.availablePlayers.isNotEmpty) { - widget.availablePlayers.sort((a, b) => a.name.compareTo(b.name)); - allPlayers = [...widget.availablePlayers]; - suggestedPlayers = [...allPlayers]; + _allPlayersFuture = Future.wait([ + db.playerDao.getAllPlayers(), + Future.delayed(minimumSkeletonDuration), + ]).then((results) => results[0] as List); + if (mounted) { + _allPlayersFuture.then((loadedPlayers) { + setState(() { + // If a list of available players is provided, use that list. + if (widget.availablePlayers.isNotEmpty) { + widget.availablePlayers.sort((a, b) => a.name.compareTo(b.name)); + allPlayers = [...widget.availablePlayers]; + suggestedPlayers = [...allPlayers]; - if (widget.initialSelectedPlayers != null) { - // Ensures that only players available for selection are pre-selected. - selectedPlayers = widget.initialSelectedPlayers! - .where( - (p) => widget.availablePlayers.any( - (available) => available.id == p.id, - ), - ) - .toList(); + if (widget.initialSelectedPlayers != null) { + // Ensures that only players available for selection are pre-selected. + selectedPlayers = widget.initialSelectedPlayers! + .where( + (p) => widget.availablePlayers.any( + (available) => available.id == p.id, + ), + ) + .toList(); + } + } else { + // Otherwise, use the loaded players from the database. + loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); + allPlayers = [...loadedPlayers]; + suggestedPlayers = [...loadedPlayers]; } - } else { - // Otherwise, use the loaded players from the database. - loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); - allPlayers = [...loadedPlayers]; - suggestedPlayers = [...loadedPlayers]; - } + isLoading = false; + }); }); - }); + } } @override @@ -176,75 +181,44 @@ class _PlayerSelectionState extends State { style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 10), - FutureBuilder( - future: _allPlayersFuture, - builder: - (BuildContext context, AsyncSnapshot> snapshot) { - if (snapshot.hasError) { - return const Center( - child: TopCenteredMessage( - icon: Icons.report, - title: 'Error', - message: 'Player data couldn\'t\nbe loaded.', - ), + /* + + */ + Expanded( + child: AppSkeleton( + enabled: isLoading, + child: Visibility( + visible: suggestedPlayers.isNotEmpty, + replacement: TopCenteredMessage( + icon: Icons.info, + title: 'Info', + message: allPlayers.isEmpty + ? 'No players created yet' + : (selectedPlayers.length == allPlayers.length) + ? 'No more players to add' + : 'No players found with that name', + ), + child: ListView.builder( + itemCount: suggestedPlayers.length, + itemBuilder: (BuildContext context, int index) { + return TextIconListTile( + text: suggestedPlayers[index].name, + onPressed: () { + setState(() { + if (!selectedPlayers.contains( + suggestedPlayers[index], + )) { + selectedPlayers.add(suggestedPlayers[index]); + widget.onChanged([...selectedPlayers]); + suggestedPlayers.remove(suggestedPlayers[index]); + } + }); + }, ); - } - bool doneLoading = - snapshot.connectionState == ConnectionState.done; - bool snapshotDataEmpty = - !snapshot.hasData || snapshot.data!.isEmpty; - if (doneLoading && - (snapshotDataEmpty && allPlayers.isEmpty)) { - return const Center( - child: TopCenteredMessage( - icon: Icons.info, - title: 'Info', - message: 'No players created yet.', - ), - ); - } - final bool isLoading = - snapshot.connectionState == ConnectionState.waiting; - return Expanded( - child: AppSkeleton( - enabled: isLoading, - child: Visibility( - visible: - (suggestedPlayers.isEmpty && allPlayers.isNotEmpty), - replacement: ListView.builder( - itemCount: suggestedPlayers.length, - itemBuilder: (BuildContext context, int index) { - return TextIconListTile( - text: suggestedPlayers[index].name, - onPressed: () { - setState(() { - if (!selectedPlayers.contains( - suggestedPlayers[index], - )) { - selectedPlayers.add( - suggestedPlayers[index], - ); - widget.onChanged([...selectedPlayers]); - suggestedPlayers.remove( - suggestedPlayers[index], - ); - } - }); - }, - ); - }, - ), - child: TopCenteredMessage( - icon: Icons.info, - title: 'Info', - message: (selectedPlayers.length == allPlayers.length) - ? 'No more players to add.' - : 'No players found with that name.', - ), - ), - ), - ); - }, + }, + ), + ), + ), ), ], ), diff --git a/test/db_tests/game_test.dart b/test/db_tests/game_test.dart index ca86a60..0ec2cfc 100644 --- a/test/db_tests/game_test.dart +++ b/test/db_tests/game_test.dart @@ -23,7 +23,7 @@ void main() { final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); - setUp(() { + setUp(() async { database = AppDatabase( DatabaseConnection( NativeDatabase.memory(), @@ -68,6 +68,16 @@ void main() { group: testGroup2, ); }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); }); tearDown(() async { await database.close(); @@ -253,7 +263,7 @@ void main() { expect(matchCount, 0); }); - test('Checking if match has winner works correclty', () async { + test('Checking if match has winner works correctly', () async { await database.matchDao.addMatch(match: testMatch1); await database.matchDao.addMatch(match: testMatchOnlyGroup); diff --git a/test/db_tests/group_game_test.dart b/test/db_tests/group_game_test.dart index a4fa146..7d812bd 100644 --- a/test/db_tests/group_game_test.dart +++ b/test/db_tests/group_game_test.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_tracker/data/db/database.dart'; @@ -21,7 +21,7 @@ void main() { final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); - setUp(() { + setUp(() async { database = AppDatabase( DatabaseConnection( NativeDatabase.memory(), @@ -53,6 +53,16 @@ void main() { group: testGroup1, ); }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + ], + ); + await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]); }); tearDown(() async { await database.close(); @@ -179,5 +189,33 @@ void main() { } } }); + + test('Adding the same group to seperate matches works correctly', () async { + final match1 = Match(name: 'Match 1', group: testGroup1); + final match2 = Match(name: 'Match 2', group: testGroup1); + + await Future.wait([ + database.matchDao.addMatch(match: match1), + database.matchDao.addMatch(match: match2), + ]); + + final group1 = await database.groupMatchDao.getGroupOfMatch( + matchId: match1.id, + ); + final group2 = await database.groupMatchDao.getGroupOfMatch( + matchId: match2.id, + ); + + expect(group1, isNotNull); + expect(group2, isNotNull); + + final groups = [group1!, group2!]; + for (final group in groups) { + expect(group.members.length, testGroup1.members.length); + expect(group.id, testGroup1.id); + expect(group.name, testGroup1.name); + expect(group.createdAt, testGroup1.createdAt); + } + }); }); } diff --git a/test/db_tests/player_game_test.dart b/test/db_tests/player_game_test.dart index e2501eb..8a4f569 100644 --- a/test/db_tests/player_game_test.dart +++ b/test/db_tests/player_game_test.dart @@ -1,5 +1,5 @@ import 'package:clock/clock.dart'; -import 'package:drift/drift.dart'; +import 'package:drift/drift.dart' hide isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:game_tracker/data/db/database.dart'; @@ -21,7 +21,7 @@ void main() { final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fakeClock = Clock(() => fixedDate); - setUp(() { + setUp(() async { database = AppDatabase( DatabaseConnection( NativeDatabase.memory(), @@ -50,6 +50,17 @@ void main() { players: [testPlayer4, testPlayer5, testPlayer6], ); }); + await database.playerDao.addPlayersAsList( + players: [ + testPlayer1, + testPlayer2, + testPlayer3, + testPlayer4, + testPlayer5, + testPlayer6, + ], + ); + await database.groupDao.addGroup(group: testgroup); }); tearDown(() async { await database.close(); @@ -185,5 +196,42 @@ void main() { expect(player.createdAt, testPlayer.createdAt); } }); + + test( + 'Adding the same player to seperate matches works correctly', + () async { + final playersList = [testPlayer1, testPlayer2, testPlayer3]; + final match1 = Match(name: 'Match 1', players: playersList); + final match2 = Match(name: 'Match 2', players: playersList); + + await Future.wait([ + database.matchDao.addMatch(match: match1), + database.matchDao.addMatch(match: match2), + ]); + + final players1 = await database.playerMatchDao.getPlayersOfMatch( + matchId: match1.id, + ); + final players2 = await database.playerMatchDao.getPlayersOfMatch( + matchId: match2.id, + ); + + expect(players1, isNotNull); + expect(players2, isNotNull); + + expect( + players1!.map((p) => p.id).toList(), + equals(players2!.map((p) => p.id).toList()), + ); + expect( + players1.map((p) => p.name).toList(), + equals(players2.map((p) => p.name).toList()), + ); + expect( + players1.map((p) => p.createdAt).toList(), + equals(players2.map((p) => p.createdAt).toList()), + ); + }, + ); }); }