Merge pull request 'CreateGameView erstellen' (#67) from feature/3-creategameview-erstellen into development

Reviewed-on: #67
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
Reviewed-by: mathiskir <mathis.kirchner.mk@gmail.com>
This commit was merged in pull request #67.
This commit is contained in:
2025-12-10 20:43:53 +00:00
10 changed files with 440 additions and 223 deletions

View File

@@ -27,5 +27,19 @@ enum ExportResult { success, canceled, unknownException }
/// - [Ruleset.singleWinner]: The game is won by a single player /// - [Ruleset.singleWinner]: The game is won by a single player
/// - [Ruleset.singleLoser]: The game is lost by a single player /// - [Ruleset.singleLoser]: The game is lost by a single player
/// - [Ruleset.mostPoints]: The player with the most points wins. /// - [Ruleset.mostPoints]: The player with the most points wins.
/// - [Ruleset.lastPoints]: The player with the fewest points wins. /// - [Ruleset.leastPoints]: The player with the fewest points wins.
enum Ruleset { singleWinner, singleLoser, mostPoints, lastPoints } enum Ruleset { singleWinner, singleLoser, mostPoints, leastPoints }
/// 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.leastPoints:
return 'Least Points';
}
}

View File

@@ -0,0 +1,86 @@
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/text_input/custom_search_bar.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseGameView extends StatefulWidget {
final List<(String, String, Ruleset)> games;
final int initialGameIndex;
const ChooseGameView({
super.key,
required this.games,
required this.initialGameIndex,
});
@override
State<ChooseGameView> createState() => _ChooseGameViewState();
}
class _ChooseGameViewState extends State<ChooseGameView> {
late int selectedGameIndex;
final TextEditingController searchBarController = TextEditingController();
@override
void initState() {
selectedGameIndex = widget.initialGameIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(selectedGameIndex);
},
),
title: const Text(
'Choose Game',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar(
controller: searchBarController,
hintText: 'Game Name',
),
),
const SizedBox(height: 5),
Expanded(
child: ListView.builder(
itemCount: widget.games.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
title: widget.games[index].$1,
description: widget.games[index].$2,
badgeText: translateRulesetToString(widget.games[index].$3),
isHighlighted: selectedGameIndex == index,
onPressed: () async {
setState(() {
if (selectedGameIndex == index) {
selectedGameIndex = -1;
} else {
selectedGameIndex = index;
}
});
},
);
},
),
),
],
),
);
}
}

View File

@@ -1,16 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.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';
class ChooseGroupView extends StatefulWidget { class ChooseGroupView extends StatefulWidget {
final List<Group> groups; final List<Group> groups;
final int initialGroupIndex; final String initialGroupId;
const ChooseGroupView({ const ChooseGroupView({
super.key, super.key,
required this.groups, required this.groups,
required this.initialGroupIndex, required this.initialGroupId,
}); });
@override @override
@@ -18,11 +20,15 @@ class ChooseGroupView extends StatefulWidget {
} }
class _ChooseGroupViewState extends State<ChooseGroupView> { class _ChooseGroupViewState extends State<ChooseGroupView> {
late int selectedGroupIndex; late String selectedGroupId;
final TextEditingController controller = TextEditingController();
final String hintText = 'Group Name';
late final List<Group> filteredGroups;
@override @override
void initState() { void initState() {
selectedGroupIndex = widget.initialGroupIndex; selectedGroupId = widget.initialGroupId;
filteredGroups = [...widget.groups];
super.initState(); super.initState();
} }
@@ -33,34 +39,90 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
appBar: AppBar( appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedGroupId == ''
? null
: widget.groups.firstWhere(
(group) => group.id == selectedGroupId,
),
);
},
),
title: const Text( title: const Text(
'Choose Group', 'Choose Group',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
centerTitle: true, centerTitle: true,
), ),
body: ListView.builder( body: Column(
padding: const EdgeInsets.only(bottom: 85), children: [
itemCount: widget.groups.length, Padding(
itemBuilder: (BuildContext context, int index) { padding: const EdgeInsets.symmetric(horizontal: 10),
return GestureDetector( child: CustomSearchBar(
onTap: () { controller: controller,
setState(() { hintText: hintText,
selectedGroupIndex = index; onChanged: (value) {
}); setState(() {
filterGroups(value);
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,
), ),
); ),
}, Expanded(
child: Visibility(
visible: filteredGroups.isNotEmpty,
replacement: const TopCenteredMessage(
icon: Icons.info,
title: 'Info',
message: 'There is no group matching your search',
),
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: filteredGroups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
onTap: () {
setState(() {
if (selectedGroupId != filteredGroups[index].id) {
selectedGroupId = filteredGroups[index].id;
} else {
selectedGroupId = '';
}
});
},
child: GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
),
);
},
),
),
),
],
), ),
); );
} }
/// Filters the groups based on the search query.
/// TODO: Maybe implement also targetting player names?
void filterGroups(String query) {
setState(() {
if (query.isEmpty) {
filteredGroups.clear();
filteredGroups.addAll(widget.groups);
} else {
filteredGroups.clear();
filteredGroups.addAll(
widget.groups.where(
(group) => group.name.toLowerCase().contains(query.toLowerCase()),
),
);
}
});
}
} }

