Compare commits
96 Commits
a29123c964
...
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 | ||
| 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 | |||
| e71cb11295 | |||
| b102ec4c1c | |||
| bd616c510a | |||
| 424a258df1 | |||
| 6dc74ca82e | |||
| 937f1e3ac8 | |||
| 46d1c25bb5 |
14
README.md
14
README.md
@@ -1,7 +1,15 @@
|
|||||||
# Game Tracker
|
# 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.
|
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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/custom_theme.dart';
|
||||||
import 'package:game_tracker/core/enums.dart';
|
import 'package:game_tracker/core/enums.dart';
|
||||||
import 'package:game_tracker/data/db/database.dart';
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
import 'package:game_tracker/data/dto/group.dart';
|
import 'package:game_tracker/data/dto/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/custom_search_bar.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: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';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
class CreateGroupView extends StatefulWidget {
|
class CreateGroupView extends StatefulWidget {
|
||||||
const CreateGroupView({super.key});
|
const CreateGroupView({super.key});
|
||||||
@@ -21,53 +17,25 @@ class CreateGroupView extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _CreateGroupViewState extends State<CreateGroupView> {
|
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 _groupNameController = TextEditingController();
|
||||||
final _searchBarController = TextEditingController();
|
late final AppDatabase db;
|
||||||
|
List<Player> selectedPlayers = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
db = Provider.of<AppDatabase>(context, listen: false);
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
_searchBarController.addListener(() {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
_groupNameController.addListener(() {
|
_groupNameController.addListener(() {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
});
|
});
|
||||||
loadPlayerList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_groupNameController.dispose();
|
_groupNameController.dispose();
|
||||||
_searchBarController.dispose();
|
|
||||||
super.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -96,204 +64,10 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: PlayerSelection(
|
||||||
margin: const EdgeInsets.symmetric(
|
onChanged: (value) {
|
||||||
horizontal: 12,
|
selectedPlayers = [...value];
|
||||||
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);
|
|
||||||
},
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
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.',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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: 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(
|
CustomWidthButton(
|
||||||
@@ -312,9 +86,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
|
|||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
if (success) {
|
if (success) {
|
||||||
_groupNameController.clear();
|
|
||||||
_searchBarController.clear();
|
|
||||||
selectedPlayers.clear();
|
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -338,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:flutter/material.dart';
|
||||||
|
import 'package:game_tracker/core/custom_theme.dart';
|
||||||
import 'package:game_tracker/data/db/database.dart';
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
import 'package:game_tracker/data/dto/game.dart';
|
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/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/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';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
class GameHistoryView extends StatefulWidget {
|
class GameHistoryView extends StatefulWidget {
|
||||||
const GameHistoryView({super.key});
|
const GameHistoryView({super.key});
|
||||||
@@ -17,7 +23,6 @@ class GameHistoryView extends StatefulWidget {
|
|||||||
class _GameHistoryViewState extends State<GameHistoryView> {
|
class _GameHistoryViewState extends State<GameHistoryView> {
|
||||||
late Future<List<Game>> _gameListFuture;
|
late Future<List<Game>> _gameListFuture;
|
||||||
late final AppDatabase db;
|
late final AppDatabase db;
|
||||||
late bool isLoading = true;
|
|
||||||
|
|
||||||
late final List<Game> skeletonData = List.filled(
|
late final List<Game> skeletonData = List.filled(
|
||||||
4,
|
4,
|
||||||
@@ -34,9 +39,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')
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -44,72 +47,104 @@ class _GameHistoryViewState extends State<GameHistoryView> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
db = Provider.of<AppDatabase>(context, listen: false);
|
db = Provider.of<AppDatabase>(context, listen: false);
|
||||||
_gameListFuture = db.gameDao.getAllGames();
|
_gameListFuture = Future.delayed(
|
||||||
|
const Duration(milliseconds: 250),
|
||||||
Future.wait([_gameListFuture]).then((result) async {
|
() => db.gameDao.getAllGames(),
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
);
|
||||||
setState(() {
|
|
||||||
isLoading = false;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder<List<Game>>(
|
return Scaffold(
|
||||||
future: _gameListFuture,
|
backgroundColor: CustomTheme.backgroundColor,
|
||||||
builder: (BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
body: Stack(
|
||||||
if (snapshot.hasError) {
|
alignment: Alignment.center,
|
||||||
return const Center(
|
children: [
|
||||||
heightFactor: 4,
|
FutureBuilder<List<Game>>(
|
||||||
child: Text(
|
future: _gameListFuture,
|
||||||
'Error while loading games.',
|
builder:
|
||||||
),
|
(BuildContext context, AsyncSnapshot<List<Game>> snapshot) {
|
||||||
);
|
if (snapshot.hasError) {
|
||||||
}
|
return const Center(
|
||||||
if (snapshot.connectionState == ConnectionState.done &&
|
child: TopCenteredMessage(
|
||||||
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
icon: Icons.report,
|
||||||
return const Center(
|
title: 'Error',
|
||||||
heightFactor: 4,
|
message: 'Game data could not be loaded',
|
||||||
child: Text('No games available.'),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (snapshot.connectionState == ConnectionState.done &&
|
||||||
final List<Game> games = (isLoading
|
(!snapshot.hasData || snapshot.data!.isEmpty)) {
|
||||||
? skeletonData
|
return const Center(
|
||||||
: (snapshot.data ?? [])
|
child: TopCenteredMessage(
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
|
icon: Icons.report,
|
||||||
.toList();
|
title: 'Info',
|
||||||
|
message: 'No games created yet',
|
||||||
return Skeletonizer(
|
),
|
||||||
effect: PulseEffect(
|
);
|
||||||
from: Colors.grey[800]!,
|
}
|
||||||
to: Colors.grey[600]!,
|
final bool isLoading =
|
||||||
duration: const Duration(milliseconds: 800),
|
snapshot.connectionState == ConnectionState.waiting;
|
||||||
|
final List<Game> games =
|
||||||
|
(isLoading ? skeletonData : (snapshot.data ?? [])
|
||||||
|
..sort(
|
||||||
|
(a, b) => b.createdAt.compareTo(a.createdAt),
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return AppSkeleton(
|
||||||
|
enabled: isLoading,
|
||||||
|
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 - 80,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
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],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
enabled: isLoading,
|
Positioned(
|
||||||
enableSwitchAnimation: true,
|
bottom: MediaQuery.paddingOf(context).bottom,
|
||||||
switchAnimationConfig: const SwitchAnimationConfig(
|
child: CustomWidthButton(
|
||||||
duration: Duration(milliseconds: 200),
|
text: 'Create Game',
|
||||||
switchInCurve: Curves.linear,
|
sizeRelativeToWidth: 0.90,
|
||||||
switchOutCurve: Curves.linear,
|
onPressed: () async {
|
||||||
transitionBuilder: AnimatedSwitcher.defaultTransitionBuilder,
|
await Navigator.push(
|
||||||
layoutBuilder: AnimatedSwitcher.defaultLayoutBuilder,
|
context,
|
||||||
),
|
MaterialPageRoute(
|
||||||
child: ListView.builder(
|
builder: (context) {
|
||||||
padding: const EdgeInsets.only(bottom: 85),
|
return const CreateGroupView();
|
||||||
itemCount: games.length + 1,
|
},
|
||||||
itemBuilder: (BuildContext context, int index) {
|
),
|
||||||
if (index == games.length) {
|
|
||||||
return SizedBox(
|
|
||||||
height: MediaQuery.paddingOf(context).bottom - 20,
|
|
||||||
);
|
);
|
||||||
}
|
setState(() {
|
||||||
return GameHistoryTile(game: games[index]);
|
_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/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/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/group_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
class GroupsView extends StatefulWidget {
|
class GroupsView extends StatefulWidget {
|
||||||
const GroupsView({super.key});
|
const GroupsView({super.key});
|
||||||
@@ -56,7 +56,7 @@ class _GroupsViewState extends State<GroupsView> {
|
|||||||
child: TopCenteredMessage(
|
child: TopCenteredMessage(
|
||||||
icon: Icons.report,
|
icon: Icons.report,
|
||||||
title: 'Error',
|
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(
|
child: TopCenteredMessage(
|
||||||
icon: Icons.info,
|
icon: Icons.info,
|
||||||
title: '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 =
|
final List<Group> groups =
|
||||||
isLoading ? skeletonData : (snapshot.data ?? [])
|
isLoading ? skeletonData : (snapshot.data ?? [])
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
return Skeletonizer(
|
return AppSkeleton(
|
||||||
effect: PulseEffect(
|
|
||||||
from: Colors.grey[800]!,
|
|
||||||
to: Colors.grey[600]!,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
),
|
|
||||||
enabled: isLoading,
|
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(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.only(bottom: 85),
|
padding: const EdgeInsets.only(bottom: 85),
|
||||||
itemCount: groups.length + 1,
|
itemCount: groups.length + 1,
|
||||||
@@ -106,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,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/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/widgets/app_skeleton.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.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/game_tile.dart';
|
||||||
import 'package:game_tracker/presentation/widgets/tiles/info_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:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
class HomeView extends StatefulWidget {
|
class HomeView extends StatefulWidget {
|
||||||
const HomeView({super.key});
|
const HomeView({super.key});
|
||||||
@@ -62,30 +62,8 @@ class _HomeViewState extends State<HomeView> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
return Skeletonizer(
|
return AppSkeleton(
|
||||||
effect: PulseEffect(
|
|
||||||
from: Colors.grey[800]!,
|
|
||||||
to: Colors.grey[600]!,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
),
|
|
||||||
enabled: isLoading,
|
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: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:game_tracker/data/db/database.dart';
|
import 'package:game_tracker/data/db/database.dart';
|
||||||
import 'package:game_tracker/data/dto/game.dart';
|
import 'package:game_tracker/data/dto/game.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/tiles/statistics_tile.dart';
|
import 'package:game_tracker/presentation/widgets/tiles/statistics_tile.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
|
|
||||||
class StatisticsView extends StatefulWidget {
|
class StatisticsView extends StatefulWidget {
|
||||||
const StatisticsView({super.key});
|
const StatisticsView({super.key});
|
||||||
@@ -48,30 +48,9 @@ class _StatisticsViewState extends State<StatisticsView> {
|
|||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (BuildContext context, BoxConstraints constraints) {
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
child: Skeletonizer(
|
child: AppSkeleton(
|
||||||
effect: PulseEffect(
|
|
||||||
from: Colors.grey[800]!,
|
|
||||||
to: Colors.grey[600]!,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
),
|
|
||||||
enabled: isLoading,
|
enabled: isLoading,
|
||||||
enableSwitchAnimation: true,
|
fixLayoutBuilder: 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: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
constraints: BoxConstraints(minWidth: constraints.maxWidth),
|
||||||
child: Column(
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -6,142 +6,132 @@ import 'package:intl/intl.dart';
|
|||||||
|
|
||||||
class GameHistoryTile extends StatefulWidget {
|
class GameHistoryTile extends StatefulWidget {
|
||||||
final Game game;
|
final Game game;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const GameHistoryTile({
|
const GameHistoryTile({super.key, required this.game, required this.onTap});
|
||||||
super.key,
|
|
||||||
required this.game,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<GameHistoryTile> createState() => _GameHistoryTileState();
|
State<GameHistoryTile> createState() => _GameHistoryTileState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GameHistoryTileState extends State<GameHistoryTile> {
|
class _GameHistoryTileState extends State<GameHistoryTile> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final group = widget.game.group;
|
final group = widget.game.group;
|
||||||
final winner = widget.game.winner;
|
final winner = widget.game.winner;
|
||||||
final allPlayers = _getAllPlayers();
|
final allPlayers = _getAllPlayers();
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
onTap: widget.onTap,
|
||||||
padding: const EdgeInsets.all(16),
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
color: CustomTheme.boxColor,
|
padding: const EdgeInsets.all(16),
|
||||||
border: Border.all(color: CustomTheme.boxBorder),
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(12),
|
color: CustomTheme.boxColor,
|
||||||
),
|
border: Border.all(color: CustomTheme.boxBorder),
|
||||||
child: Column(
|
borderRadius: BorderRadius.circular(12),
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
child: Column(
|
||||||
Row(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
|
||||||
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(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
|
||||||
Icons.group,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
group.name,
|
widget.game.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 18,
|
||||||
color: Colors.grey,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
_formatDate(widget.game.createdAt),
|
||||||
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (winner != null) ...[
|
const SizedBox(height: 8),
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
|
if (group != null) ...[
|
||||||
decoration: BoxDecoration(
|
Row(
|
||||||
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: [
|
children: [
|
||||||
const Icon(
|
const Icon(Icons.group, size: 16, color: Colors.grey),
|
||||||
Icons.emoji_events,
|
const SizedBox(width: 6),
|
||||||
size: 20,
|
|
||||||
color: Colors.amber,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Winner: ${winner.name}',
|
group.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
const SizedBox(height: 12),
|
],
|
||||||
],
|
|
||||||
|
|
||||||
if (allPlayers.isNotEmpty) ...[
|
if (winner != null) ...[
|
||||||
const Text(
|
Container(
|
||||||
'Players:',
|
padding: const EdgeInsets.symmetric(
|
||||||
style: TextStyle(
|
vertical: 8,
|
||||||
fontSize: 13,
|
horizontal: 12,
|
||||||
color: Colors.grey,
|
),
|
||||||
fontWeight: FontWeight.w500,
|
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),
|
||||||
const SizedBox(height: 6),
|
],
|
||||||
Wrap(
|
|
||||||
spacing: 6,
|
if (allPlayers.isNotEmpty) ...[
|
||||||
runSpacing: 6,
|
const Text(
|
||||||
children: allPlayers.map((player) {
|
'Players',
|
||||||
return TextIconTile(
|
style: TextStyle(
|
||||||
text: player.name,
|
fontSize: 13,
|
||||||
iconEnabled: false,
|
color: Colors.grey,
|
||||||
);
|
fontWeight: FontWeight.w500,
|
||||||
}).toList(),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Wrap(
|
||||||
|
spacing: 6,
|
||||||
|
runSpacing: 6,
|
||||||
|
children: allPlayers.map((player) {
|
||||||
|
return TextIconTile(text: player.name, iconEnabled: false);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -187,5 +177,4 @@ class _GameHistoryTileState extends State<GameHistoryTile> {
|
|||||||
|
|
||||||
return allPlayers;
|
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';
|
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