22 Commits

Author SHA1 Message Date
74402f2e04 Added fallbacks for players when same player already exists 2025-11-16 19:02:20 +01:00
5c09dfb47d Added fallbacks for groups when same group already exists 2025-11-16 19:01:58 +01:00
02735b5b1d Added missing test 2025-11-16 18:58:01 +01:00
a6f40456d8 added missing const 2025-11-16 18:44:56 +01:00
d931e85e9f changed member tile color 2025-11-16 18:36:54 +01:00
a707990356 added new color: onBoxColor 2025-11-16 18:36:38 +01:00
3bd522df6e change colors to fit home page 2025-11-16 18:29:27 +01:00
2dd4f52336 removed sample groups from groups view 2025-11-16 16:42:18 +01:00
2dad822d79 add sample groups with sample members at app startup 2025-11-16 16:34:51 +01:00
640830d8ab fixed getAllGroups not returning members 2025-11-16 16:26:28 +01:00
168d7748a9 added todo for getAllGroups tests 2025-11-16 16:26:08 +01:00
b475237b9e ignored text for players in skeleton loading 2025-11-16 16:25:50 +01:00
2a7d409df0 Merge remote-tracking branch 'origin/development' into feature/4-groupview-erstellen 2025-11-16 15:55:23 +01:00
7c34e51779 fixed black bar behind nav bar by removing safearea 2025-11-15 22:33:00 +01:00
ac6f3d0f92 added missing const's 2025-11-15 22:10:51 +01:00
511124e323 reduced mock data loading time 2025-11-15 21:57:31 +01:00
d07bc6e8bb increased skeleton item count & added daos as import 2025-11-15 21:43:03 +01:00
352fdc13f1 added skeletonizer package requirement 2025-11-15 21:25:12 +01:00
d36348c59c added skeleton loading 2025-11-15 21:24:52 +01:00
d08c79fc26 implemented groups view with create group button 2025-11-15 20:22:09 +01:00
1081fb8be7 rearrange imports and fix wrong syntax in widget call 2025-11-15 16:08:47 +01:00
2ee4d8e6fa implemented basic layout & functionality for add group button & group tiles 2025-11-14 21:53:33 +01:00
11 changed files with 370 additions and 45 deletions

View File