View File

@@ -1,11 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/ruleset_list_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget { class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String, String)> rulesets; final List<(Ruleset, String)> rulesets;
final int initialRulesetIndex; final int initialRulesetIndex;
const ChooseRulesetView({ const ChooseRulesetView({
super.key, super.key,
required this.rulesets, required this.rulesets,
@@ -35,84 +36,41 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
appBar: AppBar( appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: const Text( title: const Text(
'Choose Ruleset', 'Choose Ruleset',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
), ),
centerTitle: true, centerTitle: true,
), ),
body: Column( body: ListView.builder(
children: [ padding: const EdgeInsets.only(bottom: 85),
Container( itemCount: widget.rulesets.length,
color: CustomTheme.backgroundColor, itemBuilder: (BuildContext context, int index) {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), return TitleDescriptionListTile(
child: TabBar( onPressed: () async {
padding: const EdgeInsets.symmetric(horizontal: 5), setState(() {
// Label Settings if (selectedRulesetIndex == index) {
labelStyle: const TextStyle( selectedRulesetIndex = -1;
fontSize: 16, } else {
fontWeight: FontWeight.bold, selectedRulesetIndex = index;
), }
labelColor: Colors.white, });
unselectedLabelStyle: const TextStyle(fontSize: 14), },
unselectedLabelColor: Colors.white70, title: translateRulesetToString(widget.rulesets[index].$1),
// Indicator Settings description: widget.rulesets[index].$2,
indicator: CustomTheme.standardBoxDecoration, isHighlighted: selectedRulesetIndex == index,
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),
),
),
],
),
),
],
), ),
), ),
); );

View File

