Merge branch 'development' into feature/168-teamspiele-implementieren

# Conflicts:
#	lib/presentation/views/main_menu/match_view/create_match/create_match_view.dart
#	lib/presentation/views/main_menu/match_view/match_detail_view.dart
#	lib/presentation/views/main_menu/match_view/match_result_view.dart
#	lib/presentation/widgets/buttons/main_menu_button.dart
#	pubspec.yaml
This commit is contained in:
2026-05-18 01:06:46 +02:00
39 changed files with 846 additions and 380 deletions

View File

@@ -7,6 +7,7 @@ import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_game_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/game_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -70,7 +71,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
@@ -83,7 +84,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
},
),
actions: [
IconButton(
HapticIconButton(
icon: const Icon(Icons.add),
onPressed: () async {
final result = await Navigator.push(

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/text_input/custom_search_bar.dart';
import 'package:tallee/presentation/widgets/tiles/group_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -45,7 +46,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false,
appBar: AppBar(
leading: IconButton(
leading: HapticIconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
@@ -111,7 +112,10 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
padding: const EdgeInsets.only(bottom: 85),
itemCount: filteredGroups.length,
itemBuilder: (BuildContext context, int index) {
return GestureDetector(
return GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
onTap: () {
setState(() {
if (selectedGroupId != filteredGroups[index].id) {
@@ -121,11 +125,6 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
}
});
},
child: GroupTile(
group: filteredGroups[index],
isHighlighted:
selectedGroupId == filteredGroups[index].id,
),
);
},
),

View File

@@ -1,6 +1,7 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_popup/flutter_popup.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/common.dart';
@@ -12,6 +13,7 @@ import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/text_input/text_input_field.dart';
@@ -47,9 +49,9 @@ class _CreateGameViewState extends State<CreateGameView> {
late final AppDatabase db;
late List<(Ruleset, String)> _rulesets;
Ruleset? selectedRuleset = Ruleset.singleWinner;
late List<(GameColor, String)> _colors;
Ruleset? selectedRuleset = Ruleset.singleWinner;
GameColor? selectedColor = GameColor.orange;
/// Controller for the game name input field.
@@ -77,38 +79,20 @@ class _CreateGameViewState extends State<CreateGameView> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_rulesets = [
(
Ruleset.singleWinner,
translateRulesetToString(Ruleset.singleWinner, context),
_rulesets = List.generate(
Ruleset.values.length,
(index) => (
Ruleset.values[index],
translateRulesetToString(Ruleset.values[index], context),
),
(
Ruleset.singleLoser,
translateRulesetToString(Ruleset.singleLoser, context),
);
_colors = List.generate(
GameColor.values.length,
(index) => (
GameColor.values[index],
translateGameColorToString(GameColor.values[index], context),
),
(
Ruleset.highestScore,
translateRulesetToString(Ruleset.highestScore, context),
),
(
Ruleset.lowestScore,
translateRulesetToString(Ruleset.lowestScore, context),
),
(
Ruleset.multipleWinners,
translateRulesetToString(Ruleset.multipleWinners, context),
),
];
_colors = [
(GameColor.green, translateGameColorToString(GameColor.green, context)),
(GameColor.teal, translateGameColorToString(GameColor.teal, context)),
(GameColor.blue, translateGameColorToString(GameColor.blue, context)),
(GameColor.purple, translateGameColorToString(GameColor.purple, context)),
(GameColor.pink, translateGameColorToString(GameColor.pink, context)),
(GameColor.red, translateGameColorToString(GameColor.red, context)),
(GameColor.orange, translateGameColorToString(GameColor.orange, context)),
(GameColor.yellow, translateGameColorToString(GameColor.yellow, context)),
];
);
if (widget.gameToEdit != null) {
_gameNameController.text = widget.gameToEdit!.name;
@@ -138,7 +122,7 @@ class _CreateGameViewState extends State<CreateGameView> {
title: Text(isEditing ? loc.edit_game : loc.create_game),
actions: [
if (isEditMode())
IconButton(
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
if (!context.mounted) return;
@@ -214,10 +198,13 @@ class _CreateGameViewState extends State<CreateGameView> {
// Choose ruleset tile
if (!isEditMode())
ChooseTile(title: loc.ruleset, trailing: getColorDropdown(loc)),
ChooseTile(
title: loc.ruleset,
trailing: getRulesetDropdown(loc),
),
// Choose color tile
ChooseTile(title: loc.color, trailing: getRulesetDropdown(loc)),
ChooseTile(title: loc.color, trailing: getColorDropdown(loc)),
// Description input field
Container(
@@ -344,6 +331,12 @@ class _CreateGameViewState extends State<CreateGameView> {
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
barrierColor: Colors.transparent,
contentDecoration: CustomTheme.standardBoxDecoration,
onBeforePopup: () async {
await HapticFeedback.selectionClick();
},
onAfterPopup: () async {
await HapticFeedback.selectionClick();
},
content: StatefulBuilder(
builder: (context, setPopupState) => SizedBox(
width: 280,
@@ -353,7 +346,8 @@ class _CreateGameViewState extends State<CreateGameView> {
children: List.generate(
_rulesets.length,
(index) => GestureDetector(
onTap: () {
onTap: () async {
await HapticFeedback.selectionClick();
setState(() {
selectedRuleset = _rulesets[index].$1;
});
@@ -427,6 +421,12 @@ class _CreateGameViewState extends State<CreateGameView> {
contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 10),
barrierColor: Colors.transparent,
contentDecoration: CustomTheme.standardBoxDecoration,
onBeforePopup: () async {
await HapticFeedback.selectionClick();
},
onAfterPopup: () async {
await HapticFeedback.selectionClick();
},
content: StatefulBuilder(
builder: (context, setPopupState) => SizedBox(
width: 150,
@@ -436,7 +436,8 @@ class _CreateGameViewState extends State<CreateGameView> {
children: List.generate(
_colors.length,
(index) => GestureDetector(
onTap: () {
onTap: () async {
await HapticFeedback.selectionClick();
setState(() {
selectedColor = _colors[index].$1;
});

View File

@@ -273,11 +273,11 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// Determines whether the "Create Match" button should be enabled.
///
/// Returns `true` if:
/// - A ruleset is selected AND
/// - A game is selected AND
/// - Either a group is selected OR at least 2 players are selected.
bool isSubmitButtonEnabled() {
return (selectedGroup != null ||
(selectedPlayers.length > 1) && selectedGame != null);
return ((selectedGroup != null || selectedPlayers.length > 1) &&
selectedGame != null);
}
/// Handles navigation when the create or save button is pressed.

View File

@@ -13,6 +13,7 @@ import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/cards/team_card.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart';
@@ -69,7 +70,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
appBar: AppBar(
title: Text(loc.match_profile),
actions: [
IconButton(
HapticIconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(
@@ -297,6 +298,7 @@ class _MatchDetailViewState extends State<MatchDetailView> {
Widget getResultWidget(AppLocalizations loc) {
if (isSingleRowResult()) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getSingleResultRow(loc),
);
@@ -312,9 +314,12 @@ class _MatchDetailViewState extends State<MatchDetailView> {
if (localMatch.mvp.isNotEmpty || localMatch.mvt.isNotEmpty) {
// Single Winner / Loser
final mvpName = localMatch.isTeamMatch
? localMatch.mvt.first.name
: localMatch.mvp.first.name;
final mvps = localMatch.isTeamMatch
? localMatch.mvt
: localMatch.mvp;
final mvpName = ruleset == Ruleset.multipleWinners
? mvps.map((party) => party.name).join(', ')
: mvps.first.name;
return [
Text(
@@ -440,7 +445,8 @@ class _MatchDetailViewState extends State<MatchDetailView> {
// Returns if the result can be displayed in a single row
bool isSingleRowResult() {
return localMatch.game.ruleset == Ruleset.singleWinner ||
localMatch.game.ruleset == Ruleset.singleLoser;
localMatch.game.ruleset == Ruleset.singleLoser ||
localMatch.game.ruleset == Ruleset.multipleWinners;
}
String getPlacementText(BuildContext context, int rank) {

View File

@@ -10,6 +10,8 @@ import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/data/models/team.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/buttons/haptic_icon_button.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_checkbox_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/live_edit_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_result_view/score_list_tile.dart';
@@ -49,9 +51,11 @@ class _MatchResultViewState extends State<MatchResultView> {
late bool isTeamMatch;
/// Currently selected winner player
/// Currently selected player(s)/team(s) (winner / looser)
Player? _selectedPlayer;
Team? _selectedTeam;
final Set<Player> _selectedPlayers = {};
final Set<Team> _selectedTeams = {};
@override
void initState() {
@@ -84,11 +88,11 @@ class _MatchResultViewState extends State<MatchResultView> {
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
leading: HapticIconButton(
icon: const Icon(Icons.close),
onPressed: () {
widget.onWinnerChanged?.call();
Navigator.of(context).pop(_selectedPlayer);
Navigator.pop(context);
},
),
title: Text(widget.match.name),
@@ -142,17 +146,46 @@ class _MatchResultViewState extends State<MatchResultView> {
const SizedBox(height: 10),
// Show player selection
if (rulesetSupportsWinnerSelection())
Expanded(
child: buildWinnerSelectionWidget(isTeamMatch),
),
if (rulesetSupportsPlayerSelection())
if (ruleset == Ruleset.multipleWinners)
Expanded(
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomCheckboxListTile(
text: allPlayers[index].name,
value: _selectedPlayers.contains(
allPlayers[index],
),
onChanged: (bool value) {
setState(() {
if (value) {
_selectedPlayers.add(
allPlayers[index],
);
} else {
_selectedPlayers.remove(
allPlayers[index],
);
}
});
},
);
},
),
)
else
Expanded(
child: buildWinnerSelectionWidget(isTeamMatch),
),
// Show score entry
if (rulesetSupportsScoreEntry())
Expanded(child: buildScoreEntryWidget(isTeamMatch)),
// Show draggable placement list
if (rulesetSupportsPlacement())
if (rulesetSupportsDragBehaviour())
Expanded(child: buildPlacementWidget(isTeamMatch)),
],
),
@@ -207,16 +240,24 @@ class _MatchResultViewState extends State<MatchResultView> {
// Prefill fields
if (widget.match.mvt.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) {
_selectedTeam = allTeams.firstWhere(
(p) => p.id == widget.match.mvt.first.id,
);
if (rulesetSupportsPlayerSelection()) {
if (ruleset == Ruleset.multipleWinners) {
for (int i = 0; i < allTeams.length; i++) {
if (allTeams[i].score == 1) {
_selectedTeams.add(allTeams[i]);
}
}
} else {
_selectedTeam = allTeams.firstWhere(
(team) => team.id == widget.match.mvt.first.id,
);
}
} else if (rulesetSupportsScoreEntry()) {
for (int i = 0; i < allTeams.length; i++) {
final score = allTeams[i].score;
final score = allTeams[i].score ?? 0;
controller[i].text = score.toString();
}
} else if (rulesetSupportsPlacement()) {
} else if (rulesetSupportsDragBehaviour()) {
allTeams.sort((a, b) {
final scoreA = a.score ?? 0;
final scoreB = b.score ?? 0;
@@ -237,17 +278,25 @@ class _MatchResultViewState extends State<MatchResultView> {
// Prefill fields
if (widget.match.mvp.isNotEmpty) {
if (rulesetSupportsWinnerSelection()) {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.mvp.first.id,
);
if (rulesetSupportsPlayerSelection()) {
if (ruleset == Ruleset.multipleWinners) {
for (int i = 0; i < allPlayers.length; i++) {
if (widget.match.scores[allPlayers[i].id]?.score == 1) {
_selectedPlayers.add(allPlayers[i]);
}
}
} else {
_selectedPlayer = allPlayers.firstWhere(
(p) => p.id == widget.match.mvp.first.id,
);
}
} else if (rulesetSupportsScoreEntry()) {
for (int i = 0; i < allPlayers.length; i++) {
final scoreList = widget.match.scores[allPlayers[i].id];
final score = scoreList?.score ?? 0;
controller[i].text = score.toString();
}
} else if (rulesetSupportsPlacement()) {
} else if (rulesetSupportsDragBehaviour()) {
allPlayers.sort((a, b) {
final scoreA = widget.match.scores[a.id]?.score ?? 0;
final scoreB = widget.match.scores[b.id]?.score ?? 0;
@@ -278,12 +327,14 @@ class _MatchResultViewState extends State<MatchResultView> {
await _handleScores();
} else if (ruleset == Ruleset.placement) {
await _handlePlacement();
} else if (ruleset == Ruleset.multipleWinners) {
await _handleWinners();
}
widget.onWinnerChanged?.call();
}
/// Handles saving or removing the winner in the database.
/// Handles saving or removing the (single) winner in the database.
Future<bool> _handleWinner() async {
if (isTeamMatch) {
if (_selectedTeam == null) {
@@ -309,6 +360,18 @@ class _MatchResultViewState extends State<MatchResultView> {
}
}
/// Handles saving the (multiple) winners to the database.
Future<bool> _handleWinners() async {
if (_selectedPlayers.isEmpty) {
return await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else {
return await db.scoreEntryDao.setWinners(
matchId: widget.match.id,
winners: allPlayers.where((p) => _selectedPlayers.contains(p)).toList(),
);
}
}
/// Handles saving or removing the loser in the database.
Future<bool> _handleLoser() async {
if (isTeamMatch) {
@@ -389,20 +452,24 @@ class _MatchResultViewState extends State<MatchResultView> {
return loc.select_loser;
case Ruleset.placement:
return loc.drag_to_set_placement;
case Ruleset.multipleWinners:
return loc.select_winners;
default:
return loc.enter_points;
}
}
bool rulesetSupportsWinnerSelection() {
return ruleset == Ruleset.singleWinner || ruleset == Ruleset.singleLoser;
bool rulesetSupportsPlayerSelection() {
return ruleset == Ruleset.singleWinner ||
ruleset == Ruleset.singleLoser ||
ruleset == Ruleset.multipleWinners;
}
bool rulesetSupportsScoreEntry() {
return ruleset == Ruleset.lowestScore || ruleset == Ruleset.highestScore;
}
bool rulesetSupportsPlacement() {
bool rulesetSupportsDragBehaviour() {
return ruleset == Ruleset.placement;
}