@@ -5,6 +5,7 @@ class CustomTheme {
static Color secondaryColor = const Color(0xFFAFA2FF); static Color secondaryColor = const Color(0xFFAFA2FF);
static Color backgroundColor = const Color(0xFF0B0B0B); static Color backgroundColor = const Color(0xFF0B0B0B);
static Color boxColor = const Color(0xFF101010); static Color boxColor = const Color(0xFF101010);
static Color onBoxColor = const Color(0xFF181818);
static Color boxBorder = const Color(0xFF272727); static Color boxBorder = const Color(0xFF272727);
static AppBarTheme appBarTheme = AppBarTheme( static AppBarTheme appBarTheme = AppBarTheme(

View File

@@ -14,9 +14,14 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
Future<List<Group>> getAllGroups() async { Future<List<Group>> getAllGroups() async {
final query = select(groupTable); final query = select(groupTable);
final result = await query.get(); final result = await query.get();
return result return Future.wait(
.map((row) => Group(id: row.id, name: row.name, members: [])) result.map((groupData) async {
.toList(); 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. /// Retrieves a [Group] by its [groupId], including its members.
@@ -33,28 +38,32 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
/// Adds a new group with the given [id] and [name] to the database. /// Adds a new group with the given [id] and [name] to the database.
/// This method also adds the group's members to the [PlayerGroupTable]. /// This method also adds the group's members to the [PlayerGroupTable].
Future<void> addGroup({required Group group}) async { Future<bool> addGroup({required Group group}) async {
await db.transaction(() async { if (!await groupExists(groupId: group.id)) {
await into( await db.transaction(() async {
groupTable, await into(
).insert(GroupTableCompanion.insert(id: group.id, name: group.name)); groupTable,
await db.batch( ).insert(GroupTableCompanion.insert(id: group.id, name: group.name));
(b) => b.insertAll( await db.batch(
db.playerGroupTable, (b) => b.insertAll(
group.members db.playerGroupTable,
.map( group.members
(member) => PlayerGroupTableCompanion.insert( .map(
playerId: member.id, (member) => PlayerGroupTableCompanion.insert(
groupId: group.id, playerId: member.id,
), groupId: group.id,
) ),
.toList(), )
), .toList(),
); ),
await Future.wait( );
group.members.map((player) => db.playerDao.addPlayer(player: player)), 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. /// Deletes the group with the given [id] from the database.

View File

@@ -26,14 +26,14 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
/// Adds a new [player] to the database. /// Adds a new [player] to the database.
/// If a player with the same ID already exists, updates their name to /// If a player with the same ID already exists, updates their name to
/// the new one. /// the new one.
Future<void> addPlayer({required Player player}) async { Future<bool> addPlayer({required Player player}) async {
if (!await playerExists(playerId: player.id)) { if (!await playerExists(playerId: player.id)) {
await into( await into(
playerTable, playerTable,
).insert(PlayerTableCompanion.insert(id: player.id, name: player.name)); ).insert(PlayerTableCompanion.insert(id: player.id, name: player.name));
} else { return true;
await updatePlayername(playerId: player.id, newName: player.name);
} }
return false;
} }
/// Deletes the player with the given [id] from the database. /// Deletes the player with the given [id] from the database.

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.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/views/main_menu/custom_navigation_bar.dart'; import 'package:game_tracker/presentation/views/main_menu/custom_navigation_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -19,6 +21,7 @@ class GameTracker extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
addSampleGroupData(context);
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
title: 'Game Tracker', title: 'Game Tracker',
@@ -39,4 +42,74 @@ class GameTracker extends StatelessWidget {
home: const CustomNavigationBar(), home: const CustomNavigationBar(),
); );
} }
Future<void> addSampleGroupData(BuildContext context) async {
final db = Provider.of<AppDatabase>(context, listen: false);
if (await db.groupDao.getGroupCount() == 0) {
final List<Player> allPlayers = [
Player(id: '1', name: 'Alex'),
Player(id: '2', name: 'Ben'),
Player(id: '3', name: 'Chris'),
Player(id: '4', name: 'Daniel'),
Player(id: '5', name: 'Max Mustermann'),
Player(id: '6', name: 'Sebastian'),
Player(id: '7', name: 'Jonathan'),
Player(id: '8', name: 'Alexander'),
];
// 2. Erstelle und füge 8 Gruppen mit unterschiedlicher Spieleranzahl hinzu
await db.groupDao.addGroup(
group: Group(
id: '1',
name: 'Anfänger',
members: allPlayers.sublist(0, 3),
),
); // 3 Spieler
await db.groupDao.addGroup(
group: Group(
id: '2',
name: 'Die glorreichen Sieben',
members: allPlayers.sublist(0, 7),
),
); // 7 Spieler
await db.groupDao.addGroup(
group: Group(
id: '3',
name: 'Profis',
members: allPlayers.sublist(4, 8),
),
); // 4 Spieler
await db.groupDao.addGroup(
group: Group(
id: '4',
name: 'Duo Infernale',
members: [allPlayers[0], allPlayers[7]],
),
); // 2 Spieler
await db.groupDao.addGroup(
group: Group(
id: '5',
name: 'Die fantastischen Fünf',
members: allPlayers.sublist(1, 6),
),
); // 5 Spieler
await db.groupDao.addGroup(
group: Group(
id: '6',
name: 'Feierabend-Zocker',
members: allPlayers.sublist(0, 6),
),
); // 6 Spieler
await db.groupDao.addGroup(
group: Group(id: '7', name: 'Alle Mann an Bord!', members: allPlayers),
); // 8 Spieler
await db.groupDao.addGroup(
group: Group(
id: '8',
name: 'Testgruppe Alpha',
members: [allPlayers[1], allPlayers[3]],
),
);
}
}
} }

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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/double_row_info_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
class GameHistoryView extends StatefulWidget { class GameHistoryView extends StatefulWidget {
const GameHistoryView({super.key}); const GameHistoryView({super.key});
@@ -178,9 +178,11 @@ class _GameHistoryViewState extends State<GameHistoryView> {
Widget gameHistoryListView(allGameData, suggestedGameData) { Widget gameHistoryListView(allGameData, suggestedGameData) {
if (suggestedGameData.isEmpty && allGameData.isEmpty) { if (suggestedGameData.isEmpty && allGameData.isEmpty) {
return TopCenteredMessage("Keine Spiele erstellt"); return TopCenteredMessage(message: "Keine Spiele erstellt");
} else if (suggestedGameData.isEmpty) { } else if (suggestedGameData.isEmpty) {
return TopCenteredMessage("Kein Spiel mit den Suchparametern gefunden."); return TopCenteredMessage(
message: "Kein Spiel mit den Suchparametern gefunden.",
);
} }
return ListView.builder( return ListView.builder(
itemCount: suggestedGameData.length, itemCount: suggestedGameData.length,

View File

@@ -1,10 +1,108 @@
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/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}); const GroupsView({super.key});
@override
State<GroupsView> createState() => _GroupsViewState();
}
class _GroupsViewState extends State<GroupsView> {
late Future<List<Group>> _allGroupsFuture;
final player = Player(id: 'p1', name: 'Sample');
late final List<Group> skeletonData = List.filled(
8,
Group(
id: '0',
name: 'Sample Game',
members: [player, player, player, player],
),
);
@override
void initState() {
super.initState();
final db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const Center(child: Text('Groups View')); return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
body: Stack(
alignment: Alignment.center,
children: [
FutureBuilder<List<Group>>(
future: _allGroupsFuture,
builder:
(BuildContext context, AsyncSnapshot<List<Group>> snapshot) {
if (snapshot.hasError) {
return const Center(
child: TopCenteredMessage(
message: 'Error while loading group data.',
),
);
}
if (snapshot.connectionState == ConnectionState.done &&
(!snapshot.hasData || snapshot.data!.isEmpty)) {
return const Center(
child: TopCenteredMessage(
message: 'No groups created yet.',
),
);
}
final bool isLoading =
snapshot.connectionState == ConnectionState.waiting;
final List<Group> 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: () {}),
),
],
),
);
} }
} }

View File

@@ -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.of(context).size.width * 0.90, 60),
backgroundColor: CustomTheme.primaryColor,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
),
child: Text(
text,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 22,
color: Colors.white,
),
),
);
}
}

View File

@@ -0,0 +1,82 @@
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(
width: MediaQuery.of(context).size.width * 0.90,
margin: const EdgeInsets.symmetric(horizontal: 20, 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: 20,
),
),
const Spacer(),
Text(
'${group.members.length}',
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 20,
),
),
const SizedBox(width: 3),
const Icon(Icons.group),
],
),
const SizedBox(height: 5),
Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12.0,
runSpacing: 8.0,
children: <Widget>[
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: 17,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
const SizedBox(height: 2.5),
],
),
);
}
}

