Merge branch 'development' into enhancement/70-konsistenzfehler-im-json-vermeiden
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Failing after 2m8s

This commit is contained in:
2026-01-02 20:45:47 +00:00
6 changed files with 75 additions and 47 deletions

View File

@@ -6,8 +6,8 @@ import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/game_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/match_summary_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -112,13 +112,13 @@ class _HomeViewState extends State<HomeView> {
visible: !isLoading && loadedRecentMatches.isNotEmpty, visible: !isLoading && loadedRecentMatches.isNotEmpty,
replacement: const Center( replacement: const Center(
heightFactor: 12, heightFactor: 12,
child: Text('No recent games available'), child: Text('No recent matches available'),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MatchTile( MatchSummaryTile(
matchTitle: recentMatches[0].name, matchTitle: recentMatches[0].name,
game: 'Winner', game: 'Winner',
ruleset: 'Ruleset', ruleset: 'Ruleset',
@@ -132,20 +132,20 @@ class _HomeViewState extends State<HomeView> {
child: Divider(), child: Divider(),
), ),
if (loadedRecentMatches.length > 1) ...[ if (loadedRecentMatches.length > 1) ...[
MatchTile( MatchSummaryTile(
matchTitle: recentMatches[1].name, matchTitle: recentMatches[1].name,
game: 'Winner', game: 'Winner',
ruleset: 'Ruleset', ruleset: 'Ruleset',
players: _getPlayerText(recentMatches[1]), players: _getPlayerText(recentMatches[1]),
winner: recentMatches[1].winner == null winner: recentMatches[1].winner == null
? 'Game in progress...' ? 'Match in progress...'
: recentMatches[1].winner!.name, : recentMatches[1].winner!.name,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
] else ...[ ] else ...[
const Center( const Center(
heightFactor: 5.35, heightFactor: 5.35,
child: Text('No second game available'), child: Text('No second match available'),
), ),
], ],
], ],

View File

@@ -31,6 +31,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Controller for the match name input field /// Controller for the match name input field
final TextEditingController _matchNameController = TextEditingController(); final TextEditingController _matchNameController = TextEditingController();
/// Hint text for the match name input field
String hintText = 'Match Name';
/// List of all groups from the database /// List of all groups from the database
List<Group> groupsList = []; List<Group> groupsList = [];
@@ -107,8 +110,10 @@ class _CreateMatchViewState extends State<CreateMatchView> {
]).then((result) async { ]).then((result) async {
groupsList = result[0] as List<Group>; groupsList = result[0] as List<Group>;
playerList = result[1] as List<Player>; playerList = result[1] as List<Player>;
setState(() {
filteredPlayerList = List.from(playerList);
});
}); });
filteredPlayerList = List.from(playerList);
} }
@override @override
@@ -132,7 +137,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
child: TextInputField( child: TextInputField(
controller: _matchNameController, controller: _matchNameController,
hintText: 'Match name', hintText: hintText,
), ),
), ),
ChooseTile( ChooseTile(
@@ -151,11 +156,13 @@ class _CreateMatchViewState extends State<CreateMatchView> {
); );
setState(() { setState(() {
if (selectedGameIndex != -1) { if (selectedGameIndex != -1) {
hintText = games[selectedGameIndex].$1;
selectedRuleset = games[selectedGameIndex].$3; selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = rulesets.indexWhere( selectedRulesetIndex = rulesets.indexWhere(
(r) => r.$1 == selectedRuleset, (r) => r.$1 == selectedRuleset,
); );
} else { } else {
hintText = 'Match Name';
selectedRuleset = null; selectedRuleset = null;
} }
}); });
@@ -228,7 +235,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
onPressed: _enableCreateGameButton() onPressed: _enableCreateGameButton()
? () async { ? () async {
Match match = Match( Match match = Match(
name: _matchNameController.text.trim(), name: _matchNameController.text.isEmpty
? hintText
: _matchNameController.text.trim(),
createdAt: DateTime.now(), createdAt: DateTime.now(),
group: selectedGroup, group: selectedGroup,
players: selectedPlayers, players: selectedPlayers,
@@ -258,9 +267,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// 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 _matchNameController.text.isNotEmpty && return selectedGroup != null ||
(selectedGroup != null || (selectedPlayers != null && selectedPlayers!.length > 1) &&
(selectedPlayers != null && selectedPlayers!.length > 1)) && selectedRuleset != null;
selectedRuleset != null;
} }
} }

View File

@@ -12,7 +12,7 @@ import 'package:game_tracker/presentation/views/main_menu/match_view/create_matc
import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_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';
import 'package:game_tracker/presentation/widgets/tiles/game_history_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -30,9 +30,9 @@ class _MatchViewState extends State<MatchView> {
List<Match> matches = List.filled( List<Match> matches = List.filled(
4, 4,
Match( Match(
name: 'Skeleton Gamename', name: 'Skeleton match name',
group: Group( group: Group(
name: 'Groupname', name: 'Group name',
members: List.filled(5, Player(name: 'Player')), members: List.filled(5, Player(name: 'Player')),
), ),
winner: Player(name: 'Player'), winner: Player(name: 'Player'),
@@ -71,10 +71,10 @@ class _MatchViewState extends State<MatchView> {
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
if (index == matches.length) { if (index == matches.length) {
return SizedBox( return SizedBox(
height: MediaQuery.paddingOf(context).bottom - 80, height: MediaQuery.paddingOf(context).bottom - 20,
); );
} }
return GameHistoryTile( return MatchTile(
onTap: () async { onTap: () async {
Navigator.push( Navigator.push(
context, context,
@@ -96,7 +96,7 @@ class _MatchViewState extends State<MatchView> {
Positioned( Positioned(
bottom: MediaQuery.paddingOf(context).bottom, bottom: MediaQuery.paddingOf(context).bottom,
child: CustomWidthButton( child: CustomWidthButton(
text: 'Create Game', text: 'Create Match',
sizeRelativeToWidth: 0.90, sizeRelativeToWidth: 0.90,
onPressed: () async { onPressed: () async {
Navigator.push( Navigator.push(

View File

@@ -12,13 +12,13 @@ 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> availablePlayers; final List<Player>? availablePlayers;
final List<Player>? initialSelectedPlayers; final List<Player>? initialSelectedPlayers;
const PlayerSelection({ const PlayerSelection({
super.key, super.key,
required this.onChanged, required this.onChanged,
this.availablePlayers = const [], this.availablePlayers,
this.initialSelectedPlayers, this.initialSelectedPlayers,
}); });
@@ -56,17 +56,17 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (mounted) { if (mounted) {
_allPlayersFuture.then((loadedPlayers) { _allPlayersFuture.then((loadedPlayers) {
setState(() { setState(() {
// If a list of available players is provided, use that list. // If a list of available players is provided (even if empty), use that list.
if (widget.availablePlayers.isNotEmpty) { if (widget.availablePlayers != null) {
widget.availablePlayers.sort((a, b) => a.name.compareTo(b.name)); widget.availablePlayers!.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...widget.availablePlayers]; allPlayers = [...widget.availablePlayers!];
suggestedPlayers = [...allPlayers]; suggestedPlayers = [...allPlayers];
if (widget.initialSelectedPlayers != null) { if (widget.initialSelectedPlayers != null) {
// Ensures that only players available for selection are pre-selected. // Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers! selectedPlayers = widget.initialSelectedPlayers!
.where( .where(
(p) => widget.availablePlayers.any( (p) => widget.availablePlayers!.any(
(available) => available.id == p.id, (available) => available.id == p.id,
), ),
) )
@@ -149,18 +149,24 @@ class _PlayerSelectionState extends State<PlayerSelection> {
onIconTap: () { onIconTap: () {
setState(() { setState(() {
// Removes the player from the selection and notifies the parent. // Removes the player from the selection and notifies the parent.
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
// Get the current search query
final currentSearch = _searchBarController final currentSearch = _searchBarController
.text .text
.toLowerCase(); .toLowerCase();
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
// If the player matches the current search query (or search is empty), // If the player matches the current search query (or search is empty),
// they are added back to the suggestions and the list is re-sorted. // they are added back to the `suggestedPlayers` and the list is re-sorted.
if (currentSearch.isEmpty || if (currentSearch.isEmpty ||
player.name.toLowerCase().contains( player.name.toLowerCase().contains(
currentSearch, currentSearch,
)) { )) {
suggestedPlayers.add(player); suggestedPlayers.add(player);
suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name),
);
} }
}); });
}, },
@@ -176,9 +182,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
/*
*/
Expanded( Expanded(
child: AppSkeleton( child: AppSkeleton(
enabled: isLoading, enabled: isLoading,
@@ -187,11 +190,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
replacement: TopCenteredMessage( replacement: TopCenteredMessage(
icon: Icons.info, icon: Icons.info,
title: 'Info', title: 'Info',
message: allPlayers.isEmpty message: _getInfoText(),
? 'No players created yet'
: (selectedPlayers.length == allPlayers.length)
? 'No more players to add'
: 'No players found with that name',
), ),
child: ListView.builder( child: ListView.builder(
itemCount: suggestedPlayers.length, itemCount: suggestedPlayers.length,
@@ -200,11 +199,15 @@ class _PlayerSelectionState extends State<PlayerSelection> {
text: suggestedPlayers[index].name, text: suggestedPlayers[index].name,
onPressed: () { onPressed: () {
setState(() { setState(() {
// If the player is not already selected
if (!selectedPlayers.contains( if (!selectedPlayers.contains(
suggestedPlayers[index], suggestedPlayers[index],
)) { )) {
selectedPlayers.add(suggestedPlayers[index]); // Add to player to the front of the selectedPlayers
selectedPlayers.insert(0, suggestedPlayers[index]);
// Notify the parent widget of the change
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
// Remove the player from the suggestedPlayers
suggestedPlayers.remove(suggestedPlayers[index]); suggestedPlayers.remove(suggestedPlayers[index]);
} }
}); });
@@ -229,7 +232,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
bool success = await db.playerDao.addPlayer(player: createdPlayer); bool success = await db.playerDao.addPlayer(player: createdPlayer);
if (!context.mounted) return; if (!context.mounted) return;
if (success) { if (success) {
selectedPlayers.add(createdPlayer); selectedPlayers.insert(0, createdPlayer);
widget.onChanged([...selectedPlayers]); widget.onChanged([...selectedPlayers]);
allPlayers.add(createdPlayer); allPlayers.add(createdPlayer);
setState(() { setState(() {
@@ -263,4 +266,21 @@ class _PlayerSelectionState extends State<PlayerSelection> {
); );
} }
} }
/// Determines the appropriate info text to display when no players
/// are available in the suggested players list.
String _getInfoText() {
if (allPlayers.isEmpty) {
// No players exist in the database
return 'No players created yet';
} else if (selectedPlayers.length == allPlayers.length ||
widget.availablePlayers?.isEmpty == true) {
// All players have been selected or
// available players list is provided but empty
return 'No more players to add';
} else {
// No players match the search query
return 'No players found with that name';
}
}
} }

View File

@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
class MatchTile extends StatefulWidget { class MatchSummaryTile extends StatefulWidget {
final String matchTitle; final String matchTitle;
final String game; final String game;
final String ruleset; final String ruleset;
final String players; final String players;
final String winner; final String winner;
const MatchTile({ const MatchSummaryTile({
super.key, super.key,
required this.matchTitle, required this.matchTitle,
required this.game, required this.game,
@@ -19,10 +19,10 @@ class MatchTile extends StatefulWidget {
}); });
@override @override
State<MatchTile> createState() => _MatchTileState(); State<MatchSummaryTile> createState() => _MatchSummaryTileState();
} }
class _MatchTileState extends State<MatchTile> { class _MatchSummaryTileState extends State<MatchSummaryTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(

View File

@@ -4,17 +4,17 @@ import 'package:game_tracker/data/dto/match.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class GameHistoryTile extends StatefulWidget { class MatchTile extends StatefulWidget {
final Match match; final Match match;
final VoidCallback onTap; final VoidCallback onTap;
const GameHistoryTile({super.key, required this.match, required this.onTap}); const MatchTile({super.key, required this.match, required this.onTap});
@override @override
State<GameHistoryTile> createState() => _GameHistoryTileState(); State<MatchTile> createState() => _MatchTileState();
} }
class _GameHistoryTileState extends State<GameHistoryTile> { class _MatchTileState extends State<MatchTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final group = widget.match.group; final group = widget.match.group;