@@ -1,3 +1,4 @@
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/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
@@ -5,8 +6,10 @@ 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_game/choose_game_view.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_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart'; import 'package:game_tracker/presentation/views/main_menu/create_game/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/views/main_menu/game_result_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart'; import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart'; import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
@@ -14,7 +17,8 @@ import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CreateGameView extends StatefulWidget { class CreateGameView extends StatefulWidget {
const CreateGameView({super.key}); final VoidCallback? onWinnerChanged;
const CreateGameView({super.key, this.onWinnerChanged});
@override @override
State<CreateGameView> createState() => _CreateGameViewState(); State<CreateGameView> createState() => _CreateGameViewState();
@@ -39,12 +43,18 @@ class _CreateGameViewState extends State<CreateGameView> {
/// List of all players from the database /// List of all players from the database
List<Player> playerList = []; List<Player> playerList = [];
/// List of players filtered based on the selected group
/// If a group is selected, this list contains all players from [playerList]
/// who are not members of the selected group. If no group is selected,
/// this list is identical to [playerList].
List<Player> filteredPlayerList = [];
/// The currently selected group /// The currently selected group
Group? selectedGroup; Group? selectedGroup;
/// The index of the currently selected group in [groupsList] to mark it in /// The index of the currently selected group in [groupsList] to mark it in
/// the [ChooseGroupView] /// the [ChooseGroupView]
int selectedGroupIndex = -1; String selectedGroupId = '';
/// The currently selected ruleset /// The currently selected ruleset
Ruleset? selectedRuleset; Ruleset? selectedRuleset;
@@ -53,37 +63,48 @@ class _CreateGameViewState extends State<CreateGameView> {
/// the [ChooseRulesetView] /// the [ChooseRulesetView]
int selectedRulesetIndex = -1; int selectedRulesetIndex = -1;
/// The index of the currently selected game in [games] to mark it in
/// the [ChooseGameView]
int selectedGameIndex = -1;
/// The currently selected players /// The currently selected players
List<Player>? selectedPlayers; List<Player>? selectedPlayers;
/// List of available rulesets with their display names and descriptions /// List of available rulesets with their descriptions
/// as tuples of (Ruleset, String, String) /// as tuples of (Ruleset, String)
List<(Ruleset, String, String)> rulesets = [ /// TODO: Replace when rulesets are implemented
List<(Ruleset, String)> rulesets = [
( (
Ruleset.singleWinner, Ruleset.singleWinner,
'Single Winner',
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.', 'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
), ),
( (
Ruleset.singleLoser, Ruleset.singleLoser,
'Single Loser',
'Exactly one loser is determined; last place receives the penalty or consequence.', 'Exactly one loser is determined; last place receives the penalty or consequence.',
), ),
( (
Ruleset.mostPoints, Ruleset.mostPoints,
'Most Points',
'Traditional ruleset: the player with the most points wins.', 'Traditional ruleset: the player with the most points wins.',
), ),
( (
Ruleset.lastPoints, Ruleset.leastPoints,
'Least Points',
'Inverse scoring: the player with the fewest points wins.', 'Inverse scoring: the player with the fewest points wins.',
), ),
]; ];
// TODO: Replace when games are implemented
List<(String, String, Ruleset)> games = [
('Example Game 1', 'This is a discription', Ruleset.leastPoints),
('Example Game 2', '', Ruleset.singleWinner),
];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_gameNameController.addListener(() {
setState(() {});
});
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups(); _allGroupsFuture = db.groupDao.getAllGroups();
@@ -93,6 +114,8 @@ class _CreateGameViewState extends State<CreateGameView> {
groupsList = result[0] as List<Group>; groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>; playerList = result[1] as List<Player>;
}); });
filteredPlayerList = List.from(playerList);
} }
@override @override
@@ -113,15 +136,38 @@ class _CreateGameViewState extends State<CreateGameView> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
Container( Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: TextInputField( child: TextInputField(
controller: _gameNameController, controller: _gameNameController,
hintText: 'Game name', hintText: 'Game name',
onChanged: (value) {
setState(() {});
},
), ),
), ),
ChooseTile(
title: 'Game',
trailingText: selectedGameIndex == -1
? 'None'
: games[selectedGameIndex].$1,
onPressed: () async {
selectedGameIndex = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseGameView(
games: games,
initialGameIndex: selectedGameIndex,
),
),
);
setState(() {
if (selectedGameIndex != -1) {
selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
} else {
selectedRuleset = null;
}
});
},
),
ChooseTile( ChooseTile(
title: 'Ruleset', title: 'Ruleset',
trailingText: selectedRuleset == null trailingText: selectedRuleset == null
@@ -139,6 +185,7 @@ class _CreateGameViewState extends State<CreateGameView> {
selectedRulesetIndex = rulesets.indexWhere( selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset, (r) => r.$1 == selectedRuleset,
); );
selectedGameIndex = -1;
setState(() {}); setState(() {});
}, },
), ),
@@ -152,28 +199,28 @@ class _CreateGameViewState extends State<CreateGameView> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) => ChooseGroupView( builder: (context) => ChooseGroupView(
groups: groupsList, groups: groupsList,
initialGroupIndex: selectedGroupIndex, initialGroupId: selectedGroupId,
), ),
), ),
); );
selectedGroupIndex = groupsList.indexWhere( selectedGroupId = selectedGroup?.id ?? '';
(g) => g.id == selectedGroup?.id, if (selectedGroup != null) {
); filteredPlayerList = playerList
.where(
(p) => !selectedGroup!.members.any((m) => m.id == p.id),
)
.toList();
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {}); setState(() {});
}, },
), ),
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'), key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialPlayers: selectedGroup == null initialSelectedPlayers: selectedPlayers ?? [],
? playerList availablePlayers: filteredPlayerList,
: playerList
.where(
(p) => !selectedGroup!.members.any(
(m) => m.id == p.id,
),
)
.toList(),
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = value; selectedPlayers = value;
@@ -181,7 +228,6 @@ class _CreateGameViewState extends State<CreateGameView> {
}, },
), ),
), ),
CustomWidthButton( CustomWidthButton(
text: 'Create game', text: 'Create game',
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
@@ -191,42 +237,37 @@ class _CreateGameViewState extends State<CreateGameView> {
Game game = Game( Game game = Game(
name: _gameNameController.text.trim(), name: _gameNameController.text.trim(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup!, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
); );
// TODO: Replace with navigation to GameResultView() await db.gameDao.addGame(game: game);
print('Created game: $game'); if (context.mounted) {
Navigator.pop(context); Navigator.pushReplacement(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => GameResultView(
game: game,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
} }
: null, : 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 /// Determines whether the "Create Game" button should be enabled based on
/// the current state of the input fields. /// the current state of the input fields.
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return _gameNameController.text.isNotEmpty && return _gameNameController.text.isNotEmpty &&
(selectedGroup != null || (selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.isNotEmpty)) && (selectedPlayers != null && selectedPlayers!.length > 1)) &&
selectedRuleset != null; selectedRuleset != null;
} }
} }

View File

