Merge branch 'development' into feature/48-game-result-view-erstellen
This commit is contained in:
@@ -8,6 +8,19 @@ class CustomTheme {
|
|||||||
static Color onBoxColor = const Color(0xFF181818);
|
static Color onBoxColor = const Color(0xFF181818);
|
||||||
static Color boxBorder = const Color(0xFF272727);
|
static Color boxBorder = const Color(0xFF272727);
|
||||||
|
|
||||||
|
static BoxDecoration standardBoxDecoration = BoxDecoration(
|
||||||
|
color: boxColor,
|
||||||
|
border: Border.all(color: boxBorder),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
);
|
||||||
|
|
||||||
|
static BoxDecoration highlightedBoxDecoration = BoxDecoration(
|
||||||
|
color: boxColor,
|
||||||
|
border: Border.all(color: primaryColor),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [BoxShadow(color: primaryColor.withAlpha(120), blurRadius: 12)],
|
||||||
|
);
|
||||||
|
|
||||||
static AppBarTheme appBarTheme = AppBarTheme(
|
static AppBarTheme appBarTheme = AppBarTheme(
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: backgroundColor,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
|
|||||||
@@ -22,3 +22,10 @@ enum ImportResult {
|
|||||||
/// - [ExportResult.canceled]: The export operation was canceled by the user.
|
/// - [ExportResult.canceled]: The export operation was canceled by the user.
|
||||||
/// - [ExportResult.unknownException]: An exception occurred during export.
|
/// - [ExportResult.unknownException]: An exception occurred during export.
|
||||||
enum ExportResult { success, canceled, unknownException }
|
enum ExportResult { success, canceled, unknownException }
|
||||||
|
|
||||||
|
/// Different rulesets available for games
|
||||||
|
/// - [Ruleset.singleWinner]: The game is won by a single player
|
||||||
|
/// - [Ruleset.singleLoser]: The game is lost by a single player
|
||||||
|
/// - [Ruleset.mostPoints]: The player with the most points wins.
|
||||||
|
/// - [Ruleset.lastPoints]: The player with the fewest points wins.
|
||||||
|
enum Ruleset { singleWinner, singleLoser, mostPoints, lastPoints }
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (game.players != null) {
|
if (game.players != null) {
|
||||||
await db.playerDao.addPlayers(players: game.players!);
|
await db.playerDao.addPlayersAsList(players: game.players!);
|
||||||
for (final p in game.players ?? []) {
|
for (final p in game.players ?? []) {
|
||||||
await db.playerGameDao.addPlayerToGame(
|
await db.playerGameDao.addPlayerToGame(
|
||||||
gameId: game.id,
|
gameId: game.id,
|
||||||
@@ -89,12 +89,18 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
|||||||
|
|
||||||
if (game.group != null) {
|
if (game.group != null) {
|
||||||
await db.groupDao.addGroup(group: game.group!);
|
await db.groupDao.addGroup(group: game.group!);
|
||||||
await db.groupGameDao.addGroupToGame(game.id, game.group!.id);
|
await db.groupGameDao.addGroupToGame(
|
||||||
|
gameId: game.id,
|
||||||
|
groupId: game.group!.id,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addGames({required List<Game> games}) async {
|
/// Adds multiple [Game]s to the database in a batch operation.
|
||||||
|
/// Also adds associated players and groups if they exist.
|
||||||
|
/// If the [games] list is empty, the method returns immediately.
|
||||||
|
Future<void> addGamesAsList({required List<Game> games}) async {
|
||||||
if (games.isEmpty) return;
|
if (games.isEmpty) return;
|
||||||
await db.transaction(() async {
|
await db.transaction(() async {
|
||||||
// Add all games in batch
|
// Add all games in batch
|
||||||
@@ -253,4 +259,62 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
|||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the winner of the game with the given [gameId] to the player with
|
||||||
|
/// the given [winnerId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> setWinner({
|
||||||
|
required String gameId,
|
||||||
|
required String winnerId,
|
||||||
|
}) async {
|
||||||
|
final query = update(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final rowsAffected = await query.write(
|
||||||
|
GameTableCompanion(winnerId: Value(winnerId)),
|
||||||
|
);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retrieves the winner of the game with the given [gameId].
|
||||||
|
/// Returns the [Player] who won the game, or `null` if no winner is set.
|
||||||
|
Future<Player?> getWinner({required String gameId}) async {
|
||||||
|
final query = select(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final result = await query.getSingleOrNull();
|
||||||
|
if (result == null || result.winnerId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final winner = await db.playerDao.getPlayerById(playerId: result.winnerId!);
|
||||||
|
return winner;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the winner of the game with the given [gameId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> removeWinner({required String gameId}) async {
|
||||||
|
final query = update(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final rowsAffected = await query.write(
|
||||||
|
const GameTableCompanion(winnerId: Value(null)),
|
||||||
|
);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if the game with the given [gameId] has a winner set.
|
||||||
|
/// Returns `true` if a winner is set, otherwise `false`.
|
||||||
|
Future<bool> hasWinner({required String gameId}) async {
|
||||||
|
final query = select(gameTable)
|
||||||
|
..where((g) => g.id.equals(gameId) & g.winnerId.isNotNull());
|
||||||
|
final result = await query.getSingleOrNull();
|
||||||
|
return result != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Changes the title of the game with the given [gameId] to [newName].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> updateGameName({
|
||||||
|
required String gameId,
|
||||||
|
required String newName,
|
||||||
|
}) async {
|
||||||
|
final query = update(gameTable)..where((g) => g.id.equals(gameId));
|
||||||
|
final rowsAffected = await query.write(
|
||||||
|
GameTableCompanion(name: Value(newName)),
|
||||||
|
);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class GroupDao extends DatabaseAccessor<AppDatabase> with _$GroupDaoMixin {
|
|||||||
|
|
||||||
/// Adds multiple groups to the database.
|
/// Adds multiple groups to the database.
|
||||||
/// Also adds the group's members to the [PlayerGroupTable].
|
/// Also adds the group's members to the [PlayerGroupTable].
|
||||||
Future<void> addGroups({required List<Group> groups}) async {
|
Future<void> addGroupsAsList({required List<Group> groups}) async {
|
||||||
if (groups.isEmpty) return;
|
if (groups.isEmpty) return;
|
||||||
await db.transaction(() async {
|
await db.transaction(() async {
|
||||||
// Deduplicate groups by id - keep first occurrence
|
// Deduplicate groups by id - keep first occurrence
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
|
|
||||||
/// Associates a group with a game by inserting a record into the
|
/// Associates a group with a game by inserting a record into the
|
||||||
/// [GroupGameTable].
|
/// [GroupGameTable].
|
||||||
Future<void> addGroupToGame(String gameId, String groupId) async {
|
Future<void> addGroupToGame({
|
||||||
|
required String gameId,
|
||||||
|
required String groupId,
|
||||||
|
}) async {
|
||||||
|
if (await gameHasGroup(gameId: gameId)) {
|
||||||
|
throw Exception('Game already has a group');
|
||||||
|
}
|
||||||
await into(groupGameTable).insert(
|
await into(groupGameTable).insert(
|
||||||
GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId),
|
GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId),
|
||||||
mode: InsertMode.insertOrReplace,
|
mode: InsertMode.insertOrReplace,
|
||||||
@@ -76,4 +82,17 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the group associated with a game to [newGroupId] based on
|
||||||
|
/// [gameId].
|
||||||
|
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
|
||||||
|
Future<bool> updateGroupOfGame({
|
||||||
|
required String gameId,
|
||||||
|
required String newGroupId,
|
||||||
|
}) async {
|
||||||
|
final updatedRows =
|
||||||
|
await (update(groupGameTable)..where((g) => g.gameId.equals(gameId)))
|
||||||
|
.write(GroupGameTableCompanion(groupId: Value(newGroupId)));
|
||||||
|
return updatedRows > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class PlayerDao extends DatabaseAccessor<AppDatabase> with _$PlayerDaoMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Adds multiple [players] to the database in a batch operation.
|
/// Adds multiple [players] to the database in a batch operation.
|
||||||
Future<bool> addPlayers({required List<Player> players}) async {
|
Future<bool> addPlayersAsList({required List<Player> players}) async {
|
||||||
if (players.isEmpty) return false;
|
if (players.isEmpty) return false;
|
||||||
|
|
||||||
await db.batch(
|
await db.batch(
|
||||||
|
|||||||
@@ -79,4 +79,50 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
|
|||||||
final rowsAffected = await query.go();
|
final rowsAffected = await query.go();
|
||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates the players associated with a game based on the provided
|
||||||
|
/// [newPlayer] list. It adds new players and removes players that are no
|
||||||
|
/// longer associated with the game.
|
||||||
|
Future<void> updatePlayersFromGame({
|
||||||
|
required String gameId,
|
||||||
|
required List<Player> newPlayer,
|
||||||
|
}) async {
|
||||||
|
final currentPlayers = await getPlayersOfGame(gameId: gameId);
|
||||||
|
// Create sets of player IDs for easy comparison
|
||||||
|
final currentPlayerIds = currentPlayers?.map((p) => p.id).toSet() ?? {};
|
||||||
|
final newPlayerIdsSet = newPlayer.map((p) => p.id).toSet();
|
||||||
|
|
||||||
|
// Determine players to add and remove
|
||||||
|
final playersToAdd = newPlayerIdsSet.difference(currentPlayerIds);
|
||||||
|
final playersToRemove = currentPlayerIds.difference(newPlayerIdsSet);
|
||||||
|
|
||||||
|
db.transaction(() async {
|
||||||
|
// Remove old players
|
||||||
|
if (playersToRemove.isNotEmpty) {
|
||||||
|
await (delete(playerGameTable)..where(
|
||||||
|
(pg) =>
|
||||||
|
pg.gameId.equals(gameId) &
|
||||||
|
pg.playerId.isIn(playersToRemove.toList()),
|
||||||
|
))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new players
|
||||||
|
if (playersToAdd.isNotEmpty) {
|
||||||
|
final inserts = playersToAdd
|
||||||
|
.map(
|
||||||
|
(id) =>
|
||||||
|
PlayerGameTableCompanion.insert(playerId: id, gameId: gameId),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
await Future.wait(
|
||||||
|
inserts.map(
|
||||||
|
(c) => into(
|
||||||
|
playerGameTable,
|
||||||
|
).insert(c, mode: InsertMode.insertOrReplace),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
|
||||||
|
|
||||||
|
class ChooseGroupView extends StatefulWidget {
|
||||||
|
final List<Group> groups;
|
||||||
|
final int initialGroupIndex;
|
||||||
|
|
||||||
|
const ChooseGroupView({
|
||||||
|
super.key,
|
||||||
|
required this.groups,
|
||||||
|
required this.initialGroupIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChooseGroupView> createState() => _ChooseGroupViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChooseGroupViewState extends State<ChooseGroupView> {
|
||||||
|
late int selectedGroupIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
selectedGroupIndex = widget.initialGroupIndex;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Choose Group',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 85),
|
||||||
|
itemCount: widget.groups.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
selectedGroupIndex = index;
|
||||||
|
});
|
||||||
|
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(context).pop(widget.groups[index]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: GroupTile(
|
||||||
|
group: widget.groups[index],
|
||||||
|
isHighlighted: selectedGroupIndex == index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
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/ruleset_list_tile.dart';
|
||||||
|
|
||||||
|
class ChooseRulesetView extends StatefulWidget {
|
||||||
|
final List<(Ruleset, String, String)> rulesets;
|
||||||
|
final int initialRulesetIndex;
|
||||||
|
const ChooseRulesetView({
|
||||||
|
super.key,
|
||||||
|
required this.rulesets,
|
||||||
|
required this.initialRulesetIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
|
||||||
|
late int selectedRulesetIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
selectedRulesetIndex = widget.initialRulesetIndex;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DefaultTabController(
|
||||||
|
length: 2,
|
||||||
|
initialIndex: 0,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Choose Ruleset',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: CustomTheme.backgroundColor,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: TabBar(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 5),
|
||||||
|
// Label Settings
|
||||||
|
labelStyle: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
labelColor: Colors.white,
|
||||||
|
unselectedLabelStyle: const TextStyle(fontSize: 14),
|
||||||
|
unselectedLabelColor: Colors.white70,
|
||||||
|
// Indicator Settings
|
||||||
|
indicator: CustomTheme.standardBoxDecoration,
|
||||||
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
|
indicatorWeight: 1,
|
||||||
|
indicatorPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20,
|
||||||
|
vertical: 0,
|
||||||
|
),
|
||||||
|
// Divider Settings
|
||||||
|
dividerHeight: 0,
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: 'Rulesets'),
|
||||||
|
Tab(text: 'Gametypes'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
indent: 30,
|
||||||
|
endIndent: 30,
|
||||||
|
thickness: 3,
|
||||||
|
radius: BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
children: [
|
||||||
|
ListView.builder(
|
||||||
|
padding: const EdgeInsets.only(bottom: 85),
|
||||||
|
itemCount: widget.rulesets.length,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
return RulesetListTile(
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
selectedRulesetIndex = index;
|
||||||
|
});
|
||||||
|
Future.delayed(const Duration(milliseconds: 500), () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(widget.rulesets[index].$1);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: widget.rulesets[index].$2,
|
||||||
|
description: widget.rulesets[index].$3,
|
||||||
|
isHighlighted: selectedRulesetIndex == index,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Center(
|
||||||
|
child: Text(
|
||||||
|
'No gametypes available',
|
||||||
|
style: TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
import 'package:game_tracker/core/enums.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/views/main_menu/create_game/choose_group_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class CreateGameView extends StatefulWidget {
|
||||||
|
const CreateGameView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateGameView> createState() => _CreateGameViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateGameViewState extends State<CreateGameView> {
|
||||||
|
/// Reference to the app database
|
||||||
|
late final AppDatabase db;
|
||||||
|
|
||||||
|
/// Futures to load all groups and players from the database
|
||||||
|
late Future<List<Group>> _allGroupsFuture;
|
||||||
|
|
||||||
|
/// Future to load all players from the database
|
||||||
|
late Future<List<Player>> _allPlayersFuture;
|
||||||
|
|
||||||
|
/// Controller for the game name input field
|
||||||
|
final TextEditingController _gameNameController = TextEditingController();
|
||||||
|
|
||||||
|
/// List of all groups from the database
|
||||||
|
List<Group> groupsList = [];
|
||||||
|
|
||||||
|
/// List of all players from the database
|
||||||
|
List<Player> playerList = [];
|
||||||
|
|
||||||
|
/// The currently selected group
|
||||||
|
Group? selectedGroup;
|
||||||
|
|
||||||
|
/// The index of the currently selected group in [groupsList] to mark it in
|
||||||
|
/// the [ChooseGroupView]
|
||||||
|
int selectedGroupIndex = -1;
|
||||||
|
|
||||||
|
/// The currently selected ruleset
|
||||||
|
Ruleset? selectedRuleset;
|
||||||
|
|
||||||
|
/// The index of the currently selected ruleset in [rulesets] to mark it in
|
||||||
|
/// the [ChooseRulesetView]
|
||||||
|
int selectedRulesetIndex = -1;
|
||||||
|
|
||||||
|
/// The currently selected players
|
||||||
|
List<Player>? selectedPlayers;
|
||||||
|
|
||||||
|
/// List of available rulesets with their display names and descriptions
|
||||||
|
/// as tuples of (Ruleset, String, String)
|
||||||
|
List<(Ruleset, String, String)> rulesets = [
|
||||||
|
(
|
||||||
|
Ruleset.singleWinner,
|
||||||
|
'Single Winner',
|
||||||
|
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ruleset.singleLoser,
|
||||||
|
'Single Loser',
|
||||||
|
'Exactly one loser is determined; last place receives the penalty or consequence.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ruleset.mostPoints,
|
||||||
|
'Most Points',
|
||||||
|
'Traditional ruleset: the player with the most points wins.',
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Ruleset.lastPoints,
|
||||||
|
'Least Points',
|
||||||
|
'Inverse scoring: the player with the fewest points wins.',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
|
|
||||||
|
_allGroupsFuture = db.groupDao.getAllGroups();
|
||||||
|
_allPlayersFuture = db.playerDao.getAllPlayers();
|
||||||
|
|
||||||
|
Future.wait([_allGroupsFuture, _allPlayersFuture]).then((result) async {
|
||||||
|
groupsList = result[0] as List<Group>;
|
||||||
|
playerList = result[1] as List<Player>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
|
scrolledUnderElevation: 0,
|
||||||
|
title: const Text(
|
||||||
|
'Create new game',
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
child: TextInputField(
|
||||||
|
controller: _gameNameController,
|
||||||
|
hintText: 'Game name',
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ChooseTile(
|
||||||
|
title: 'Ruleset',
|
||||||
|
trailingText: selectedRuleset == null
|
||||||
|
? 'None'
|
||||||
|
: translateRulesetToString(selectedRuleset!),
|
||||||
|
onPressed: () async {
|
||||||
|
selectedRuleset = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChooseRulesetView(
|
||||||
|
rulesets: rulesets,
|
||||||
|
initialRulesetIndex: selectedRulesetIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
selectedRulesetIndex = rulesets.indexWhere(
|
||||||
|
(r) => r.$1 == selectedRuleset,
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ChooseTile(
|
||||||
|
title: 'Group',
|
||||||
|
trailingText: selectedGroup == null
|
||||||
|
? 'None'
|
||||||
|
: selectedGroup!.name,
|
||||||
|
onPressed: () async {
|
||||||
|
selectedGroup = await Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ChooseGroupView(
|
||||||
|
groups: groupsList,
|
||||||
|
initialGroupIndex: selectedGroupIndex,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
selectedGroupIndex = groupsList.indexWhere(
|
||||||
|
(g) => g.id == selectedGroup?.id,
|
||||||
|
);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: PlayerSelection(
|
||||||
|
key: ValueKey(selectedGroup?.id ?? 'no_group'),
|
||||||
|
initialPlayers: selectedGroup == null
|
||||||
|
? playerList
|
||||||
|
: playerList
|
||||||
|
.where(
|
||||||
|
(p) => !selectedGroup!.members.any(
|
||||||
|
(m) => m.id == p.id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
selectedPlayers = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
CustomWidthButton(
|
||||||
|
text: 'Create game',
|
||||||
|
sizeRelativeToWidth: 0.95,
|
||||||
|
buttonType: ButtonType.primary,
|
||||||
|
onPressed: _enableCreateGameButton()
|
||||||
|
? () async {
|
||||||
|
Game game = Game(
|
||||||
|
name: _gameNameController.text.trim(),
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
group: selectedGroup!,
|
||||||
|
players: selectedPlayers,
|
||||||
|
);
|
||||||
|
// TODO: Replace with navigation to GameResultView()
|
||||||
|
print('Created game: $game');
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Translates a [Ruleset] enum value to its corresponding string representation.
|
||||||
|
String translateRulesetToString(Ruleset ruleset) {
|
||||||
|
switch (ruleset) {
|
||||||
|
case Ruleset.singleWinner:
|
||||||
|
return 'Single Winner';
|
||||||
|
case Ruleset.singleLoser:
|
||||||
|
return 'Single Loser';
|
||||||
|
case Ruleset.mostPoints:
|
||||||
|
return 'Most Points';
|
||||||
|
case Ruleset.lastPoints:
|
||||||
|
return 'Least Points';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines whether the "Create Game" button should be enabled based on
|
||||||
|
/// the current state of the input fields.
|
||||||
|
bool _enableCreateGameButton() {
|
||||||
|
return _gameNameController.text.isNotEmpty &&
|
||||||
|
(selectedGroup != null ||
|
||||||
|
(selectedPlayers != null && selectedPlayers!.isNotEmpty)) &&
|
||||||
|
selectedRuleset != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ import 'package:game_tracker/data/dto/group.dart';
|
|||||||
import 'package:game_tracker/data/dto/player.dart';
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/text_input_field.dart';
|
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class CreateGroupView extends StatefulWidget {
|
class CreateGroupView extends StatefulWidget {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import 'package:game_tracker/data/dto/game.dart';
|
|||||||
import 'package:game_tracker/data/dto/group.dart';
|
import 'package:game_tracker/data/dto/group.dart';
|
||||||
import 'package:game_tracker/data/dto/player.dart';
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
|
import 'package:game_tracker/presentation/views/main_menu/create_group_view.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
|
|
||||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
|
||||||
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart';
|
||||||
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class GameHistoryView extends StatefulWidget {
|
class GameHistoryView extends StatefulWidget {
|
||||||
@@ -38,9 +38,7 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
winner: Player(name: 'Skeleton Player 1'),
|
winner: Player(name: 'Skeleton Player 1'),
|
||||||
players: [
|
players: [Player(name: 'Skeleton Player 6')],
|
||||||
Player(name: 'Skeleton Player 6')
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -67,7 +65,8 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
children: [
|
children: [
|
||||||
FutureBuilder<List<Game>>(
|
FutureBuilder<List<Game>>(
|
||||||
future: _gameListFuture,
|
future: _gameListFuture,
|
||||||
builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
builder:
|
||||||
|
(BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
||||||
if (snapshot.hasError) {
|
if (snapshot.hasError) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: TopCenteredMessage(
|
child: TopCenteredMessage(
|
||||||
@@ -88,10 +87,11 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<Game> games = (isLoading
|
final List<Game> games =
|
||||||
? skeletonData
|
(isLoading ? skeletonData : (snapshot.data ?? [])
|
||||||
: (snapshot.data ?? [])
|
..sort(
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
|
(a, b) => b.createdAt.compareTo(a.createdAt),
|
||||||
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
return AppSkeleton(
|
return AppSkeleton(
|
||||||
@@ -105,7 +105,9 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
height: MediaQuery.paddingOf(context).bottom - 80,
|
height: MediaQuery.paddingOf(context).bottom - 80,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return GameHistoryTile(game: games[index]); // Placeholder
|
return GameHistoryTile(
|
||||||
|
game: games[index],
|
||||||
|
); // Placeholder
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ class _GroupsViewState extends State<GroupsView> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: MediaQuery.paddingOf(context).bottom,
|
bottom: MediaQuery.paddingOf(context).bottom,
|
||||||
child: CustomWidthButton(
|
child: CustomWidthButton(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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/player.dart';
|
import 'package:game_tracker/data/dto/player.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/custom_search_bar.dart';
|
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/text_icon_list_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||||
@@ -11,8 +11,13 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
class PlayerSelection extends StatefulWidget {
|
class PlayerSelection extends StatefulWidget {
|
||||||
final Function(List<Player> value) onChanged;
|
final Function(List<Player> value) onChanged;
|
||||||
|
final List<Player> initialPlayers;
|
||||||
|
|
||||||
const PlayerSelection({super.key, required this.onChanged});
|
const PlayerSelection({
|
||||||
|
super.key,
|
||||||
|
required this.onChanged,
|
||||||
|
this.initialPlayers = const [],
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlayerSelection> createState() => _PlayerSelectionState();
|
State<PlayerSelection> createState() => _PlayerSelectionState();
|
||||||
@@ -46,9 +51,14 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
|||||||
suggestedPlayers = skeletonData;
|
suggestedPlayers = skeletonData;
|
||||||
_allPlayersFuture.then((loadedPlayers) {
|
_allPlayersFuture.then((loadedPlayers) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
if (widget.initialPlayers.isNotEmpty) {
|
||||||
|
allPlayers = [...widget.initialPlayers];
|
||||||
|
suggestedPlayers = [...widget.initialPlayers];
|
||||||
|
} else {
|
||||||
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||||
allPlayers = [...loadedPlayers];
|
allPlayers = [...loadedPlayers];
|
||||||
suggestedPlayers = [...loadedPlayers];
|
suggestedPlayers = [...loadedPlayers];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -58,11 +68,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
|||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
color: CustomTheme.boxColor,
|
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
43
lib/presentation/widgets/tiles/choose_tile.dart
Normal file
43
lib/presentation/widgets/tiles/choose_tile.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
|
||||||
|
class ChooseTile extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final String? trailingText;
|
||||||
|
const ChooseTile({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
this.trailingText,
|
||||||
|
this.onPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ChooseTile> createState() => _ChooseTileState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ChooseTileState extends State<ChooseTile> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 15),
|
||||||
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
if (widget.trailingText != null) Text(widget.trailingText!),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
const Icon(Icons.arrow_forward_ios, size: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,20 @@ import 'package:game_tracker/data/dto/group.dart';
|
|||||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||||
|
|
||||||
class GroupTile extends StatelessWidget {
|
class GroupTile extends StatelessWidget {
|
||||||
const GroupTile({super.key, required this.group});
|
const GroupTile({super.key, required this.group, this.isHighlighted = false});
|
||||||
|
|
||||||
final Group group;
|
final Group group;
|
||||||
|
final bool isHighlighted;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return AnimatedContainer(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: isHighlighted
|
||||||
color: CustomTheme.boxColor,
|
? CustomTheme.highlightedBoxDecoration
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
: CustomTheme.standardBoxDecoration,
|
||||||
borderRadius: BorderRadius.circular(12),
|
duration: const Duration(milliseconds: 150),
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ class _InfoTileState extends State<InfoTile> {
|
|||||||
padding: widget.padding ?? const EdgeInsets.all(12),
|
padding: widget.padding ?? const EdgeInsets.all(12),
|
||||||
height: widget.height,
|
height: widget.height,
|
||||||
width: widget.width ?? 380,
|
width: widget.width ?? 380,
|
||||||
decoration: BoxDecoration(
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: CustomTheme.boxColor,
|
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ class _QuickInfoTileState extends State<QuickInfoTile> {
|
|||||||
padding: widget.padding ?? const EdgeInsets.all(12),
|
padding: widget.padding ?? const EdgeInsets.all(12),
|
||||||
height: widget.height ?? 110,
|
height: widget.height ?? 110,
|
||||||
width: widget.width ?? 180,
|
width: widget.width ?? 180,
|
||||||
decoration: BoxDecoration(
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
color: CustomTheme.boxColor,
|
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
55
lib/presentation/widgets/tiles/ruleset_list_tile.dart
Normal file
55
lib/presentation/widgets/tiles/ruleset_list_tile.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
|
|
||||||
|
class RulesetListTile extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final String description;
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final bool isHighlighted;
|
||||||
|
|
||||||
|
const RulesetListTile({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.description,
|
||||||
|
this.onPressed,
|
||||||
|
this.isHighlighted = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onPressed,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
|
||||||
|
decoration: isHighlighted
|
||||||
|
? CustomTheme.highlightedBoxDecoration
|
||||||
|
: CustomTheme.standardBoxDecoration,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 5),
|
||||||
|
Text(description, style: const TextStyle(fontSize: 14)),
|
||||||
|
const SizedBox(height: 2.5),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,11 +26,7 @@ class SettingsListTile extends StatelessWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||||
decoration: BoxDecoration(
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
color: CustomTheme.boxColor,
|
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -18,11 +18,7 @@ class TextIconListTile extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
decoration: BoxDecoration(
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
color: CustomTheme.boxColor,
|
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisSize: MainAxisSize.max,
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ class DataTransferService {
|
|||||||
.toList() ??
|
.toList() ??
|
||||||
[];
|
[];
|
||||||
|
|
||||||
await db.playerDao.addPlayers(players: importedPlayers);
|
await db.playerDao.addPlayersAsList(players: importedPlayers);
|
||||||
await db.groupDao.addGroups(groups: importedGroups);
|
await db.groupDao.addGroupsAsList(groups: importedGroups);
|
||||||
await db.gameDao.addGames(games: importedGames);
|
await db.gameDao.addGamesAsList(games: importedGames);
|
||||||
} else {
|
} else {
|
||||||
return ImportResult.invalidSchema;
|
return ImportResult.invalidSchema;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Adding and fetching multiple games works correctly', () async {
|
test('Adding and fetching multiple games works correctly', () async {
|
||||||
await database.gameDao.addGames(
|
await database.gameDao.addGamesAsList(
|
||||||
games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers],
|
games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -234,5 +234,97 @@ void main() {
|
|||||||
gameCount = await database.gameDao.getGameCount();
|
gameCount = await database.gameDao.getGameCount();
|
||||||
expect(gameCount, 0);
|
expect(gameCount, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Checking if game has winner works correclty', () async {
|
||||||
|
await database.gameDao.addGame(game: testGame1);
|
||||||
|
await database.gameDao.addGame(game: testGameOnlyGroup);
|
||||||
|
|
||||||
|
var hasWinner = await database.gameDao.hasWinner(gameId: testGame1.id);
|
||||||
|
expect(hasWinner, true);
|
||||||
|
|
||||||
|
hasWinner = await database.gameDao.hasWinner(
|
||||||
|
gameId: testGameOnlyGroup.id,
|
||||||
|
);
|
||||||
|
expect(hasWinner, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Fetching the winner of a game works correctly', () async {
|
||||||
|
await database.gameDao.addGame(game: testGame1);
|
||||||
|
|
||||||
|
final winner = await database.gameDao.getWinner(gameId: testGame1.id);
|
||||||
|
if (winner == null) {
|
||||||
|
fail('Winner is null');
|
||||||
|
} else {
|
||||||
|
expect(winner.id, testGame1.winner!.id);
|
||||||
|
expect(winner.name, testGame1.winner!.name);
|
||||||
|
expect(winner.createdAt, testGame1.winner!.createdAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Updating the winner of a game works correctly', () async {
|
||||||
|
await database.gameDao.addGame(game: testGame1);
|
||||||
|
|
||||||
|
final winner = await database.gameDao.getWinner(gameId: testGame1.id);
|
||||||
|
if (winner == null) {
|
||||||
|
fail('Winner is null');
|
||||||
|
} else {
|
||||||
|
expect(winner.id, testGame1.winner!.id);
|
||||||
|
expect(winner.name, testGame1.winner!.name);
|
||||||
|
expect(winner.createdAt, testGame1.winner!.createdAt);
|
||||||
|
expect(winner.id, testPlayer4.id);
|
||||||
|
expect(winner.id != testPlayer5.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.gameDao.setWinner(
|
||||||
|
gameId: testGame1.id,
|
||||||
|
winnerId: testPlayer5.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
final newWinner = await database.gameDao.getWinner(gameId: testGame1.id);
|
||||||
|
|
||||||
|
if (newWinner == null) {
|
||||||
|
fail('New winner is null');
|
||||||
|
} else {
|
||||||
|
expect(newWinner.id, testPlayer5.id);
|
||||||
|
expect(newWinner.name, testPlayer5.name);
|
||||||
|
expect(newWinner.createdAt, testPlayer5.createdAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Removing a winner works correctly', () async {
|
||||||
|
await database.gameDao.addGame(game: testGame2);
|
||||||
|
|
||||||
|
var hasWinner = await database.gameDao.hasWinner(gameId: testGame2.id);
|
||||||
|
expect(hasWinner, true);
|
||||||
|
|
||||||
|
await database.gameDao.removeWinner(gameId: testGame2.id);
|
||||||
|
|
||||||
|
hasWinner = await database.gameDao.hasWinner(gameId: testGame2.id);
|
||||||
|
expect(hasWinner, false);
|
||||||
|
|
||||||
|
final removedWinner = await database.gameDao.getWinner(
|
||||||
|
gameId: testGame2.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(removedWinner, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Renaming a game works correctly', () async {
|
||||||
|
await database.gameDao.addGame(game: testGame1);
|
||||||
|
|
||||||
|
var fetchedGame = await database.gameDao.getGameById(
|
||||||
|
gameId: testGame1.id,
|
||||||
|
);
|
||||||
|
expect(fetchedGame.name, testGame1.name);
|
||||||
|
|
||||||
|
const newName = 'Updated Game Name';
|
||||||
|
await database.gameDao.updateGameName(
|
||||||
|
gameId: testGame1.id,
|
||||||
|
newName: newName,
|
||||||
|
);
|
||||||
|
|
||||||
|
fetchedGame = await database.gameDao.getGameById(gameId: testGame1.id);
|
||||||
|
expect(fetchedGame.name, newName);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ void main() {
|
|||||||
late Player testPlayer3;
|
late Player testPlayer3;
|
||||||
late Player testPlayer4;
|
late Player testPlayer4;
|
||||||
late Player testPlayer5;
|
late Player testPlayer5;
|
||||||
late Group testgroup;
|
late Group testGroup1;
|
||||||
|
late Group testGroup2;
|
||||||
late Game testgameWithGroup;
|
late Game testgameWithGroup;
|
||||||
late Game testgameWithPlayers;
|
late Game testgameWithPlayers;
|
||||||
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
|
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
|
||||||
@@ -35,15 +36,19 @@ void main() {
|
|||||||
testPlayer3 = Player(name: 'Charlie');
|
testPlayer3 = Player(name: 'Charlie');
|
||||||
testPlayer4 = Player(name: 'Diana');
|
testPlayer4 = Player(name: 'Diana');
|
||||||
testPlayer5 = Player(name: 'Eve');
|
testPlayer5 = Player(name: 'Eve');
|
||||||
testgroup = Group(
|
testGroup1 = Group(
|
||||||
name: 'Test Group',
|
name: 'Test Group',
|
||||||
members: [testPlayer1, testPlayer2, testPlayer3],
|
members: [testPlayer1, testPlayer2, testPlayer3],
|
||||||
);
|
);
|
||||||
|
testGroup2 = Group(
|
||||||
|
name: 'Test Group',
|
||||||
|
members: [testPlayer3, testPlayer2],
|
||||||
|
);
|
||||||
testgameWithPlayers = Game(
|
testgameWithPlayers = Game(
|
||||||
name: 'Test Game with Players',
|
name: 'Test Game with Players',
|
||||||
players: [testPlayer4, testPlayer5],
|
players: [testPlayer4, testPlayer5],
|
||||||
);
|
);
|
||||||
testgameWithGroup = Game(name: 'Test Game with Group', group: testgroup);
|
testgameWithGroup = Game(name: 'Test Game with Group', group: testGroup1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
@@ -52,7 +57,7 @@ void main() {
|
|||||||
group('Group-Game Tests', () {
|
group('Group-Game Tests', () {
|
||||||
test('Game has group works correctly', () async {
|
test('Game has group works correctly', () async {
|
||||||
await database.gameDao.addGame(game: testgameWithPlayers);
|
await database.gameDao.addGame(game: testgameWithPlayers);
|
||||||
await database.groupDao.addGroup(group: testgroup);
|
await database.groupDao.addGroup(group: testGroup1);
|
||||||
|
|
||||||
var gameHasGroup = await database.groupGameDao.gameHasGroup(
|
var gameHasGroup = await database.groupGameDao.gameHasGroup(
|
||||||
gameId: testgameWithPlayers.id,
|
gameId: testgameWithPlayers.id,
|
||||||
@@ -61,8 +66,8 @@ void main() {
|
|||||||
expect(gameHasGroup, false);
|
expect(gameHasGroup, false);
|
||||||
|
|
||||||
await database.groupGameDao.addGroupToGame(
|
await database.groupGameDao.addGroupToGame(
|
||||||
testgameWithPlayers.id,
|
gameId: testgameWithPlayers.id,
|
||||||
testgroup.id,
|
groupId: testGroup1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
gameHasGroup = await database.groupGameDao.gameHasGroup(
|
gameHasGroup = await database.groupGameDao.gameHasGroup(
|
||||||
@@ -74,15 +79,15 @@ void main() {
|
|||||||
|
|
||||||
test('Adding a group to a game works correctly', () async {
|
test('Adding a group to a game works correctly', () async {
|
||||||
await database.gameDao.addGame(game: testgameWithPlayers);
|
await database.gameDao.addGame(game: testgameWithPlayers);
|
||||||
await database.groupDao.addGroup(group: testgroup);
|
await database.groupDao.addGroup(group: testGroup1);
|
||||||
await database.groupGameDao.addGroupToGame(
|
await database.groupGameDao.addGroupToGame(
|
||||||
testgameWithPlayers.id,
|
gameId: testgameWithPlayers.id,
|
||||||
testgroup.id,
|
groupId: testGroup1.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
var groupAdded = await database.groupGameDao.isGroupInGame(
|
var groupAdded = await database.groupGameDao.isGroupInGame(
|
||||||
gameId: testgameWithPlayers.id,
|
gameId: testgameWithPlayers.id,
|
||||||
groupId: testgroup.id,
|
groupId: testGroup1.id,
|
||||||
);
|
);
|
||||||
expect(groupAdded, true);
|
expect(groupAdded, true);
|
||||||
|
|
||||||
@@ -120,14 +125,55 @@ void main() {
|
|||||||
fail('Group should not be null');
|
fail('Group should not be null');
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(group.id, testgroup.id);
|
expect(group.id, testGroup1.id);
|
||||||
expect(group.name, testgroup.name);
|
expect(group.name, testGroup1.name);
|
||||||
expect(group.createdAt, testgroup.createdAt);
|
expect(group.createdAt, testGroup1.createdAt);
|
||||||
expect(group.members.length, testgroup.members.length);
|
expect(group.members.length, testGroup1.members.length);
|
||||||
for (int i = 0; i < group.members.length; i++) {
|
for (int i = 0; i < group.members.length; i++) {
|
||||||
expect(group.members[i].id, testgroup.members[i].id);
|
expect(group.members[i].id, testGroup1.members[i].id);
|
||||||
expect(group.members[i].name, testgroup.members[i].name);
|
expect(group.members[i].name, testGroup1.members[i].name);
|
||||||
expect(group.members[i].createdAt, testgroup.members[i].createdAt);
|
expect(group.members[i].createdAt, testGroup1.members[i].createdAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Updating the group of a game works correctly', () async {
|
||||||
|
await database.gameDao.addGame(game: testgameWithGroup);
|
||||||
|
|
||||||
|
var group = await database.groupGameDao.getGroupOfGame(
|
||||||
|
gameId: testgameWithGroup.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (group == null) {
|
||||||
|
fail('Initial group should not be null');
|
||||||
|
} else {
|
||||||
|
expect(group.id, testGroup1.id);
|
||||||
|
expect(group.name, testGroup1.name);
|
||||||
|
expect(group.createdAt, testGroup1.createdAt);
|
||||||
|
expect(group.members.length, testGroup1.members.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.groupDao.addGroup(group: testGroup2);
|
||||||
|
await database.groupGameDao.updateGroupOfGame(
|
||||||
|
gameId: testgameWithGroup.id,
|
||||||
|
newGroupId: testGroup2.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
group = await database.groupGameDao.getGroupOfGame(
|
||||||
|
gameId: testgameWithGroup.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (group == null) {
|
||||||
|
fail('Updated group should not be null');
|
||||||
|
} else {
|
||||||
|
expect(group.id, testGroup2.id);
|
||||||
|
expect(group.name, testGroup2.name);
|
||||||
|
expect(group.createdAt, testGroup2.createdAt);
|
||||||
|
expect(group.members.length, testGroup2.members.length);
|
||||||
|
for (int i = 0; i < group.members.length; i++) {
|
||||||
|
expect(group.members[i].id, testGroup2.members[i].id);
|
||||||
|
expect(group.members[i].name, testGroup2.members[i].name);
|
||||||
|
expect(group.members[i].createdAt, testGroup2.members[i].createdAt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Adding and fetching multiple groups works correctly', () async {
|
test('Adding and fetching multiple groups works correctly', () async {
|
||||||
await database.groupDao.addGroups(
|
await database.groupDao.addGroupsAsList(
|
||||||
groups: [testGroup1, testGroup2, testGroup3, testGroup4],
|
groups: [testGroup1, testGroup2, testGroup3, testGroup4],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -136,5 +136,48 @@ void main() {
|
|||||||
expect(players[i].createdAt, testGameOnlyPlayers.players![i].createdAt);
|
expect(players[i].createdAt, testGameOnlyPlayers.players![i].createdAt);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Updating the games players works coreclty', () async {
|
||||||
|
await database.gameDao.addGame(game: testGameOnlyPlayers);
|
||||||
|
|
||||||
|
final newPlayers = [testPlayer1, testPlayer2, testPlayer4];
|
||||||
|
await database.playerDao.addPlayersAsList(players: newPlayers);
|
||||||
|
|
||||||
|
// First, remove all existing players
|
||||||
|
final existingPlayers = await database.playerGameDao.getPlayersOfGame(
|
||||||
|
gameId: testGameOnlyPlayers.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingPlayers == null || existingPlayers.isEmpty) {
|
||||||
|
fail('Existing players should not be null or empty');
|
||||||
|
}
|
||||||
|
|
||||||
|
await database.playerGameDao.updatePlayersFromGame(
|
||||||
|
gameId: testGameOnlyPlayers.id,
|
||||||
|
newPlayer: newPlayers,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedPlayers = await database.playerGameDao.getPlayersOfGame(
|
||||||
|
gameId: testGameOnlyPlayers.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (updatedPlayers == null) {
|
||||||
|
fail('Updated players should not be null');
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(updatedPlayers.length, newPlayers.length);
|
||||||
|
|
||||||
|
/// Create a map of new players for easy lookup
|
||||||
|
final testPlayers = {for (var p in newPlayers) p.id: p};
|
||||||
|
|
||||||
|
/// Verify each updated player matches the new players
|
||||||
|
for (final player in updatedPlayers) {
|
||||||
|
final testPlayer = testPlayers[player.id]!;
|
||||||
|
|
||||||
|
expect(player.id, testPlayer.id);
|
||||||
|
expect(player.name, testPlayer.name);
|
||||||
|
expect(player.createdAt, testPlayer.createdAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Adding and fetching multiple players works correctly', () async {
|
test('Adding and fetching multiple players works correctly', () async {
|
||||||
await database.playerDao.addPlayers(
|
await database.playerDao.addPlayersAsList(
|
||||||
players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4],
|
players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user