Compare commits
111 Commits
290948e50d
...
enhancemen
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f79495775 | |||
| dcd8b460c1 | |||
| dbbe04d4cc | |||
| 1ed6290628 | |||
| 91a7273964 | |||
| b1bb8b919f | |||
| 697767f0de | |||
| 306a783d67 | |||
| 03035138ac | |||
| 7323f52153 | |||
| f5842f9c4a | |||
| e3ac91bf48 | |||
| dba448b9c1 | |||
| d8551b3a27 | |||
| 8f2c7493d0 | |||
| f7f97fcdcb | |||
| 9ac6b6e04c | |||
| e77896c1d4 | |||
| dd2024e96e | |||
| cd9780871f | |||
| 3169eebd14 | |||
| ec902c6196 | |||
| b719a6662b | |||
| 09b407eba8 | |||
| 877c2921d9 | |||
|
|
5ce4964c32 | ||
|
|
fb28de5772 | ||
|
|
f713bd6fb7 | ||
| 71b2f30d29 | |||
| d2d6852f31 | |||
| 126dc7ed97 | |||
| 40a3c1b82e | |||
|
|
da722c5277 | ||
|
|
516c2afd1e | ||
|
|
9ee9da2ac8 | ||
|
|
aa208bb2ef | ||
|
|
a29123c964 | ||
|
|
8c005d6e5e | ||
|
|
cc50e497c9 | ||
|
|
ae348499d4 | ||
|
|
b443230285 | ||
|
|
099e587d45 | ||
| dc0e536221 | |||
| 2a34243e69 | |||
| 499415e0c5 | |||
| 397c5c1550 | |||
| 738f242eee | |||
| 745aaef978 | |||
| b5234c765c | |||
| 919c9f57ac | |||
| 27424694ce | |||
| 84338f8f66 | |||
| 733df2dcb5 | |||
| 9ba3dd7909 | |||
| 2838376434 | |||
| 86ec4de5c0 | |||
| 479e9a2575 | |||
| d97871d15b | |||
| 00fd6880e9 | |||
| 649330f358 | |||
| 07d81d687b | |||
| b291673899 | |||
| 5fbf2ccb45 | |||
| e489d16c51 | |||
| 7cfffadb86 | |||
| ae529effd2 | |||
| 4c3b2152eb | |||
| 51e3c04e72 | |||
| 2b9f038b0d | |||
| 0653700f9c | |||
| 7be80e6f91 | |||
| a4b934388d | |||
| f8c0dbba5a | |||
| ebb531d825 | |||
| fc9779153d | |||
| a2522cef13 | |||
| 442e1d64a3 | |||
| 54b54796e8 | |||
| 686463720a | |||
| 6a77028171 | |||
| f1bd9c18e0 | |||
| 6c9b742bdf | |||
| 744a402602 | |||
| 2d9148788e | |||
| 18f635e6ef | |||
| 9efbc12909 | |||
| 7c7676abee | |||
| 1faa74f026 | |||
| 3afae89234 | |||
| 093c527591 | |||
| 2ba710ca2d | |||
| 9054b163ce | |||
| e182c815a1 | |||
| c284d10943 | |||
| 72e48ada94 | |||
|
|
4591a6857d | ||
|
|
44279bc148 | ||
|
|
32c7d45809 | ||
|
|
4341c2509e | ||
| a3fa499662 | |||
| f25737cdc5 | |||
| 0b500b5248 | |||
| e71cb11295 | |||
| b102ec4c1c | |||
| 974d6e9b56 | |||
| 694cac7f26 | |||
| bd616c510a | |||
| 424a258df1 | |||
| 6dc74ca82e | |||
| 937f1e3ac8 | |||
| 46d1c25bb5 |
14
README.md
14
README.md
@@ -1,7 +1,15 @@
|
||||
# Game Tracker
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
### Versions Supported
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.
|
||||
|
||||
@@ -8,6 +8,19 @@ class CustomTheme {
|
||||
static Color onBoxColor = const Color(0xFF181818);
|
||||
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(
|
||||
backgroundColor: backgroundColor,
|
||||
foregroundColor: Colors.white,
|
||||
|
||||
@@ -22,3 +22,10 @@ enum ImportResult {
|
||||
/// - [ExportResult.canceled]: The export operation was canceled by the user.
|
||||
/// - [ExportResult.unknownException]: An exception occurred during export.
|
||||
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) {
|
||||
await db.playerDao.addPlayers(players: game.players!);
|
||||
await db.playerDao.addPlayersAsList(players: game.players!);
|
||||
for (final p in game.players ?? []) {
|
||||
await db.playerGameDao.addPlayerToGame(
|
||||
gameId: game.id,
|
||||
@@ -89,12 +89,18 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
|
||||
if (game.group != null) {
|
||||
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;
|
||||
await db.transaction(() async {
|
||||
// Add all games in batch
|
||||
@@ -253,4 +259,62 @@ class GameDao extends DatabaseAccessor<AppDatabase> with _$GameDaoMixin {
|
||||
final rowsAffected = await query.go();
|
||||
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.
|
||||
/// 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;
|
||||
await db.transaction(() async {
|
||||
// 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
|
||||
/// [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(
|
||||
GroupGameTableCompanion.insert(groupId: groupId, gameId: gameId),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
@@ -76,4 +82,17 @@ class GroupGameDao extends DatabaseAccessor<AppDatabase>
|
||||
final rowsAffected = await query.go();
|
||||
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.
|
||||
Future<bool> addPlayers({required List<Player> players}) async {
|
||||
Future<bool> addPlayersAsList({required List<Player> players}) async {
|
||||
if (players.isEmpty) return false;
|
||||
|
||||
await db.batch(
|
||||
|
||||
@@ -79,4 +79,50 @@ class PlayerGameDao extends DatabaseAccessor<AppDatabase>
|
||||
final rowsAffected = await query.go();
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,13 @@
|
||||
import 'package:flutter/material.dart' hide ButtonStyle;
|
||||
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/group.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/custom_search_bar.dart';
|
||||
import 'package:game_tracker/presentation/widgets/text_input_field.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/top_centered_message.dart';
|
||||
import 'package:game_tracker/presentation/widgets/player_selection.dart';
|
||||
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class CreateGroupView extends StatefulWidget {
|
||||
const CreateGroupView({super.key});
|
||||
@@ -21,53 +17,25 @@ class CreateGroupView extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
List<Player> selectedPlayers = [];
|
||||
List<Player> suggestedPlayers = [];
|
||||
List<Player> allPlayers = [];
|
||||
late final AppDatabase db;
|
||||
late Future<List<Player>> _allPlayersFuture;
|
||||
late final List<Player> skeletonData = List.filled(
|
||||
7,
|
||||
Player(name: 'Player 0'),
|
||||
);
|
||||
final _groupNameController = TextEditingController();
|
||||
final _searchBarController = TextEditingController();
|
||||
late final AppDatabase db;
|
||||
List<Player> selectedPlayers = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
_searchBarController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
_groupNameController.addListener(() {
|
||||
setState(() {});
|
||||
});
|
||||
loadPlayerList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_groupNameController.dispose();
|
||||
_searchBarController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void loadPlayerList() {
|
||||
_allPlayersFuture = Future.delayed(
|
||||
const Duration(milliseconds: 250),
|
||||
() => db.playerDao.getAllPlayers(),
|
||||
);
|
||||
suggestedPlayers = skeletonData;
|
||||
_allPlayersFuture.then((loadedPlayers) {
|
||||
setState(() {
|
||||
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
allPlayers = [...loadedPlayers];
|
||||
suggestedPlayers = [...loadedPlayers];
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -96,204 +64,11 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomSearchBar(
|
||||
controller: _searchBarController,
|
||||
constraints: const BoxConstraints(
|
||||
maxHeight: 45,
|
||||
minHeight: 45,
|
||||
),
|
||||
hintText: 'Search for players',
|
||||
trailingButtonShown: true,
|
||||
trailingButtonicon: Icons.add_circle,
|
||||
trailingButtonEnabled: _searchBarController.text
|
||||
.trim()
|
||||
.isNotEmpty,
|
||||
onTrailingButtonPressed: () async {
|
||||
addNewPlayerFromSearch(context: context);
|
||||
},
|
||||
child: PlayerSelection(
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
if (value.isEmpty) {
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
return !selectedPlayers.contains(player);
|
||||
}).toList();
|
||||
} else {
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
final bool nameMatches = player.name
|
||||
.toLowerCase()
|
||||
.contains(value.toLowerCase());
|
||||
final bool isNotSelected = !selectedPlayers
|
||||
.contains(player);
|
||||
return nameMatches && isNotSelected;
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
selectedPlayers = [...value];
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Ausgewählte Spieler: (${selectedPlayers.length})',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: <Widget>[
|
||||
for (var player in selectedPlayers)
|
||||
TextIconTile(
|
||||
text: player.name,
|
||||
onIconTap: () {
|
||||
setState(() {
|
||||
final currentSearch = _searchBarController.text
|
||||
.toLowerCase();
|
||||
selectedPlayers.remove(player);
|
||||
if (currentSearch.isEmpty ||
|
||||
player.name.toLowerCase().contains(
|
||||
currentSearch,
|
||||
)) {
|
||||
suggestedPlayers.add(player);
|
||||
suggestedPlayers.sort(
|
||||
(a, b) => a.name.compareTo(b.name),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
'Alle Spieler:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder(
|
||||
future: _allPlayersFuture,
|
||||
builder:
|
||||
(
|
||||
BuildContext context,
|
||||
AsyncSnapshot<List<Player>> snapshot,
|
||||
) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.report,
|
||||
title: 'Error',
|
||||
message: 'Player data couldn\'t\nbe loaded.',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done &&
|
||||
(!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty ||
|
||||
(selectedPlayers.isEmpty &&
|
||||
allPlayers.isEmpty))) {
|
||||
return const Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message: 'No players created yet.',
|
||||
),
|
||||
);
|
||||
}
|
||||
final bool isLoading =
|
||||
snapshot.connectionState ==
|
||||
ConnectionState.waiting;
|
||||
return Expanded(
|
||||
child: 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: Visibility(
|
||||
visible:
|
||||
(suggestedPlayers.isEmpty &&
|
||||
allPlayers.isNotEmpty),
|
||||
replacement: ListView.builder(
|
||||
itemCount: suggestedPlayers.length,
|
||||
itemBuilder:
|
||||
(BuildContext context, int index) {
|
||||
return TextIconListTile(
|
||||
text: suggestedPlayers[index].name,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (!selectedPlayers.contains(
|
||||
suggestedPlayers[index],
|
||||
)) {
|
||||
selectedPlayers.add(
|
||||
suggestedPlayers[index],
|
||||
);
|
||||
selectedPlayers.sort(
|
||||
(a, b) => a.name.compareTo(
|
||||
b.name,
|
||||
),
|
||||
);
|
||||
suggestedPlayers.remove(
|
||||
suggestedPlayers[index],
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message:
|
||||
(selectedPlayers.length ==
|
||||
allPlayers.length)
|
||||
? 'No more players to add.'
|
||||
: 'No players found with that name.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
CustomWidthButton(
|
||||
text: 'Create group',
|
||||
@@ -311,9 +86,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
_groupNameController.clear();
|
||||
_searchBarController.clear();
|
||||
selectedPlayers.clear();
|
||||
Navigator.pop(context);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -337,47 +109,4 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new player to the database from the search bar input.
|
||||
/// Shows a snackbar indicating success or failure.
|
||||
/// [context] - BuildContext to show the snackbar.
|
||||
void addNewPlayerFromSearch({required BuildContext context}) async {
|
||||
String playerName = _searchBarController.text.trim();
|
||||
Player createdPlayer = Player(name: playerName);
|
||||
bool success = await db.playerDao.addPlayer(player: createdPlayer);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
selectedPlayers.add(createdPlayer);
|
||||
allPlayers.add(createdPlayer);
|
||||
setState(() {
|
||||
_searchBarController.clear();
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
return !selectedPlayers.contains(player);
|
||||
}).toList();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Successfully added player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Could not add player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
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/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_group_view.dart';
|
||||
import 'package:game_tracker/presentation/views/main_menu/game_result_view.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/tiles/game_history_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class GameHistoryView extends StatefulWidget {
|
||||
const GameHistoryView({super.key});
|
||||
@@ -19,17 +25,21 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
||||
late final AppDatabase db;
|
||||
|
||||
late final List<Game> skeletonData = List.filled(
|
||||
2,
|
||||
4,
|
||||
Game(
|
||||
name: 'Skeleton Game',
|
||||
group: Group(
|
||||
name: 'Skeleton Group',
|
||||
members: [
|
||||
Player(name: 'Skeleton Player 1'),
|
||||
Player(name: 'Skeleton Player 2'),
|
||||
Player(name: 'Player 1'),
|
||||
Player(name: 'Player 2'),
|
||||
Player(name: 'Player 3'),
|
||||
Player(name: 'Long Name Player 4'),
|
||||
Player(name: 'Player 5'),
|
||||
],
|
||||
),
|
||||
winner: Player(name: 'Skeleton Player 1'),
|
||||
players: [Player(name: 'Skeleton Player 6')],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -45,62 +55,96 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<Game>>(
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
body: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
FutureBuilder<List<Game>>(
|
||||
future: _gameListFuture,
|
||||
builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(
|
||||
heightFactor: 4,
|
||||
child: Text(
|
||||
'Error while loading recent games.',
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.report,
|
||||
title: 'Error',
|
||||
message: 'Game data could not be loaded',
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState == ConnectionState.done &&
|
||||
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
||||
return const Center(
|
||||
heightFactor: 4,
|
||||
child: Text('No recent games available.'),
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.report,
|
||||
title: 'Info',
|
||||
message: 'No games created yet',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final bool isLoading = snapshot.connectionState == ConnectionState.waiting;
|
||||
final List<Game> games = (isLoading
|
||||
? skeletonData
|
||||
: (snapshot.data ?? [])
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
|
||||
.take(2)
|
||||
final bool isLoading =
|
||||
snapshot.connectionState == ConnectionState.waiting;
|
||||
final List<Game> games =
|
||||
(isLoading ? skeletonData : (snapshot.data ?? [])
|
||||
..sort(
|
||||
(a, b) => b.createdAt.compareTo(a.createdAt),
|
||||
))
|
||||
.toList();
|
||||
|
||||
return Skeletonizer(
|
||||
effect: PulseEffect(
|
||||
from: Colors.grey[800]!,
|
||||
to: Colors.grey[600]!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
return AppSkeleton(
|
||||
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: games.length + 1,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
if (index == games.length) {
|
||||
return SizedBox(
|
||||
height: MediaQuery.paddingOf(context).bottom - 20,
|
||||
height: MediaQuery.paddingOf(context).bottom - 80,
|
||||
);
|
||||
}
|
||||
return GameHistoryTile(game: games[index]);
|
||||
return GameHistoryTile(
|
||||
onTap: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) =>
|
||||
GameResultView(game: games[index]),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_gameListFuture = db.gameDao.getAllGames();
|
||||
});
|
||||
},
|
||||
game: games[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: CustomWidthButton(
|
||||
text: 'Create Game',
|
||||
sizeRelativeToWidth: 0.90,
|
||||
onPressed: () async {
|
||||
await Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const CreateGroupView();
|
||||
},
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_gameListFuture = db.gameDao.getAllGames();
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/presentation/views/main_menu/game_result_view.dart
Normal file
144
lib/presentation/views/main_menu/game_result_view.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
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/game.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class GameResultView extends StatefulWidget {
|
||||
final Game game;
|
||||
|
||||
const GameResultView({super.key, required this.game});
|
||||
|
||||
@override
|
||||
State<GameResultView> createState() => _GameResultViewState();
|
||||
}
|
||||
|
||||
class _GameResultViewState extends State<GameResultView> {
|
||||
late final List<Player> allPlayers;
|
||||
late final AppDatabase db;
|
||||
Player? _selectedPlayer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
allPlayers = getAllPlayers(widget.game);
|
||||
if (widget.game.winner != null) {
|
||||
_selectedPlayer = allPlayers.firstWhere(
|
||||
(p) => p.id == widget.game.winner!.id,
|
||||
);
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
appBar: AppBar(
|
||||
backgroundColor: CustomTheme.backgroundColor,
|
||||
scrolledUnderElevation: 0,
|
||||
title: Text(
|
||||
widget.game.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 10,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 10,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Select Winner:',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: RadioGroup<Player>(
|
||||
groupValue: _selectedPlayer,
|
||||
onChanged: (Player? value) async {
|
||||
setState(() {
|
||||
_selectedPlayer = value;
|
||||
});
|
||||
await _handleWinnerSaving();
|
||||
},
|
||||
child: ListView.builder(
|
||||
itemCount: allPlayers.length,
|
||||
itemBuilder: (context, index) {
|
||||
return CustomRadioListTile(
|
||||
text: allPlayers[index].name,
|
||||
value: allPlayers[index],
|
||||
onContainerTap: (value) async {
|
||||
setState(() {
|
||||
// Check if the already selected player is the same as the newly tapped player.
|
||||
if (_selectedPlayer == value) {
|
||||
// If yes deselected the player by setting it to null.
|
||||
_selectedPlayer = null;
|
||||
} else {
|
||||
// If no assign the newly tapped player to the selected player.
|
||||
(_selectedPlayer = value);
|
||||
}
|
||||
});
|
||||
await _handleWinnerSaving();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleWinnerSaving() async {
|
||||
if (_selectedPlayer == null) {
|
||||
await db.gameDao.removeWinner(gameId: widget.game.id);
|
||||
} else {
|
||||
await db.gameDao.setWinner(
|
||||
gameId: widget.game.id,
|
||||
winnerId: _selectedPlayer!.id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Player> getAllPlayers(Game game) {
|
||||
if (game.group == null && game.players != null) {
|
||||
return [...game.players!];
|
||||
} else if (game.group != null && game.players != null) {
|
||||
return [...game.players!, ...game.group!.members];
|
||||
}
|
||||
return [...game.group!.members];
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,11 @@ 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/create_group_view.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/tiles/group_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class GroupsView extends StatefulWidget {
|
||||
const GroupsView({super.key});
|
||||
@@ -56,7 +56,7 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.report,
|
||||
title: 'Error',
|
||||
message: 'Group data couldn\'t\nbe loaded.',
|
||||
message: 'Group data couldn\'t\nbe loaded',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -66,7 +66,7 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message: 'No groups created yet.',
|
||||
message: 'No groups created yet',
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -75,22 +75,8 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
final List<Group> groups =
|
||||
isLoading ? skeletonData : (snapshot.data ?? [])
|
||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||
return Skeletonizer(
|
||||
effect: PulseEffect(
|
||||
from: Colors.grey[800]!,
|
||||
to: Colors.grey[600]!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
return AppSkeleton(
|
||||
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,
|
||||
@@ -106,7 +92,6 @@ class _GroupsViewState extends State<GroupsView> {
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
Positioned(
|
||||
bottom: MediaQuery.paddingOf(context).bottom,
|
||||
child: CustomWidthButton(
|
||||
|
||||
@@ -3,12 +3,12 @@ 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/widgets/app_skeleton.dart';
|
||||
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class HomeView extends StatefulWidget {
|
||||
const HomeView({super.key});
|
||||
@@ -62,30 +62,8 @@ class _HomeViewState extends State<HomeView> {
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return Skeletonizer(
|
||||
effect: PulseEffect(
|
||||
from: Colors.grey[800]!,
|
||||
to: Colors.grey[600]!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
return AppSkeleton(
|
||||
enabled: isLoading,
|
||||
enableSwitchAnimation: true,
|
||||
switchAnimationConfig: SwitchAnimationConfig(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.linear,
|
||||
switchOutCurve: Curves.linear,
|
||||
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
|
||||
layoutBuilder:
|
||||
(Widget? currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -151,15 +129,6 @@ class _HomeViewState extends State<HomeView> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done &&
|
||||
(!snapshot.hasData ||
|
||||
snapshot.data!.isEmpty)) {
|
||||
return const Center(
|
||||
heightFactor: 4,
|
||||
child: Text('No recent games available.'),
|
||||
);
|
||||
}
|
||||
final List<Game> games =
|
||||
(isLoading
|
||||
? skeletonData
|
||||
@@ -214,7 +183,7 @@ class _HomeViewState extends State<HomeView> {
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
heightFactor: 4,
|
||||
heightFactor: 12,
|
||||
child: Text('No recent games available.'),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/data/db/database.dart';
|
||||
import 'package:game_tracker/data/dto/game.dart';
|
||||
import 'package:game_tracker/data/dto/player.dart';
|
||||
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class StatisticsView extends StatefulWidget {
|
||||
const StatisticsView({super.key});
|
||||
@@ -48,30 +48,9 @@ class _StatisticsViewState extends State<StatisticsView> {
|
||||
return LayoutBuilder(
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
return SingleChildScrollView(
|
||||
child: Skeletonizer(
|
||||
effect: PulseEffect(
|
||||
from: Colors.grey[800]!,
|
||||
to: Colors.grey[600]!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
child: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
enableSwitchAnimation: true,
|
||||
switchAnimationConfig: SwitchAnimationConfig(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.linear,
|
||||
switchOutCurve: Curves.linear,
|
||||
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
|
||||
layoutBuilder:
|
||||
(Widget? currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
fixLayoutBuilder: true,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||
child: Column(
|
||||
|
||||
51
lib/presentation/widgets/app_skeleton.dart
Normal file
51
lib/presentation/widgets/app_skeleton.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class AppSkeleton extends StatefulWidget {
|
||||
final Widget child;
|
||||
final bool enabled;
|
||||
final bool fixLayoutBuilder;
|
||||
|
||||
const AppSkeleton({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
this.fixLayoutBuilder = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AppSkeleton> createState() => _AppSkeletonState();
|
||||
}
|
||||
|
||||
class _AppSkeletonState extends State<AppSkeleton> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Skeletonizer(
|
||||
effect: PulseEffect(
|
||||
from: Colors.grey[800]!,
|
||||
to: Colors.grey[600]!,
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
enabled: widget.enabled,
|
||||
enableSwitchAnimation: true,
|
||||
switchAnimationConfig: SwitchAnimationConfig(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
switchInCurve: Curves.linear,
|
||||
switchOutCurve: Curves.linear,
|
||||
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
|
||||
layoutBuilder: !widget.fixLayoutBuilder
|
||||
? AnimatedSwitcher.defaultLayoutBuilder
|
||||
: (Widget? currentChild, List<Widget> previousChildren) {
|
||||
return Stack(
|
||||
alignment: Alignment.topCenter,
|
||||
children: [
|
||||
...previousChildren,
|
||||
if (currentChild != null) currentChild,
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
267
lib/presentation/widgets/player_selection.dart
Normal file
267
lib/presentation/widgets/player_selection.dart
Normal file
@@ -0,0 +1,267 @@
|
||||
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/player.dart';
|
||||
import 'package:game_tracker/presentation/widgets/app_skeleton.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_tile.dart';
|
||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PlayerSelection extends StatefulWidget {
|
||||
final Function(List<Player> value) onChanged;
|
||||
final List<Player> initialPlayers;
|
||||
|
||||
const PlayerSelection({
|
||||
super.key,
|
||||
required this.onChanged,
|
||||
this.initialPlayers = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
State<PlayerSelection> createState() => _PlayerSelectionState();
|
||||
}
|
||||
|
||||
class _PlayerSelectionState extends State<PlayerSelection> {
|
||||
List<Player> selectedPlayers = [];
|
||||
List<Player> suggestedPlayers = [];
|
||||
List<Player> allPlayers = [];
|
||||
late final TextEditingController _searchBarController =
|
||||
TextEditingController();
|
||||
late final AppDatabase db;
|
||||
late Future<List<Player>> _allPlayersFuture;
|
||||
late final List<Player> skeletonData = List.filled(
|
||||
7,
|
||||
Player(name: 'Player 0'),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
db = Provider.of<AppDatabase>(context, listen: false);
|
||||
loadPlayerList();
|
||||
}
|
||||
|
||||
void loadPlayerList() {
|
||||
_allPlayersFuture = Future.delayed(
|
||||
const Duration(milliseconds: 250),
|
||||
() => db.playerDao.getAllPlayers(),
|
||||
);
|
||||
suggestedPlayers = skeletonData;
|
||||
_allPlayersFuture.then((loadedPlayers) {
|
||||
setState(() {
|
||||
if (widget.initialPlayers.isNotEmpty) {
|
||||
allPlayers = [...widget.initialPlayers];
|
||||
suggestedPlayers = [...widget.initialPlayers];
|
||||
} else {
|
||||
loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
|
||||
allPlayers = [...loadedPlayers];
|
||||
suggestedPlayers = [...loadedPlayers];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
CustomSearchBar(
|
||||
controller: _searchBarController,
|
||||
constraints: const BoxConstraints(maxHeight: 45, minHeight: 45),
|
||||
hintText: 'Search for players',
|
||||
trailingButtonShown: true,
|
||||
trailingButtonicon: Icons.add_circle,
|
||||
trailingButtonEnabled: _searchBarController.text.trim().isNotEmpty,
|
||||
onTrailingButtonPressed: () async {
|
||||
addNewPlayerFromSearch(context: context);
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
// Filters the list of suggested players based on the search input.
|
||||
if (value.isEmpty) {
|
||||
// If the search is empty, it shows all unselected players.
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
return !selectedPlayers.contains(player);
|
||||
}).toList();
|
||||
} else {
|
||||
// If there is input, it filters by name match (case-insensitive) and ensures
|
||||
// that already selected players are excluded from the results.
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
final bool nameMatches = player.name.toLowerCase().contains(
|
||||
value.toLowerCase(),
|
||||
);
|
||||
final bool isNotSelected = !selectedPlayers.contains(
|
||||
player,
|
||||
);
|
||||
return nameMatches && isNotSelected;
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Selected players: (${selectedPlayers.length})',
|
||||
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
crossAxisAlignment: WrapCrossAlignment.start,
|
||||
spacing: 8.0,
|
||||
runSpacing: 8.0,
|
||||
children: <Widget>[
|
||||
// Generates a TextIconTile for each selected player.
|
||||
for (var player in selectedPlayers)
|
||||
TextIconTile(
|
||||
text: player.name,
|
||||
onIconTap: () {
|
||||
setState(() {
|
||||
// Removes the player from the selection and notifies the parent.
|
||||
final currentSearch = _searchBarController.text
|
||||
.toLowerCase();
|
||||
selectedPlayers.remove(player);
|
||||
widget.onChanged([...selectedPlayers]);
|
||||
// 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 Text(
|
||||
'All players:',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder(
|
||||
future: _allPlayersFuture,
|
||||
builder:
|
||||
(BuildContext context, AsyncSnapshot<List<Player>> snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.report,
|
||||
title: 'Error',
|
||||
message: 'Player data couldn\'t\nbe loaded.',
|
||||
),
|
||||
);
|
||||
}
|
||||
bool doneLoading =
|
||||
snapshot.connectionState == ConnectionState.done;
|
||||
bool snapshotDataEmpty =
|
||||
!snapshot.hasData || snapshot.data!.isEmpty;
|
||||
if (doneLoading &&
|
||||
(snapshotDataEmpty && allPlayers.isEmpty)) {
|
||||
return const Center(
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message: 'No players created yet.',
|
||||
),
|
||||
);
|
||||
}
|
||||
final bool isLoading =
|
||||
snapshot.connectionState == ConnectionState.waiting;
|
||||
return Expanded(
|
||||
child: AppSkeleton(
|
||||
enabled: isLoading,
|
||||
child: Visibility(
|
||||
visible:
|
||||
(suggestedPlayers.isEmpty && allPlayers.isNotEmpty),
|
||||
replacement: ListView.builder(
|
||||
itemCount: suggestedPlayers.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return TextIconListTile(
|
||||
text: suggestedPlayers[index].name,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
if (!selectedPlayers.contains(
|
||||
suggestedPlayers[index],
|
||||
)) {
|
||||
selectedPlayers.add(
|
||||
suggestedPlayers[index],
|
||||
);
|
||||
widget.onChanged([...selectedPlayers]);
|
||||
suggestedPlayers.remove(
|
||||
suggestedPlayers[index],
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
child: TopCenteredMessage(
|
||||
icon: Icons.info,
|
||||
title: 'Info',
|
||||
message: (selectedPlayers.length == allPlayers.length)
|
||||
? 'No more players to add.'
|
||||
: 'No players found with that name.',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Adds a new player to the database from the search bar input.
|
||||
/// Shows a snackbar indicating success or failure.
|
||||
/// [context] - BuildContext to show the snackbar.
|
||||
void addNewPlayerFromSearch({required BuildContext context}) async {
|
||||
String playerName = _searchBarController.text.trim();
|
||||
Player createdPlayer = Player(name: playerName);
|
||||
bool success = await db.playerDao.addPlayer(player: createdPlayer);
|
||||
if (!context.mounted) return;
|
||||
if (success) {
|
||||
selectedPlayers.add(createdPlayer);
|
||||
widget.onChanged([...selectedPlayers]);
|
||||
allPlayers.add(createdPlayer);
|
||||
setState(() {
|
||||
_searchBarController.clear();
|
||||
suggestedPlayers = allPlayers.where((player) {
|
||||
return !selectedPlayers.contains(player);
|
||||
}).toList();
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Successfully added player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
backgroundColor: CustomTheme.boxColor,
|
||||
content: Center(
|
||||
child: Text(
|
||||
'Could not add player $playerName.',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
50
lib/presentation/widgets/tiles/custom_radio_list_tile.dart
Normal file
50
lib/presentation/widgets/tiles/custom_radio_list_tile.dart
Normal file
@@ -0,0 +1,50 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
|
||||
class CustomRadioListTile<T> extends StatelessWidget {
|
||||
final String text;
|
||||
final T value;
|
||||
final ValueChanged<T> onContainerTap;
|
||||
|
||||
const CustomRadioListTile({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.value,
|
||||
required this.onContainerTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => onContainerTap(value),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Radio<T>(
|
||||
value: value,
|
||||
activeColor: CustomTheme.primaryColor,
|
||||
toggleable: true,
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
|
||||
Widget doubleRowInfoTile(
|
||||
String titleOneUpperLeft,
|
||||
String titleTwoUpperLeft,
|
||||
String titleUpperRight,
|
||||
String titleLowerLeft,
|
||||
String titleLowerRight,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
color: CustomTheme.secondaryColor,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 10,
|
||||
child: Text(
|
||||
'$titleOneUpperLeft $titleTwoUpperLeft',
|
||||
style: const TextStyle(fontSize: 20),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Text(
|
||||
titleUpperRight,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 10,
|
||||
child: Text(
|
||||
titleLowerLeft,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Text(
|
||||
titleLowerRight,
|
||||
style: const TextStyle(fontSize: 20),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.end,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,77 +1,180 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:game_tracker/core/custom_theme.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:game_tracker/data/dto/game.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class GameHistoryTile extends StatefulWidget {
|
||||
final String gameTitle;
|
||||
final String gameType;
|
||||
final String date;
|
||||
final String groupName;
|
||||
final String winner;
|
||||
final Game game;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const GameHistoryTile({
|
||||
super.key,
|
||||
required this.gameTitle,
|
||||
required this.gameType,
|
||||
required this.date,
|
||||
required this.groupName,
|
||||
required this.winner,
|
||||
});
|
||||
const GameHistoryTile({super.key, required this.game, required this.onTap});
|
||||
|
||||
@override
|
||||
State<GameHistoryTile> createState() => _GameHistoryTileState();
|
||||
}
|
||||
|
||||
class _GameHistoryTileState extends State<GameHistoryTile> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
final group = widget.game.group;
|
||||
final winner = widget.game.winner;
|
||||
final allPlayers = _getAllPlayers();
|
||||
|
||||
return GestureDetector(
|
||||
onTap: widget.onTap,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
padding: const EdgeInsets.all(16),
|
||||
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(
|
||||
widget.gameTitle,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.game.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatDate(widget.game.createdAt),
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (group != null) ...[
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
widget.date,
|
||||
const Icon(Icons.group, size: 16, color: Colors.grey),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
group.name,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
textAlign: TextAlign.left,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 5),
|
||||
const Text('·'),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
widget.gameType,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
textAlign: TextAlign.left,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
]
|
||||
)
|
||||
if (winner != null) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 12,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Colors.green.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.emoji_events,
|
||||
size: 20,
|
||||
color: Colors.amber,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Winner: ${winner.name}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
if (allPlayers.isNotEmpty) ...[
|
||||
const Text(
|
||||
'Players',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: allPlayers.map((player) {
|
||||
return TextIconTile(text: player.name, iconEnabled: false);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDate(DateTime dateTime) {
|
||||
final now = DateTime.now();
|
||||
final difference = now.difference(dateTime);
|
||||
|
||||
if (difference.inDays == 0) {
|
||||
return 'Today at ${DateFormat('HH:mm').format(dateTime)}';
|
||||
} else if (difference.inDays == 1) {
|
||||
return 'Yesterday at ${DateFormat('HH:mm').format(dateTime)}';
|
||||
} else if (difference.inDays < 7) {
|
||||
return '${difference.inDays} days ago';
|
||||
} else {
|
||||
return DateFormat('MMM d, yyyy').format(dateTime);
|
||||
}
|
||||
}
|
||||
|
||||
List<dynamic> _getAllPlayers() {
|
||||
final allPlayers = <dynamic>[];
|
||||
final playerIds = <String>{};
|
||||
|
||||
// Add players from game.players
|
||||
if (widget.game.players != null) {
|
||||
for (var player in widget.game.players!) {
|
||||
if (!playerIds.contains(player.id)) {
|
||||
allPlayers.add(player);
|
||||
playerIds.add(player.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add players from game.group.players
|
||||
if (widget.game.group?.members != null) {
|
||||
for (var player in widget.game.group!.members) {
|
||||
if (!playerIds.contains(player.id)) {
|
||||
allPlayers.add(player);
|
||||
playerIds.add(player.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allPlayers;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,20 @@ import 'package:game_tracker/data/dto/group.dart';
|
||||
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
|
||||
|
||||
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 bool isHighlighted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return AnimatedContainer(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
decoration: isHighlighted
|
||||
? CustomTheme.highlightedBoxDecoration
|
||||
: CustomTheme.standardBoxDecoration,
|
||||
duration: const Duration(milliseconds: 150),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
||||
@@ -29,11 +29,7 @@ class _InfoTileState extends State<InfoTile> {
|
||||
padding: widget.padding ?? const EdgeInsets.all(12),
|
||||
height: widget.height,
|
||||
width: widget.width ?? 380,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
|
||||
@@ -29,11 +29,7 @@ class _QuickInfoTileState extends State<QuickInfoTile> {
|
||||
padding: widget.padding ?? const EdgeInsets.all(12),
|
||||
height: widget.height ?? 110,
|
||||
width: widget.width ?? 180,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
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(
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
||||
@@ -18,11 +18,7 @@ class TextIconListTile extends StatelessWidget {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
decoration: BoxDecoration(
|
||||
color: CustomTheme.boxColor,
|
||||
border: Border.all(color: CustomTheme.boxBorder),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
decoration: CustomTheme.standardBoxDecoration,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
||||
@@ -110,9 +110,9 @@ class DataTransferService {
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
await db.playerDao.addPlayers(players: importedPlayers);
|
||||
await db.groupDao.addGroups(groups: importedGroups);
|
||||
await db.gameDao.addGames(games: importedGames);
|
||||
await db.playerDao.addPlayersAsList(players: importedPlayers);
|
||||
await db.groupDao.addGroupsAsList(groups: importedGroups);
|
||||
await db.gameDao.addGamesAsList(games: importedGames);
|
||||
} else {
|
||||
return ImportResult.invalidSchema;
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ void main() {
|
||||
});
|
||||
|
||||
test('Adding and fetching multiple games works correctly', () async {
|
||||
await database.gameDao.addGames(
|
||||
await database.gameDao.addGamesAsList(
|
||||
games: [testGame1, testGame2, testGameOnlyGroup, testGameOnlyPlayers],
|
||||
);
|
||||
|
||||
@@ -234,5 +234,97 @@ void main() {
|
||||
gameCount = await database.gameDao.getGameCount();
|
||||
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 testPlayer4;
|
||||
late Player testPlayer5;
|
||||
late Group testgroup;
|
||||
late Group testGroup1;
|
||||
late Group testGroup2;
|
||||
late Game testgameWithGroup;
|
||||
late Game testgameWithPlayers;
|
||||
final fixedDate = DateTime(2025, 19, 11, 00, 11, 23);
|
||||
@@ -35,15 +36,19 @@ void main() {
|
||||
testPlayer3 = Player(name: 'Charlie');
|
||||
testPlayer4 = Player(name: 'Diana');
|
||||
testPlayer5 = Player(name: 'Eve');
|
||||
testgroup = Group(
|
||||
testGroup1 = Group(
|
||||
name: 'Test Group',
|
||||
members: [testPlayer1, testPlayer2, testPlayer3],
|
||||
);
|
||||
testGroup2 = Group(
|
||||
name: 'Test Group',
|
||||
members: [testPlayer3, testPlayer2],
|
||||
);
|
||||
testgameWithPlayers = Game(
|
||||
name: 'Test Game with Players',
|
||||
players: [testPlayer4, testPlayer5],
|
||||
);
|
||||
testgameWithGroup = Game(name: 'Test Game with Group', group: testgroup);
|
||||
testgameWithGroup = Game(name: 'Test Game with Group', group: testGroup1);
|
||||
});
|
||||
});
|
||||
tearDown(() async {
|
||||
@@ -52,7 +57,7 @@ void main() {
|
||||
group('Group-Game Tests', () {
|
||||
test('Game has group works correctly', () async {
|
||||
await database.gameDao.addGame(game: testgameWithPlayers);
|
||||
await database.groupDao.addGroup(group: testgroup);
|
||||
await database.groupDao.addGroup(group: testGroup1);
|
||||
|
||||
var gameHasGroup = await database.groupGameDao.gameHasGroup(
|
||||
gameId: testgameWithPlayers.id,
|
||||
@@ -61,8 +66,8 @@ void main() {
|
||||
expect(gameHasGroup, false);
|
||||
|
||||
await database.groupGameDao.addGroupToGame(
|
||||
testgameWithPlayers.id,
|
||||
testgroup.id,
|
||||
gameId: testgameWithPlayers.id,
|
||||
groupId: testGroup1.id,
|
||||
);
|
||||
|
||||
gameHasGroup = await database.groupGameDao.gameHasGroup(
|
||||
@@ -74,15 +79,15 @@ void main() {
|
||||
|
||||
test('Adding a group to a game works correctly', () async {
|
||||
await database.gameDao.addGame(game: testgameWithPlayers);
|
||||
await database.groupDao.addGroup(group: testgroup);
|
||||
await database.groupDao.addGroup(group: testGroup1);
|
||||
await database.groupGameDao.addGroupToGame(
|
||||
testgameWithPlayers.id,
|
||||
testgroup.id,
|
||||
gameId: testgameWithPlayers.id,
|
||||
groupId: testGroup1.id,
|
||||
);
|
||||
|
||||
var groupAdded = await database.groupGameDao.isGroupInGame(
|
||||
gameId: testgameWithPlayers.id,
|
||||
groupId: testgroup.id,
|
||||
groupId: testGroup1.id,
|
||||
);
|
||||
expect(groupAdded, true);
|
||||
|
||||
@@ -120,14 +125,55 @@ void main() {
|
||||
fail('Group should not be null');
|
||||
}
|
||||
|
||||
expect(group.id, testgroup.id);
|
||||
expect(group.name, testgroup.name);
|
||||
expect(group.createdAt, testgroup.createdAt);
|
||||
expect(group.members.length, testgroup.members.length);
|
||||
expect(group.id, testGroup1.id);
|
||||
expect(group.name, testGroup1.name);
|
||||
expect(group.createdAt, testGroup1.createdAt);
|
||||
expect(group.members.length, testGroup1.members.length);
|
||||
for (int i = 0; i < group.members.length; i++) {
|
||||
expect(group.members[i].id, testgroup.members[i].id);
|
||||
expect(group.members[i].name, testgroup.members[i].name);
|
||||
expect(group.members[i].createdAt, testgroup.members[i].createdAt);
|
||||
expect(group.members[i].id, testGroup1.members[i].id);
|
||||
expect(group.members[i].name, testGroup1.members[i].name);
|
||||
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 {
|
||||
await database.groupDao.addGroups(
|
||||
await database.groupDao.addGroupsAsList(
|
||||
groups: [testGroup1, testGroup2, testGroup3, testGroup4],
|
||||
);
|
||||
|
||||
|
||||
@@ -136,5 +136,48 @@ void main() {
|
||||
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 {
|
||||
await database.playerDao.addPlayers(
|
||||
await database.playerDao.addPlayersAsList(
|
||||
players: [testPlayer1, testPlayer2, testPlayer3, testPlayer4],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user