Merge branch 'development' into feature/108-Möglichkeit-der-horizontalen-Nutzung-deaktivieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m15s
Pull Request Pipeline / lint (pull_request) Successful in 2m22s

This commit is contained in:
2025-12-31 16:41:08 +00:00
11 changed files with 188 additions and 68 deletions

View File

@@ -21,7 +21,7 @@ class GroupMatchDao extends DatabaseAccessor<AppDatabase>
} }
await into(groupMatchTable).insert( await into(groupMatchTable).insert(
GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId), GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrIgnore,
); );
} }

View File

@@ -65,8 +65,9 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
); );
} }
/// Adds a new [Match] to the database. /// Adds a new [Match] to the database. Also adds players and group
/// Also adds associated players and group if they exist. /// associations. This method assumes that the players and groups added to
/// this match are already present in the database.
Future<void> addMatch({required Match match}) async { Future<void> addMatch({required Match match}) async {
await db.transaction(() async { await db.transaction(() async {
await into(matchTable).insert( await into(matchTable).insert(
@@ -80,7 +81,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
); );
if (match.players != null) { if (match.players != null) {
await db.playerDao.addPlayersAsList(players: match.players!);
for (final p in match.players ?? []) { for (final p in match.players ?? []) {
await db.playerMatchDao.addPlayerToMatch( await db.playerMatchDao.addPlayerToMatch(
matchId: match.id, matchId: match.id,
@@ -90,7 +90,6 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
} }
if (match.group != null) { if (match.group != null) {
await db.groupDao.addGroup(group: match.group!);
await db.groupMatchDao.addGroupToMatch( await db.groupMatchDao.addGroupToMatch(
matchId: match.id, matchId: match.id,
groupId: match.group!.id, groupId: match.group!.id,
@@ -102,6 +101,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
/// Adds multiple [Match]s to the database in a batch operation. /// Adds multiple [Match]s to the database in a batch operation.
/// Also adds associated players and groups if they exist. /// Also adds associated players and groups if they exist.
/// If the [matches] list is empty, the method returns immediately. /// If the [matches] list is empty, the method returns immediately.
/// This Method should only be used to import matches from a different device.
Future<void> addMatchAsList({required List<Match> matches}) async { Future<void> addMatchAsList({required List<Match> matches}) async {
if (matches.isEmpty) return; if (matches.isEmpty) return;
await db.transaction(() async { await db.transaction(() async {
@@ -186,7 +186,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
matchId: match.id, matchId: match.id,
playerId: p.id, playerId: p.id,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrIgnore,
); );
} }
} }
@@ -204,7 +204,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
playerId: m.id, playerId: m.id,
groupId: match.group!.id, groupId: match.group!.id,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrIgnore,
); );
} }
} }
@@ -221,7 +221,7 @@ class MatchDao extends DatabaseAccessor<AppDatabase> with _$MatchDaoMixin {
matchId: match.id, matchId: match.id,
groupId: match.group!.id, groupId: match.group!.id,
), ),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrIgnore,
); );
} }
} }

View File

@@ -18,7 +18,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
}) async { }) async {
await into(playerMatchTable).insert( await into(playerMatchTable).insert(
PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId), PlayerMatchTableCompanion.insert(playerId: playerId, matchId: matchId),
mode: InsertMode.insertOrReplace, mode: InsertMode.insertOrIgnore,
); );
} }
@@ -121,7 +121,7 @@ class PlayerMatchDao extends DatabaseAccessor<AppDatabase>
inserts.map( inserts.map(
(c) => into( (c) => into(
playerMatchTable, playerMatchTable,
).insert(c, mode: InsertMode.insertOrReplace), ).insert(c, mode: InsertMode.insertOrIgnore),
), ),
); );
} }

View File