View File

@@ -1,14 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
Widget TopCenteredMessage(String message) { class TopCenteredMessage extends StatelessWidget {
return Container( const TopCenteredMessage({super.key, required this.message});
padding: EdgeInsets.only(top: 100),
margin: EdgeInsets.only(left: 10, right: 10), final String message;
alignment: Alignment.topCenter,
child: Text( @override
"$message", Widget build(BuildContext context) {
style: TextStyle(fontSize: 20), return Container(
textAlign: TextAlign.center, padding: const EdgeInsets.only(top: 100),
), margin: const EdgeInsets.symmetric(horizontal: 10),
); alignment: Alignment.topCenter,
child: Text(
message,
style: const TextStyle(fontSize: 20),
textAlign: TextAlign.center,
),
);
}
} }

View File

@@ -18,6 +18,7 @@ dependencies:
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
path_provider: ^2.1.5 path_provider: ^2.1.5
provider: ^6.1.5 provider: ^6.1.5
skeletonizer: ^2.1.0+1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@@ -36,6 +36,29 @@ void main() {
await database.close(); await database.close();
}); });
test('all groups get fetched correclty', () 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);
final allGroups = await database.groupDao.getAllGroups();
expect(allGroups.length, 2);
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);
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);
});
test('group and group members gets added correctly', () async { test('group and group members gets added correctly', () async {
await database.groupDao.addGroup(group: testgroup); await database.groupDao.addGroup(group: testgroup);