Renamed every instance of "game" to "match"

This commit is contained in:
2025-12-11 20:07:32 +01:00
parent d0059b44a8
commit 99cea1e703
41 changed files with 1525 additions and 1459 deletions

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

@@ -0,0 +1,128 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.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 {
final List<Group> groups;
final String initialGroupId;
const ChooseGroupView({
super.key,
required this.groups,
required this.initialGroupId,
});
@override
State<ChooseGroupView> createState() => _ChooseGroupViewState();
}
class _ChooseGroupViewState extends State<ChooseGroupView> {
late String selectedGroupId;
final TextEditingController controller = TextEditingController();
final String hintText = 'Group Name';
late final List<Group> filteredGroups;
@override
void initState() {
selectedGroupId = widget.initialGroupId;
filteredGroups = [...widget.groups];
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(
selectedGroupId == ''
? null
: widget.groups.firstWhere(
(group) => group.id == selectedGroupId,
),
);
},
),
title: const Text(
'Choose Group',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: CustomSearchBar(
controller: controller,
hintText: hintText,
onChanged: (value) {
setState(() {
filterGroups(value);
});
},
),
),
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

@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String)> rulesets;
final int initialRulesetIndex;
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: const Text(
'Choose Ruleset',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
onPressed: () async {
setState(() {
if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1;
} else {
selectedRulesetIndex = index;
}
});
},
title: translateRulesetToString(widget.rulesets[index].$1),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);
},
),
),
);
}
}

View File

@@ -0,0 +1,273 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_game_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
import 'package:provider/provider.dart';
class CreateMatchView extends StatefulWidget {
final VoidCallback? onWinnerChanged;
const CreateMatchView({super.key, this.onWinnerChanged});
@override
State<CreateMatchView> createState() => _CreateMatchViewState();
}
class _CreateMatchViewState extends State<CreateMatchView> {
/// Reference to the app database
late final AppDatabase db;
/// Futures to load all groups and players from the database
late Future<List<Group>> _allGroupsFuture;
/// Future to load all players from the database
late Future<List<Player>> _allPlayersFuture;
/// Controller for the game name input field
final TextEditingController _gameNameController = TextEditingController();
/// List of all groups from the database
List<Group> groupsList = [];
/// List of all players from the database
List<Player> playerList = [];
/// 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
Group? selectedGroup;
/// The index of the currently selected group in [groupsList] to mark it in
/// the [ChooseGroupView]
String selectedGroupId = '';
/// The currently selected ruleset
Ruleset? selectedRuleset;
/// The index of the currently selected ruleset in [rulesets] to mark it in
/// the [ChooseRulesetView]
int selectedRulesetIndex = -1;
/// The index of the currently selected game in [games] to mark it in
/// the [ChooseGameView]
int selectedGameIndex = -1;
/// The currently selected players
List<Player>? selectedPlayers;
/// List of available rulesets with their descriptions
/// as tuples of (Ruleset, String)
/// TODO: Replace when rulesets are implemented
List<(Ruleset, String)> rulesets = [
(
Ruleset.singleWinner,
'Exactly one winner is chosen; ties are resolved by a predefined tiebreaker.',
),
(
Ruleset.singleLoser,
'Exactly one loser is determined; last place receives the penalty or consequence.',
),
(
Ruleset.mostPoints,
'Traditional ruleset: the player with the most points wins.',
),
(
Ruleset.leastPoints,
'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
void initState() {
super.initState();
_gameNameController.addListener(() {
setState(() {});
});
db = Provider.of<AppDatabase>(context, listen: false);
_allGroupsFuture = db.groupDao.getAllGroups();
_allPlayersFuture = db.playerDao.getAllPlayers();
Future.wait([_allGroupsFuture, _allPlayersFuture]).then((result) async {
groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>;
});
filteredPlayerList = List.from(playerList);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
backgroundColor: CustomTheme.backgroundColor,
scrolledUnderElevation: 0,
title: const Text(
'Create new game',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
centerTitle: true,
),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: TextInputField(
controller: _gameNameController,
hintText: 'Game name',
),
),
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(
title: 'Ruleset',
trailingText: selectedRuleset == null
? 'None'
: translateRulesetToString(selectedRuleset!),
onPressed: () async {
selectedRuleset = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseRulesetView(
rulesets: rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
selectedGameIndex = -1;
setState(() {});
},
),
ChooseTile(
title: 'Group',
trailingText: selectedGroup == null
? 'None'
: selectedGroup!.name,
onPressed: () async {
selectedGroup = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroupId,
),
),
);
selectedGroupId = selectedGroup?.id ?? '';
if (selectedGroup != null) {
filteredPlayerList = playerList
.where(
(p) => !selectedGroup!.members.any((m) => m.id == p.id),
)
.toList();
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {});
},
),
Expanded(
child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers ?? [],
availablePlayers: filteredPlayerList,
onChanged: (value) {
setState(() {
selectedPlayers = value;
});
},
),
),
CustomWidthButton(
text: 'Create game',
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton()
? () async {
Match match = Match(
name: _gameNameController.text.trim(),
createdAt: DateTime.now(),
group: selectedGroup,
players: selectedPlayers,
);
await db.matchDao.addMatch(match: match);
if (context.mounted) {
Navigator.pushReplacement(
context,
CupertinoPageRoute(
fullscreenDialog: true,
builder: (context) => GameResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
),
);
}
}
: null,
),
],
),
),
);
}
/// Determines whether the "Create Game" button should be enabled based on
/// the current state of the input fields.
bool _enableCreateGameButton() {
return _gameNameController.text.isNotEmpty &&
(selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.length > 1)) &&
selectedRuleset != null;
}
}