CreateGameView erstellen #67

Merged
sneeex merged 35 commits from feature/3-creategameview-erstellen into development 2025-12-10 20:43:54 +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),
flixcoo marked this conversation as resolved
Review

Warum lässt sich in Choose Game View suchen, im Choose Group View aber nicht? Können beides ja potenziell sehr lange Listen sein

Warum lässt sich in Choose Game View suchen, im Choose Group View aber nicht? Können beides ja potenziell sehr lange Listen sein
Review

Erledigt (So würde dass dann auch für die Games funktionieren)

Erledigt (So würde dass dann auch für die Games funktionieren)
), ),
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',
),
flixcoo marked this conversation as resolved
Review

Hier bitte ohne Punkt, das hatten wir ja gesagt, und wir müssen uns darauf einigen ob der Titel infos erhält, oder nur keywords wie info/error, weil ich das bis jetzt so gemacht habe.
Sonst passt so

Hier bitte ohne Punkt, das hatten wir ja gesagt, und wir müssen uns darauf einigen ob der Titel infos erhält, oder nur keywords wie info/error, weil ich das bis jetzt so gemacht habe. Sonst passt so
Review

Wenn der Titel keine Infos enthalten soll, würd ich daraus n enum Attribut machen damit der je nach enum gesetzt wird (Icon und Titel)

Wenn der Titel keine Infos enthalten soll, würd ich daraus n enum Attribut machen damit der je nach enum gesetzt wird (Icon und Titel)
Review

ja findste mit oder ohne richtigen Title besser?

ja findste mit oder ohne richtigen Title besser?
Review

ist mir auch egal, lass es so oder nicht. Wenn du es mit Info/Error im Titel willst, mach am besten das einmal temporär in den Titel und dann nen Issue für Änderung der anderen Titel in eine enum Version oder halt eine Version mit aussagekräftigem Titel.

Aber auf jeden Fall den Punkt weg machen am Ende von Message
dann approve ich

ist mir auch egal, lass es so oder nicht. Wenn du es mit Info/Error im Titel willst, mach am besten das einmal temporär in den Titel und dann nen Issue für Änderung der anderen Titel in eine enum Version oder halt eine Version mit aussagekräftigem Titel. **Aber auf jeden Fall den Punkt weg machen am Ende von Message** dann approve ich
Review

erledigt

erledigt
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(
flixcoo marked this conversation as resolved
Review

badgetext nicht overflow sicher
grafik.png

badgetext nicht overflow sicher ![grafik.png](/attachments/44ff847b-617f-4a67-9a09-1547fe967e1c)
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),
],
],
),
),
);
}
}