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:
@@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user