@@ -5,7 +5,7 @@ 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/create_game/create_game_view.dart';
import 'package:game_tracker/presentation/views/main_menu/game_result_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/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';
@@ -98,17 +98,16 @@ class _GameHistoryViewState extends State<GameHistoryView> {
} }
return GameHistoryTile( return GameHistoryTile(
onTap: () async { onTap: () async {
await Navigator.push( Navigator.push(
context, context,
CupertinoPageRoute( CupertinoPageRoute(
fullscreenDialog: true, fullscreenDialog: true,
builder: (context) => builder: (context) => GameResultView(
GameResultView(game: games[index]), game: games[index],
onWinnerChanged: refreshGameList,
),
), ),
); );
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
}, },
game: games[index], game: games[index],
); );
@@ -123,17 +122,13 @@ class _GameHistoryViewState extends State<GameHistoryView> {
text: 'Create Game', text: 'Create Game',
sizeRelativeToWidth: 0.90, sizeRelativeToWidth: 0.90,
onPressed: () async { onPressed: () async {
await Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) =>
return const CreateGroupView(); CreateGameView(onWinnerChanged: refreshGameList),
},
), ),
); );
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
}, },
), ),
), ),
@@ -141,4 +136,10 @@ class _GameHistoryViewState extends State<GameHistoryView> {
), ),
); );
} }
void refreshGameList() {
setState(() {
_gameListFuture = db.gameDao.getAllGames();
});
}
} }

View File

@@ -9,8 +9,9 @@ import 'package:provider/provider.dart';
class GameResultView extends StatefulWidget { class GameResultView extends StatefulWidget {
final Game game; final Game game;
const GameResultView({super.key, required this.game}); final VoidCallback? onWinnerChanged;
const GameResultView({super.key, required this.game, this.onWinnerChanged});
@override @override
State<GameResultView> createState() => _GameResultViewState(); State<GameResultView> createState() => _GameResultViewState();
} }
@@ -131,6 +132,7 @@ class _GameResultViewState extends State<GameResultView> {
winnerId: _selectedPlayer!.id, winnerId: _selectedPlayer!.id,
); );
} }
widget.onWinnerChanged?.call();
} }
List<Player> getAllPlayers(Game game) { List<Player> getAllPlayers(Game game) {

View File

@@ -11,12 +11,14 @@ import 'package:provider/provider.dart';
class PlayerSelection extends StatefulWidget { class PlayerSelection extends StatefulWidget {
final Function(List<Player> value) onChanged; final Function(List<Player> value) onChanged;
final List<Player> initialPlayers; final List<Player> availablePlayers;
final List<Player>? initialSelectedPlayers;
const PlayerSelection({ const PlayerSelection({
super.key, super.key,
required this.onChanged, required this.onChanged,
this.initialPlayers = const [], this.availablePlayers = const [],
this.initialSelectedPlayers,
}); });
@override @override
@@ -51,10 +53,24 @@ class _PlayerSelectionState extends State<PlayerSelection> {
suggestedPlayers = skeletonData; suggestedPlayers = skeletonData;
_allPlayersFuture.then((loadedPlayers) { _allPlayersFuture.then((loadedPlayers) {
setState(() { setState(() {
if (widget.initialPlayers.isNotEmpty) { // If a list of available players is provided, use that list.
allPlayers = [...widget.initialPlayers]; if (widget.availablePlayers.isNotEmpty) {
suggestedPlayers = [...widget.initialPlayers]; widget.availablePlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers];
suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => widget.availablePlayers.any(
(available) => available.id == p.id,
),
)
.toList();
}
} else { } else {
// Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers]; allPlayers = [...loadedPlayers];
suggestedPlayers = [...loadedPlayers]; suggestedPlayers = [...loadedPlayers];

View File

@@ -1,55 +0,0 @@
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),
],
),
),
);
}
}

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class TitleDescriptionListTile extends StatelessWidget {
final String title;
final String description;
final VoidCallback? onPressed;
final bool isHighlighted;
final String? badgeText;
final Color? badgeColor;
const TitleDescriptionListTile({
super.key,
required this.title,
required this.description,
this.onPressed,
this.isHighlighted = false,
this.badgeText,
this.badgeColor,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onPressed,
child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 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,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 230,
child: Text(
title,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
),
if (badgeText != null) ...[
const Spacer(),
Container(
constraints: const BoxConstraints(maxWidth: 100),
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
vertical: 2,
horizontal: 6,
),
decoration: BoxDecoration(
color: badgeColor ?? CustomTheme.primaryColor,
borderRadius: BorderRadius.circular(4),
),
child: Text(
badgeText!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
softWrap: false,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
if (description.isNotEmpty) ...[
const SizedBox(height: 5),
Text(description, style: const TextStyle(fontSize: 14)),
const SizedBox(height: 2.5),
],
],
),
),
);
}
}