@@ -74,10 +74,18 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
Expanded( Expanded(
child: Visibility( child: Visibility(
visible: filteredGroups.isNotEmpty, visible: filteredGroups.isNotEmpty,
replacement: const TopCenteredMessage( replacement: Visibility(
icon: Icons.info, visible: widget.groups.isNotEmpty,
title: 'Info', replacement: const TopCenteredMessage(
message: 'There is no group matching your search', 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( child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85), padding: const EdgeInsets.only(bottom: 85),

View File

@@ -28,8 +28,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Reference to the app database /// Reference to the app database
late final AppDatabase db; late final AppDatabase db;
/// Controller for the game name input field /// Controller for the match name input field
final TextEditingController _gameNameController = TextEditingController(); final TextEditingController _matchNameController = TextEditingController();
/// List of all groups from the database /// List of all groups from the database
List<Group> groupsList = []; List<Group> groupsList = [];
@@ -95,7 +95,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_gameNameController.addListener(() { _matchNameController.addListener(() {
setState(() {}); setState(() {});
}); });
@@ -119,7 +119,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: const Text( title: const Text(
'Create new game', 'Create new match',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
centerTitle: true, centerTitle: true,
@@ -131,8 +131,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: TextInputField( child: TextInputField(
controller: _gameNameController, controller: _matchNameController,
hintText: 'Game name', hintText: 'Match name',
), ),
), ),
ChooseTile( ChooseTile(
@@ -222,13 +222,13 @@ class _CreateMatchViewState extends State<CreateMatchView> {
), ),
), ),
CustomWidthButton( CustomWidthButton(
text: 'Create game', text: 'Create match',
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton() onPressed: _enableCreateGameButton()
? () async { ? () async {
Match match = Match( Match match = Match(
name: _gameNameController.text.trim(), name: _matchNameController.text.trim(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
@@ -239,7 +239,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
context, context,
CupertinoPageRoute( CupertinoPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => GameResultView( builder: (context) => MatchResultView(
match: match, match: match,
onWinnerChanged: widget.onWinnerChanged, onWinnerChanged: widget.onWinnerChanged,
), ),
@@ -258,7 +258,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Determines whether the "Create Game" button should be enabled based on /// Determines whether the "Create Game" button should be enabled based on
/// the current state of the input fields. /// the current state of the input fields.
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return _gameNameController.text.isNotEmpty && return _matchNameController.text.isNotEmpty &&
(selectedGroup != null || (selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.length > 1)) && (selectedPlayers != null && selectedPlayers!.length > 1)) &&
selectedRuleset != null; selectedRuleset != null;

View File

@@ -6,17 +6,17 @@ import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class GameResultView extends StatefulWidget { class MatchResultView extends StatefulWidget {
final Match match; final Match match;
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
const GameResultView({super.key, required this.match, this.onWinnerChanged}); const MatchResultView({super.key, required this.match, this.onWinnerChanged});
@override @override
State<GameResultView> createState() => _GameResultViewState(); State<MatchResultView> createState() => _MatchResultViewState();
} }
class _GameResultViewState extends State<GameResultView> { class _MatchResultViewState extends State<MatchResultView> {
late final List<Player> allPlayers; late final List<Player> allPlayers;
late final AppDatabase db; late final AppDatabase db;
Player? _selectedPlayer; Player? _selectedPlayer;
@@ -38,6 +38,13 @@ class _GameResultViewState extends State<GameResultView> {
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () {
widget.onWinnerChanged?.call();
Navigator.of(context).pop();
},
),
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
title: Text( title: Text(
@@ -135,12 +142,12 @@ class _GameResultViewState extends State<GameResultView> {
widget.onWinnerChanged?.call(); widget.onWinnerChanged?.call();
} }
List<Player> getAllPlayers(Match game) { List<Player> getAllPlayers(Match match) {
if (game.group == null && game.players != null) { if (match.group == null && match.players != null) {
return [...game.players!]; return [...match.players!];
} else if (game.group != null && game.players != null) { } else if (match.group != null && match.players != null) {
return [...game.players!, ...game.group!.members]; return [...match.players!, ...match.group!.members];
} }
return [...game.group!.members]; return [...match.group!.members];
} }
} }

View File

@@ -80,7 +80,7 @@ class _MatchViewState extends State<MatchView> {
context, context,
CupertinoPageRoute( CupertinoPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => GameResultView( builder: (context) => MatchResultView(
match: matches[index], match: matches[index],
onWinnerChanged: loadGames, onWinnerChanged: loadGames,
), ),

View File

@@ -133,33 +133,42 @@ class _PlayerSelectionState extends State<PlayerSelection> {
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Wrap( SizedBox(
alignment: WrapAlignment.start, height: 50,
crossAxisAlignment: WrapCrossAlignment.start, child: selectedPlayers.isEmpty
spacing: 8.0, ? const Center(child: Text('No players selected'))
runSpacing: 8.0, : SingleChildScrollView(
children: <Widget>[ scrollDirection: Axis.horizontal,
// Generates a TextIconTile for each selected player. child: Row(
for (var player in selectedPlayers) children: [
TextIconTile( for (var player in selectedPlayers)
text: player.name, Padding(
onIconTap: () { padding: const EdgeInsets.only(right: 8.0),
setState(() { child: TextIconTile(
// Removes the player from the selection and notifies the parent. text: player.name,
final currentSearch = _searchBarController.text onIconTap: () {
.toLowerCase(); setState(() {
selectedPlayers.remove(player); // Removes the player from the selection and notifies the parent.
widget.onChanged([...selectedPlayers]); final currentSearch = _searchBarController
// If the player matches the current search query (or search is empty), .text
// they are added back to the suggestions and the list is re-sorted. .toLowerCase();
if (currentSearch.isEmpty || selectedPlayers.remove(player);
player.name.toLowerCase().contains(currentSearch)) { widget.onChanged([...selectedPlayers]);
suggestedPlayers.add(player); // If the player matches the current search query (or search is empty),
} // they are added back to the suggestions and the list is re-sorted.
}); if (currentSearch.isEmpty ||
}, player.name.toLowerCase().contains(
), currentSearch,
], )) {
suggestedPlayers.add(player);
}
});
},
),
),
],
),
),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
const Text( const Text(

View File

@@ -23,7 +23,7 @@ void main() {
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23); final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate); final fakeClock = Clock(() => fixedDate);
setUp(() { setUp(() async {
database = AppDatabase( database = AppDatabase(
DatabaseConnection( DatabaseConnection(
NativeDatabase.memory(), NativeDatabase.memory(),
@@ -68,6 +68,16 @@ void main() {
group: testGroup2, group: testGroup2,
); );
}); });
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
}); });
tearDown(() async { tearDown(() async {
await database.close(); await database.close();
@@ -253,7 +263,7 @@ void main() {
expect(matchCount, 0); 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: testMatch1);
await database.matchDao.addMatch(match: testMatchOnlyGroup); await database.matchDao.addMatch(match: testMatchOnlyGroup);

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart' hide isNotNull;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.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 fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate); final fakeClock = Clock(() => fixedDate);
setUp(() { setUp(() async {
database = AppDatabase( database = AppDatabase(
DatabaseConnection( DatabaseConnection(
NativeDatabase.memory(), NativeDatabase.memory(),
@@ -53,6 +53,16 @@ void main() {
group: testGroup1, group: testGroup1,
); );
}); });
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
],
);
await database.groupDao.addGroupsAsList(groups: [testGroup1, testGroup2]);
}); });
tearDown(() async { tearDown(() async {
await database.close(); 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);
}
});
}); });
} }

View File

@@ -1,5 +1,5 @@
import 'package:clock/clock.dart'; import 'package:clock/clock.dart';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart' hide isNotNull;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:game_tracker/data/db/database.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 fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
final fakeClock = Clock(() => fixedDate); final fakeClock = Clock(() => fixedDate);
setUp(() { setUp(() async {
database = AppDatabase( database = AppDatabase(
DatabaseConnection( DatabaseConnection(
NativeDatabase.memory(), NativeDatabase.memory(),
@@ -50,6 +50,17 @@ void main() {
players: [testPlayer4, testPlayer5, testPlayer6], players: [testPlayer4, testPlayer5, testPlayer6],
); );
}); });
await database.playerDao.addPlayersAsList(
players: [
testPlayer1,
testPlayer2,
testPlayer3,
testPlayer4,
testPlayer5,
testPlayer6,
],
);
await database.groupDao.addGroup(group: testgroup);
}); });
tearDown(() async { tearDown(() async {
await database.close(); await database.close();
@@ -185,5 +196,42 @@ void main() {
expect(player.createdAt, testPlayer.createdAt); 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()),
);
},
);
}); });
} }