diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index 438aab5..16e9585 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -5,6 +5,7 @@ class CustomTheme { static Color secondaryColor = const Color(0xFFAFA2FF); static Color backgroundColor = const Color(0xFF0B0B0B); static Color boxColor = const Color(0xFF101010); + static Color onBoxColor = const Color(0xFF181818); static Color boxBorder = const Color(0xFF272727); static AppBarTheme appBarTheme = AppBarTheme( diff --git a/lib/data/dao/group_dao.dart b/lib/data/dao/group_dao.dart index 1f0e2c8..8eb3a1a 100644 --- a/lib/data/dao/group_dao.dart +++ b/lib/data/dao/group_dao.dart @@ -14,9 +14,14 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { Future> getAllGroups() async { final query = select(groupTable); final result = await query.get(); - return result - .map((row) => Group(id: row.id, name: row.name, members: [])) - .toList(); + return Future.wait( + result.map((groupData) async { + final members = await db.playerGroupDao.getPlayersOfGroupById( + groupId: groupData.id, + ); + return Group(id: groupData.id, name: groupData.name, members: members); + }), + ); } /// Retrieves a [Group] by its [groupId], including its members. @@ -33,28 +38,32 @@ class GroupDao extends DatabaseAccessor with _$GroupDaoMixin { /// Adds a new group with the given [id] and [name] to the database. /// This method also adds the group's members to the [PlayerGroupTable]. - Future addGroup({required Group group}) async { - await db.transaction(() async { - await into( - groupTable, - ).insert(GroupTableCompanion.insert(id: group.id, name: group.name)); - await db.batch( - (b) => b.insertAll( - db.playerGroupTable, - group.members - .map( - (member) => PlayerGroupTableCompanion.insert( - playerId: member.id, - groupId: group.id, - ), - ) - .toList(), - ), - ); - await Future.wait( - group.members.map((player) => db.playerDao.addPlayer(player: player)), - ); - }); + Future addGroup({required Group group}) async { + if (!await groupExists(groupId: group.id)) { + await db.transaction(() async { + await into( + groupTable, + ).insert(GroupTableCompanion.insert(id: group.id, name: group.name)); + await db.batch( + (b) => b.insertAll( + db.playerGroupTable, + group.members + .map( + (member) => PlayerGroupTableCompanion.insert( + playerId: member.id, + groupId: group.id, + ), + ) + .toList(), + ), + ); + await Future.wait( + group.members.map((player) => db.playerDao.addPlayer(player: player)), + ); + return true; + }); + } + return false; } /// Deletes the group with the given [id] from the database. diff --git a/lib/data/dao/player_dao.dart b/lib/data/dao/player_dao.dart index 976d4b0..591634c 100644 --- a/lib/data/dao/player_dao.dart +++ b/lib/data/dao/player_dao.dart @@ -26,14 +26,14 @@ class PlayerDao extends DatabaseAccessor with _$PlayerDaoMixin { /// Adds a new [player] to the database. /// If a player with the same ID already exists, updates their name to /// the new one. - Future addPlayer({required Player player}) async { + Future addPlayer({required Player player}) async { if (!await playerExists(playerId: player.id)) { await into( playerTable, ).insert(PlayerTableCompanion.insert(id: player.id, name: player.name)); - } else { - await updatePlayername(playerId: player.id, newName: player.name); + return true; } + return false; } /// Deletes the player with the given [id] from the database. diff --git a/lib/presentation/views/main_menu/game_history_view.dart b/lib/presentation/views/main_menu/game_history_view.dart index de75ae6..3642a88 100644 --- a/lib/presentation/views/main_menu/game_history_view.dart +++ b/lib/presentation/views/main_menu/game_history_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:game_tracker/core/custom_theme.dart'; -import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/double_row_info_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; class GameHistoryView extends StatefulWidget { const GameHistoryView({super.key}); @@ -178,9 +178,17 @@ class _GameHistoryViewState extends State { Widget gameHistoryListView(allGameData, suggestedGameData) { if (suggestedGameData.isEmpty && allGameData.isEmpty) { - return TopCenteredMessage("Keine Spiele erstellt"); + return TopCenteredMessage( + icon: Icons.info, + title: "Info", + message: "Keine Spiele erstellt", + ); } else if (suggestedGameData.isEmpty) { - return TopCenteredMessage("Kein Spiel mit den Suchparametern gefunden."); + return TopCenteredMessage( + icon: Icons.search, + title: "Info", + message: "Kein Spiel mit den Suchparametern gefunden.", + ); } return ListView.builder( itemCount: suggestedGameData.length, diff --git a/lib/presentation/views/main_menu/groups_view.dart b/lib/presentation/views/main_menu/groups_view.dart index 485d516..7f1f32d 100644 --- a/lib/presentation/views/main_menu/groups_view.dart +++ b/lib/presentation/views/main_menu/groups_view.dart @@ -1,10 +1,112 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.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'; +import 'package:game_tracker/data/dto/player.dart'; +import 'package:game_tracker/presentation/widgets/full_width_button.dart'; +import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; +import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; +import 'package:provider/provider.dart'; +import 'package:skeletonizer/skeletonizer.dart'; -class GroupsView extends StatelessWidget { +class GroupsView extends StatefulWidget { const GroupsView({super.key}); + @override + State createState() => _GroupsViewState(); +} + +class _GroupsViewState extends State { + late Future> _allGroupsFuture; + + final player = Player(id: 'p1', name: 'Sample'); + late final List skeletonData = List.filled( + 7, + Group( + id: '0', + name: 'Sample Game', + members: [player, player, player, player, player, player], + ), + ); + + @override + void initState() { + super.initState(); + final db = Provider.of(context, listen: false); + _allGroupsFuture = db.groupDao.getAllGroups(); + } + @override Widget build(BuildContext context) { - return const Center(child: Text('Groups View')); + return Scaffold( + backgroundColor: CustomTheme.backgroundColor, + 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.', + ), + ); + } + 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 ?? []); + return Skeletonizer( + effect: PulseEffect( + from: Colors.grey[800]!, + to: Colors.grey[600]!, + duration: const Duration(milliseconds: 800), + ), + enabled: isLoading, + enableSwitchAnimation: true, + switchAnimationConfig: const SwitchAnimationConfig( + duration: Duration(milliseconds: 200), + switchInCurve: Curves.linear, + switchOutCurve: Curves.linear, + transitionBuilder: + AnimatedSwitcher.defaultTransitionBuilder, + layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder, + ), + child: ListView.builder( + padding: const EdgeInsets.only(bottom: 85), + itemCount: groups.length + 1, + itemBuilder: (BuildContext context, int index) { + if (index == groups.length) { + return const SizedBox(height: 60); + } + return GroupTile(group: groups[index]); + }, + ), + ); + }, + ), + + Positioned( + bottom: 80, + child: FullWidthButton(text: 'Create Group', onPressed: () {}), + ), + ], + ), + ); } } diff --git a/lib/presentation/widgets/full_width_button.dart b/lib/presentation/widgets/full_width_button.dart new file mode 100644 index 0000000..bd18c64 --- /dev/null +++ b/lib/presentation/widgets/full_width_button.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; + +class FullWidthButton extends StatelessWidget { + const FullWidthButton({super.key, required this.text, this.onPressed}); + + final String text; + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + minimumSize: Size(MediaQuery.sizeOf(context).width * 0.9, 60), + backgroundColor: CustomTheme.primaryColor, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: Text( + text, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 22, + color: Colors.white, + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/tiles/group_tile.dart b/lib/presentation/widgets/tiles/group_tile.dart new file mode 100644 index 0000000..448c68c --- /dev/null +++ b/lib/presentation/widgets/tiles/group_tile.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; +import 'package:game_tracker/data/dto/group.dart'; +import 'package:skeletonizer/skeletonizer.dart'; + +class GroupTile extends StatelessWidget { + const GroupTile({super.key, required this.group}); + + final Group group; + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), + decoration: BoxDecoration( + color: CustomTheme.boxColor, + border: Border.all(color: CustomTheme.boxBorder), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + group.name, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const Spacer(), + Text( + '${group.members.length}', + style: const TextStyle( + fontWeight: FontWeight.w900, + fontSize: 18, + ), + ), + const SizedBox(width: 3), + const Icon(Icons.group, size: 22), + ], + ), + const SizedBox(height: 5), + Wrap( + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.start, + spacing: 12.0, + runSpacing: 8.0, + children: [ + for (var member in group.members) + Container( + padding: const EdgeInsets.symmetric( + vertical: 5, + horizontal: 10, + ), + decoration: BoxDecoration( + color: CustomTheme.onBoxColor, + borderRadius: BorderRadius.circular(12), + ), + child: Skeleton.ignore( + child: Text( + member.name, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + const SizedBox(height: 2.5), + ], + ), + ); + } +} diff --git a/lib/presentation/widgets/top_centered_message.dart b/lib/presentation/widgets/top_centered_message.dart index 6fe34ff..a5deea2 100644 --- a/lib/presentation/widgets/top_centered_message.dart +++ b/lib/presentation/widgets/top_centered_message.dart @@ -1,14 +1,39 @@ import 'package:flutter/material.dart'; -Widget TopCenteredMessage(String message) { - return Container( - padding: EdgeInsets.only(top: 100), - margin: EdgeInsets.only(left: 10, right: 10), - alignment: Alignment.topCenter, - child: Text( - "$message", - style: TextStyle(fontSize: 20), - textAlign: TextAlign.center, - ), - ); +class TopCenteredMessage extends StatelessWidget { + const TopCenteredMessage({ + super.key, + required this.icon, + required this.title, + required this.message, + }); + + final String title; + final String message; + final IconData icon; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.only(top: 100), + margin: const EdgeInsets.symmetric(horizontal: 10), + alignment: Alignment.topCenter, + child: Column( + children: [ + Icon(icon, size: 45), + const SizedBox(height: 10), + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + Text( + message, + style: const TextStyle(fontSize: 16), + textAlign: TextAlign.center, + ), + ], + ), + ); + } } diff --git a/test/db_tests/group_test.dart b/test/db_tests/group_test.dart index a8fbcd5..d8d6ce2 100644 --- a/test/db_tests/group_test.dart +++ b/test/db_tests/group_test.dart @@ -35,114 +35,146 @@ void main() { tearDown(() async { await database.close(); }); + group('group tests', () { + test('all groups get fetched correctly', () async { + final testgroup2 = Group( + id: 'gr2', + name: 'Second Group', + members: [player2, player3, player4], + ); + await database.groupDao.addGroup(group: testgroup); + await database.groupDao.addGroup(group: testgroup2); - test('group and group members gets added correctly', () async { - await database.groupDao.addGroup(group: testgroup); + final allGroups = await database.groupDao.getAllGroups(); + expect(allGroups.length, 2); - final result = await database.groupDao.getGroupById(groupId: testgroup.id); + final fetchedGroup1 = allGroups.firstWhere((g) => g.id == testgroup.id); + expect(fetchedGroup1.name, testgroup.name); + expect(fetchedGroup1.members.length, testgroup.members.length); + expect(fetchedGroup1.members.elementAt(0).id, player1.id); - expect(result.id, testgroup.id); - expect(result.name, testgroup.name); + final fetchedGroup2 = allGroups.firstWhere((g) => g.id == testgroup2.id); + expect(fetchedGroup2.name, testgroup2.name); + expect(fetchedGroup2.members.length, testgroup2.members.length); + expect(fetchedGroup2.members.elementAt(0).id, player2.id); + }); - expect(result.members.length, testgroup.members.length); - for (int i = 0; i < testgroup.members.length; i++) { - expect(result.members[i].id, testgroup.members[i].id); - expect(result.members[i].name, testgroup.members[i].name); - } - }); + test('group and group members gets added correctly', () async { + await database.groupDao.addGroup(group: testgroup); - test('group gets deleted correctly', () async { - await database.groupDao.addGroup(group: testgroup); + final result = await database.groupDao.getGroupById( + groupId: testgroup.id, + ); - final groupDeleted = await database.groupDao.deleteGroup( - groupId: testgroup.id, - ); - expect(groupDeleted, true); + expect(result.id, testgroup.id); + expect(result.name, testgroup.name); - final groupExists = await database.groupDao.groupExists( - groupId: testgroup.id, - ); - expect(groupExists, false); - }); + expect(result.members.length, testgroup.members.length); + for (int i = 0; i < testgroup.members.length; i++) { + expect(result.members[i].id, testgroup.members[i].id); + expect(result.members[i].name, testgroup.members[i].name); + } + }); - test('group name gets updated correcly ', () async { - await database.groupDao.addGroup(group: testgroup); + test('group gets deleted correctly', () async { + await database.groupDao.addGroup(group: testgroup); - const newGroupName = 'new group name'; + final groupDeleted = await database.groupDao.deleteGroup( + groupId: testgroup.id, + ); + expect(groupDeleted, true); - await database.groupDao.updateGroupname( - groupId: testgroup.id, - newName: newGroupName, - ); + final groupExists = await database.groupDao.groupExists( + groupId: testgroup.id, + ); + expect(groupExists, false); + }); - final result = await database.groupDao.getGroupById(groupId: testgroup.id); - expect(result.name, newGroupName); - }); + test('group name gets updated correcly ', () async { + await database.groupDao.addGroup(group: testgroup); - test('Adding player to group works correctly', () async { - await database.groupDao.addGroup(group: testgroup); + const newGroupName = 'new group name'; - await database.playerGroupDao.addPlayerToGroup( - player: player4, - groupId: testgroup.id, - ); + await database.groupDao.updateGroupname( + groupId: testgroup.id, + newName: newGroupName, + ); - final playerAdded = await database.playerGroupDao.isPlayerInGroup( - playerId: player4.id, - groupId: testgroup.id, - ); + final result = await database.groupDao.getGroupById( + groupId: testgroup.id, + ); + expect(result.name, newGroupName); + }); - expect(playerAdded, true); + test('Adding player to group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); - final playerAdded2 = await database.playerGroupDao.isPlayerInGroup( - playerId: 'a', - groupId: testgroup.id, - ); + await database.playerGroupDao.addPlayerToGroup( + player: player4, + groupId: testgroup.id, + ); - expect(playerAdded2, false); + final playerAdded = await database.playerGroupDao.isPlayerInGroup( + playerId: player4.id, + groupId: testgroup.id, + ); - expect(playerAdded, true); + expect(playerAdded, true); - final result = await database.groupDao.getGroupById(groupId: testgroup.id); - expect(result.members.length, testgroup.members.length + 1); + final playerAdded2 = await database.playerGroupDao.isPlayerInGroup( + playerId: 'a', + groupId: testgroup.id, + ); - final addedPlayer = result.members.firstWhere((p) => p.id == player4.id); - expect(addedPlayer.name, player4.name); - }); + expect(playerAdded2, false); - test('Removing player from group works correctly', () async { - await database.groupDao.addGroup(group: testgroup); + expect(playerAdded, true); - final playerToRemove = testgroup.members[0]; + final result = await database.groupDao.getGroupById( + groupId: testgroup.id, + ); + expect(result.members.length, testgroup.members.length + 1); - final removed = await database.playerGroupDao.removePlayerFromGroup( - playerId: playerToRemove.id, - groupId: testgroup.id, - ); - expect(removed, true); + final addedPlayer = result.members.firstWhere((p) => p.id == player4.id); + expect(addedPlayer.name, player4.name); + }); - final result = await database.groupDao.getGroupById(groupId: testgroup.id); - expect(result.members.length, testgroup.members.length - 1); + test('Removing player from group works correctly', () async { + await database.groupDao.addGroup(group: testgroup); - final playerExists = result.members.any((p) => p.id == playerToRemove.id); - expect(playerExists, false); - }); + final playerToRemove = testgroup.members[0]; - test('get group count works correctly', () async { - final initialCount = await database.groupDao.getGroupCount(); - expect(initialCount, 0); + final removed = await database.playerGroupDao.removePlayerFromGroup( + playerId: playerToRemove.id, + groupId: testgroup.id, + ); + expect(removed, true); - await database.groupDao.addGroup(group: testgroup); + final result = await database.groupDao.getGroupById( + groupId: testgroup.id, + ); + expect(result.members.length, testgroup.members.length - 1); - final groupAdded = await database.groupDao.getGroupCount(); - expect(groupAdded, 1); + final playerExists = result.members.any((p) => p.id == playerToRemove.id); + expect(playerExists, false); + }); - final groupRemoved = await database.groupDao.deleteGroup( - groupId: testgroup.id, - ); - expect(groupRemoved, true); + test('get group count works correctly', () async { + final initialCount = await database.groupDao.getGroupCount(); + expect(initialCount, 0); - final finalCount = await database.groupDao.getGroupCount(); - expect(finalCount, 0); + await database.groupDao.addGroup(group: testgroup); + + final groupAdded = await database.groupDao.getGroupCount(); + expect(groupAdded, 1); + + final groupRemoved = await database.groupDao.deleteGroup( + groupId: testgroup.id, + ); + expect(groupRemoved, true); + + final finalCount = await database.groupDao.getGroupCount(); + expect(finalCount, 0); + }); }); } diff --git a/test/db_tests/player_test.dart b/test/db_tests/player_test.dart index 7fb9152..5258c66 100644 --- a/test/db_tests/player_test.dart +++ b/test/db_tests/player_test.dart @@ -24,6 +24,25 @@ void main() { }); group('player tests', () { + test('all players get fetched correctly', () async { + final testPlayer2 = Player(id: 'gr2', name: 'Second Group'); + await database.playerDao.addPlayer(player: testPlayer); + await database.playerDao.addPlayer(player: testPlayer2); + + final allPlayers = await database.playerDao.getAllPlayers(); + expect(allPlayers.length, 2); + + final fetchedPlayer1 = allPlayers.firstWhere( + (g) => g.id == testPlayer.id, + ); + expect(fetchedPlayer1.name, testPlayer.name); + + final fetchedPlayer2 = allPlayers.firstWhere( + (g) => g.id == testPlayer2.id, + ); + expect(fetchedPlayer2.name, testPlayer2.name); + }); + test('players get inserted correcly ', () async { await database.playerDao.addPlayer(player: testPlayer); final result = await database.playerDao.getPlayerById(