6 Commits

Author SHA1 Message Date
9c4eff5056 Moved button
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 46s
Pull Request Pipeline / lint (pull_request) Successful in 48s
2026-04-19 16:02:14 +02:00
ad2e4bc398 Added constraint parameter 2026-04-19 15:56:11 +02:00
2ad3698067 Updated documentation 2026-04-19 15:25:22 +02:00
32e1c587d4 Updated popups in views 2026-04-19 15:19:38 +02:00
55437d83c4 Changed popup class structure 2026-04-19 15:19:30 +02:00
15702a108d Add: custom dialog action 2026-04-19 15:18:27 +02:00
16 changed files with 203 additions and 495 deletions

View File

@@ -1,9 +1,5 @@
include: package:flutter_lints/flutter.yaml include: package:flutter_lints/flutter.yaml
analyzer:
exclude:
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart
linter: linter:
rules: rules:
avoid_print: false avoid_print: false
@@ -16,3 +12,7 @@ linter:
unnecessary_const: true unnecessary_const: true
lines_longer_than_80_chars: false lines_longer_than_80_chars: false
constant_identifier_names: false constant_identifier_names: false
analyzer:
exclude:
- lib/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart

View File

@@ -85,21 +85,21 @@ class CustomTheme {
); );
static const SearchBarThemeData searchBarTheme = SearchBarThemeData( static const SearchBarThemeData searchBarTheme = SearchBarThemeData(
textStyle: WidgetStatePropertyAll(TextStyle(color: textColor)), textStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.textColor)),
hintStyle: WidgetStatePropertyAll(TextStyle(color: hintColor)), hintStyle: WidgetStatePropertyAll(TextStyle(color: CustomTheme.hintColor)),
); );
static final RadioThemeData radioTheme = RadioThemeData( static final RadioThemeData radioTheme = RadioThemeData(
fillColor: WidgetStateProperty.resolveWith<Color>((states) { fillColor: WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.selected)) { if (states.contains(WidgetState.selected)) {
return primaryColor; return CustomTheme.primaryColor;
} }
return textColor; return CustomTheme.textColor;
}), }),
); );
static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme( static const InputDecorationTheme inputDecorationTheme = InputDecorationTheme(
labelStyle: TextStyle(color: textColor), labelStyle: TextStyle(color: CustomTheme.textColor),
hintStyle: TextStyle(color: hintColor), hintStyle: TextStyle(color: CustomTheme.hintColor),
); );
} }

View File

@@ -22,11 +22,10 @@
"days_ago": "vor {count} Tagen", "days_ago": "vor {count} Tagen",
"delete": "Löschen", "delete": "Löschen",
"delete_all_data": "Alle Daten löschen", "delete_all_data": "Alle Daten löschen",
"delete_group": "Gruppe löschen", "delete_group": "Diese Gruppe löschen",
"delete_match": "Spiel löschen", "delete_match": "Spiel löschen",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"edit_match": "Gruppe bearbeiten", "edit_match": "Gruppe bearbeiten",
"enter_points": "Punkte eingeben",
"enter_results": "Ergebnisse eintragen", "enter_results": "Ergebnisse eintragen",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen",
"error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen", "error_deleting_group": "Fehler beim Löschen der Gruppe, bitte erneut versuchen",
@@ -75,7 +74,6 @@
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"players": "Spieler:innen", "players": "Spieler:innen",
"players_count": "{count} Spieler", "players_count": "{count} Spieler",
"points": "Punkte",
"privacy_policy": "Datenschutzerklärung", "privacy_policy": "Datenschutzerklärung",
"quick_create": "Schnellzugriff", "quick_create": "Schnellzugriff",
"recent_matches": "Letzte Spiele", "recent_matches": "Letzte Spiele",
@@ -89,14 +87,12 @@
"save_changes": "Änderungen speichern", "save_changes": "Änderungen speichern",
"search_for_groups": "Nach Gruppen suchen", "search_for_groups": "Nach Gruppen suchen",
"search_for_players": "Nach Spieler:innen suchen", "search_for_players": "Nach Spieler:innen suchen",
"select_winner": "Gewinner:in wählen", "select_winner": "Gewinner:in wählen:",
"select_loser": "Verlierer:in wählen",
"selected_players": "Ausgewählte Spieler:innen", "selected_players": "Ausgewählte Spieler:innen",
"settings": "Einstellungen", "settings": "Einstellungen",
"single_loser": "Ein:e Verlierer:in", "single_loser": "Ein:e Verlierer:in",
"single_winner": "Ein:e Gewinner:in", "single_winner": "Ein:e Gewinner:in",
"highest_score": "Höchste Punkte", "highest_score": "Höchste Punkte",
"loser": "Verlierer:in",
"lowest_score": "Niedrigste Punkte", "lowest_score": "Niedrigste Punkte",
"multiple_winners": "Mehrere Gewinner:innen", "multiple_winners": "Mehrere Gewinner:innen",
"statistics": "Statistiken", "statistics": "Statistiken",

View File

@@ -83,9 +83,6 @@
"@edit_match": { "@edit_match": {
"description": "Button & Appbar label for editing a match" "description": "Button & Appbar label for editing a match"
}, },
"@enter_points": {
"description": "Label to enter players points"
},
"@enter_results": { "@enter_results": {
"description": "Button text to enter match results" "description": "Button text to enter match results"
}, },
@@ -235,9 +232,6 @@
} }
} }
}, },
"@points": {
"description": "Points label"
},
"@privacy_policy": { "@privacy_policy": {
"description": "Privacy policy menu item" "description": "Privacy policy menu item"
}, },
@@ -277,9 +271,6 @@
"@select_winner": { "@select_winner": {
"description": "Label to select the winner" "description": "Label to select the winner"
}, },
"@select_loser": {
"description": "Label to select the loser"
},
"@selected_players": { "@selected_players": {
"description": "Shows the number of selected players" "description": "Shows the number of selected players"
}, },
@@ -360,7 +351,6 @@
"delete_match": "Delete Match", "delete_match": "Delete Match",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"edit_match": "Edit Match", "edit_match": "Edit Match",
"enter_points": "Enter points",
"enter_results": "Enter Results", "enter_results": "Enter Results",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
"error_deleting_group": "Error while deleting group, please try again", "error_deleting_group": "Error while deleting group, please try again",
@@ -409,7 +399,6 @@
"player_name": "Player name", "player_name": "Player name",
"players": "Players", "players": "Players",
"players_count": "{count} Players", "players_count": "{count} Players",
"points": "Points",
"privacy_policy": "Privacy Policy", "privacy_policy": "Privacy Policy",
"quick_create": "Quick Create", "quick_create": "Quick Create",
"recent_matches": "Recent Matches", "recent_matches": "Recent Matches",
@@ -422,14 +411,12 @@
"save_changes": "Save Changes", "save_changes": "Save Changes",
"search_for_groups": "Search for groups", "search_for_groups": "Search for groups",
"search_for_players": "Search for players", "search_for_players": "Search for players",
"select_winner": "Select Winner", "select_winner": "Select Winner:",
"select_loser": "Select Loser",
"selected_players": "Selected players", "selected_players": "Selected players",
"settings": "Settings", "settings": "Settings",
"single_loser": "Single Loser", "single_loser": "Single Loser",
"single_winner": "Single Winner", "single_winner": "Single Winner",
"highest_score": "Highest Score", "highest_score": "Highest Score",
"loser": "Loser",
"lowest_score": "Lowest Score", "lowest_score": "Lowest Score",
"multiple_winners": "Multiple Winners", "multiple_winners": "Multiple Winners",
"statistics": "Statistics", "statistics": "Statistics",

View File

@@ -254,12 +254,6 @@ abstract class AppLocalizations {
/// **'Edit Match'** /// **'Edit Match'**
String get edit_match; String get edit_match;
/// Label to enter players points
///
/// In en, this message translates to:
/// **'Enter points'**
String get enter_points;
/// Button text to enter match results /// Button text to enter match results
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -548,12 +542,6 @@ abstract class AppLocalizations {
/// **'{count} Players'** /// **'{count} Players'**
String players_count(int count); String players_count(int count);
/// Points label
///
/// In en, this message translates to:
/// **'Points'**
String get points;
/// Privacy policy menu item /// Privacy policy menu item
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -629,15 +617,9 @@ abstract class AppLocalizations {
/// Label to select the winner /// Label to select the winner
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Select Winner'** /// **'Select Winner:'**
String get select_winner; String get select_winner;
/// Label to select the loser
///
/// In en, this message translates to:
/// **'Select Loser'**
String get select_loser;
/// Shows the number of selected players /// Shows the number of selected players
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -668,12 +650,6 @@ abstract class AppLocalizations {
/// **'Highest Score'** /// **'Highest Score'**
String get highest_score; String get highest_score;
/// No description provided for @loser.
///
/// In en, this message translates to:
/// **'Loser'**
String get loser;
/// No description provided for @lowest_score. /// No description provided for @lowest_score.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -79,7 +79,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete_all_data => 'Alle Daten löschen'; String get delete_all_data => 'Alle Daten löschen';
@override @override
String get delete_group => 'Gruppe löschen'; String get delete_group => 'Diese Gruppe löschen';
@override @override
String get delete_match => 'Spiel löschen'; String get delete_match => 'Spiel löschen';
@@ -90,9 +90,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get edit_match => 'Gruppe bearbeiten'; String get edit_match => 'Gruppe bearbeiten';
@override
String get enter_points => 'Punkte eingeben';
@override @override
String get enter_results => 'Ergebnisse eintragen'; String get enter_results => 'Ergebnisse eintragen';
@@ -243,9 +240,6 @@ class AppLocalizationsDe extends AppLocalizations {
return '$count Spieler'; return '$count Spieler';
} }
@override
String get points => 'Punkte';
@override @override
String get privacy_policy => 'Datenschutzerklärung'; String get privacy_policy => 'Datenschutzerklärung';
@@ -287,10 +281,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get search_for_players => 'Nach Spieler:innen suchen'; String get search_for_players => 'Nach Spieler:innen suchen';
@override @override
String get select_winner => 'Gewinner:in wählen'; String get select_winner => 'Gewinner:in wählen:';
@override
String get select_loser => 'Verlierer:in wählen';
@override @override
String get selected_players => 'Ausgewählte Spieler:innen'; String get selected_players => 'Ausgewählte Spieler:innen';
@@ -307,9 +298,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get highest_score => 'Höchste Punkte'; String get highest_score => 'Höchste Punkte';
@override
String get loser => 'Verlierer:in';
@override @override
String get lowest_score => 'Niedrigste Punkte'; String get lowest_score => 'Niedrigste Punkte';

View File

@@ -90,9 +90,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get edit_match => 'Edit Match'; String get edit_match => 'Edit Match';
@override
String get enter_points => 'Enter points';
@override @override
String get enter_results => 'Enter Results'; String get enter_results => 'Enter Results';
@@ -243,9 +240,6 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count Players'; return '$count Players';
} }
@override
String get points => 'Points';
@override @override
String get privacy_policy => 'Privacy Policy'; String get privacy_policy => 'Privacy Policy';
@@ -287,10 +281,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get search_for_players => 'Search for players'; String get search_for_players => 'Search for players';
@override @override
String get select_winner => 'Select Winner'; String get select_winner => 'Select Winner:';
@override
String get select_loser => 'Select Loser';
@override @override
String get selected_players => 'Selected players'; String get selected_players => 'Selected players';
@@ -307,9 +298,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get highest_score => 'Highest Score'; String get highest_score => 'Highest Score';
@override
String get loser => 'Loser';
@override @override
String get lowest_score => 'Lowest Score'; String get lowest_score => 'Lowest Score';

View File

@@ -3,6 +3,7 @@ import 'package:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart'; import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/group.dart'; import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
@@ -10,10 +11,10 @@ import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:tallee/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart'; import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.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/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -70,23 +71,16 @@ class _GroupDetailViewState extends State<GroupDetailView> {
context: context, context: context,
builder: (context) => CustomAlertDialog( builder: (context) => CustomAlertDialog(
title: '${loc.delete_group}?', title: '${loc.delete_group}?',
content: loc.this_cannot_be_undone, content: Text(loc.this_cannot_be_undone),
actions: [ actions: [
AnimatedDialogButton( CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(color: CustomTheme.textColor),
),
),
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text( text: loc.delete,
loc.delete, ),
style: const TextStyle( CustomDialogAction(
color: CustomTheme.secondaryColor, onPressed: () => Navigator.of(context).pop(false),
), buttonType: ButtonType.secondary,
), text: loc.cancel,
), ),
], ],
), ),

View File

@@ -10,10 +10,10 @@ import 'package:tallee/data/models/match.dart';
import 'package:tallee/l10n/generated/app_localizations.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/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart'; import 'package:tallee/presentation/widgets/buttons/main_menu_button.dart';
import 'package:tallee/presentation/widgets/colored_icon_container.dart'; import 'package:tallee/presentation/widgets/colored_icon_container.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.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/tiles/info_tile.dart'; import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:tallee/presentation/widgets/tiles/text_icon_tile.dart';
@@ -65,23 +65,16 @@ class _MatchDetailViewState extends State<MatchDetailView> {
context: context, context: context,
builder: (context) => CustomAlertDialog( builder: (context) => CustomAlertDialog(
title: '${loc.delete_match}?', title: '${loc.delete_match}?',
content: loc.this_cannot_be_undone, content: Text(loc.this_cannot_be_undone),
actions: [ actions: [
AnimatedDialogButton( CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(color: CustomTheme.textColor),
),
),
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text( text: loc.delete,
loc.delete, ),
style: const TextStyle( CustomDialogAction(
color: CustomTheme.secondaryColor, onPressed: () => Navigator.of(context).pop(false),
), buttonType: ButtonType.secondary,
), text: loc.cancel,
), ),
], ],
), ),
@@ -176,7 +169,37 @@ class _MatchDetailViewState extends State<MatchDetailView> {
vertical: 4, vertical: 4,
horizontal: 8, horizontal: 8,
), ),
child: getResultWidget(loc), child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
/// TODO: Implement different ruleset results display
if (match.winner != null) ...[
Text(
loc.winner,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
match.winner!.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
] else ...[
Text(
loc.no_results_entered_yet,
style: const TextStyle(
fontSize: 14,
color: CustomTheme.textColor,
),
),
],
],
),
), ),
), ),
], ],
@@ -235,91 +258,4 @@ class _MatchDetailViewState extends State<MatchDetailView> {
}); });
widget.onMatchUpdate.call(); widget.onMatchUpdate.call();
} }
/// Returns the widget to be displayed in the result [InfoTile]
/// TODO: Update when score logic is overhauled
Widget getResultWidget(AppLocalizations loc) {
if (isSingleRowResult()) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: getResultRow(loc),
);
} else {
return Column(
children: [
for (var player in match.players)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
player.name,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
Text(
'0 ${loc.points}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
],
),
],
);
}
}
/// Returns the result row for single winner/loser rulesets or a placeholder
/// if no result is entered yet
/// TODO: Update when score logic is overhauled
List<Widget> getResultRow(AppLocalizations loc) {
if (match.winner != null && match.game.ruleset == Ruleset.singleWinner) {
return [
Text(
loc.winner,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.winner!.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
];
} else if (match.game.ruleset == Ruleset.singleLoser) {
return [
Text(
loc.loser,
style: const TextStyle(fontSize: 16, color: CustomTheme.textColor),
),
Text(
match.winner!.name,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CustomTheme.primaryColor,
),
),
];
} else {
return [
Text(
loc.no_results_entered_yet,
style: const TextStyle(fontSize: 14, color: CustomTheme.textColor),
),
];
}
}
// Returns if the result can be displayed in a single row
bool isSingleRowResult() {
return match.game.ruleset == Ruleset.singleWinner ||
match.game.ruleset == Ruleset.singleLoser;
}
} }

View File

@@ -1,33 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart'; import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/match.dart'; import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart'; import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/buttons/custom_width_button.dart';
import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/custom_radio_list_tile.dart';
import 'package:tallee/presentation/widgets/tiles/score_list_tile.dart';
class MatchResultView extends StatefulWidget { class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match /// A view that allows selecting and saving the winner of a match
/// [match]: The match for which the winner is to be selected /// [match]: The match for which the winner is to be selected
/// [onWinnerChanged]: Optional callback invoked when the winner is changed /// [onWinnerChanged]: Optional callback invoked when the winner is changed
const MatchResultView({ const MatchResultView({super.key, required this.match, this.onWinnerChanged});
super.key,
required this.match,
this.ruleset = Ruleset.singleWinner,
this.onWinnerChanged,
});
/// The match for which the winner is to be selected /// The match for which the winner is to be selected
final Match match; final Match match;
/// The ruleset of the match, determines how the winner is selected or how
/// scores are entered
final Ruleset ruleset;
/// Optional callback invoked when the winner is changed /// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
@@ -41,9 +29,6 @@ class _MatchResultViewState extends State<MatchResultView> {
/// List of all players who participated in the match /// List of all players who participated in the match
late final List<Player> allPlayers; late final List<Player> allPlayers;
/// List of text controllers for score entry, one for each player
late final List<TextEditingController> controller;
/// Currently selected winner player /// Currently selected winner player
Player? _selectedPlayer; Player? _selectedPlayer;
@@ -54,19 +39,10 @@ class _MatchResultViewState extends State<MatchResultView> {
allPlayers = widget.match.players; allPlayers = widget.match.players;
allPlayers.sort((a, b) => a.name.compareTo(b.name)); allPlayers.sort((a, b) => a.name.compareTo(b.name));
controller = List.generate(
allPlayers.length,
(index) => TextEditingController(),
);
if (widget.match.winner != null) { if (widget.match.winner != null) {
if (rulesetSupportsWinnerSelection()) { _selectedPlayer = allPlayers.firstWhere(
_selectedPlayer = allPlayers.firstWhere( (p) => p.id == widget.match.winner!.id,
(p) => p.id == widget.match.winner!.id, );
);
} else if (rulesetSupportsScoreEntry()) {
/// TODO: Update when score logic is overhauled
}
} }
super.initState(); super.initState();
} }
@@ -74,7 +50,6 @@ class _MatchResultViewState extends State<MatchResultView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar( appBar: AppBar(
@@ -110,77 +85,50 @@ class _MatchResultViewState extends State<MatchResultView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
'${getTitleForRuleset(loc)}:', loc.select_winner,
style: const TextStyle( style: const TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
if (rulesetSupportsWinnerSelection()) Expanded(
Expanded( child: RadioGroup<Player>(
child: RadioGroup<Player>( groupValue: _selectedPlayer,
groupValue: _selectedPlayer, onChanged: (Player? value) async {
onChanged: (Player? value) async { setState(() {
setState(() { _selectedPlayer = value;
_selectedPlayer = value; });
}); await _handleWinnerSaving();
}, },
child: ListView.builder( child: ListView.builder(
itemCount: allPlayers.length,
itemBuilder: (context, index) {
return CustomRadioListTile(
text: allPlayers[index].name,
value: allPlayers[index],
onContainerTap: (value) async {
setState(() {
// Check if the already selected player is the same as the newly tapped player.
if (_selectedPlayer == value) {
// If yes deselected the player by setting it to null.
_selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
},
);
},
),
),
),
if (rulesetSupportsScoreEntry())
Expanded(
child: ListView.separated(
itemCount: allPlayers.length, itemCount: allPlayers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
print(allPlayers[index].name); return CustomRadioListTile(
return ScoreListTile(
text: allPlayers[index].name, text: allPlayers[index].name,
controller: controller[index], value: allPlayers[index],
); onContainerTap: (value) async {
}, setState(() {
separatorBuilder: (BuildContext context, int index) { // Check if the already selected player is the same as the newly tapped player.
return const Padding( if (_selectedPlayer == value) {
padding: EdgeInsets.symmetric(vertical: 8.0), // If yes deselected the player by setting it to null.
child: Divider(indent: 20), _selectedPlayer = null;
} else {
// If no assign the newly tapped player to the selected player.
(_selectedPlayer = value);
}
});
await _handleWinnerSaving();
},
); );
}, },
), ),
), ),
),
], ],
), ),
), ),
), ),
CustomWidthButton(
text: loc.save_changes,
sizeRelativeToWidth: 0.95,
onPressed: () async {
await _handleSaving();
if (!context.mounted) return;
Navigator.of(context).pop(_selectedPlayer);
},
),
], ],
), ),
), ),
@@ -189,20 +137,7 @@ class _MatchResultViewState extends State<MatchResultView> {
/// Handles saving or removing the winner in the database /// Handles saving or removing the winner in the database
/// based on the current selection. /// based on the current selection.
Future<void> _handleSaving() async { Future<void> _handleWinnerSaving() async {
if (widget.ruleset == Ruleset.singleWinner) {
await _handleWinner();
} else if (widget.ruleset == Ruleset.singleLoser) {
await _handleLoser();
} else if (widget.ruleset == Ruleset.lowestScore ||
widget.ruleset == Ruleset.highestScore) {
await _handleScores();
}
widget.onWinnerChanged?.call();
}
Future<bool> _handleWinner() async {
if (_selectedPlayer == null) { if (_selectedPlayer == null) {
await db.scoreEntryDao.removeWinner(matchId: widget.match.id); await db.scoreEntryDao.removeWinner(matchId: widget.match.id);
} else { } else {
@@ -211,53 +146,6 @@ class _MatchResultViewState extends State<MatchResultView> {
playerId: _selectedPlayer!.id, playerId: _selectedPlayer!.id,
); );
} }
} widget.onWinnerChanged?.call();
Future<bool> _handleLoser() async {
if (_selectedPlayer == null) {
/// TODO: Update when score logic is overhauled
return false;
} else {
/// TODO: Update when score logic is overhauled
return false;
}
}
/// Handles saving the scores for each player in the database.
Future<bool> _handleScores() async {
for (int i = 0; i < allPlayers.length; i++) {
var text = controller[i].text;
if (text.isEmpty) {
text = '0';
}
final score = int.parse(text);
await db.playerMatchDao.updatePlayerScore(
matchId: widget.match.id,
playerId: allPlayers[i].id,
newScore: score,
);
}
return false;
}
String getTitleForRuleset(AppLocalizations loc) {
switch (widget.ruleset) {
case Ruleset.singleWinner:
return loc.select_winner;
case Ruleset.singleLoser:
return loc.select_loser;
default:
return loc.enter_points;
}
}
bool rulesetSupportsWinnerSelection() {
return widget.ruleset == Ruleset.singleWinner ||
widget.ruleset == Ruleset.singleLoser;
}
bool rulesetSupportsScoreEntry() {
return widget.ruleset == Ruleset.lowestScore ||
widget.ruleset == Ruleset.highestScore;
} }
} }

View File

@@ -9,8 +9,8 @@ import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/core/enums.dart'; import 'package:tallee/core/enums.dart';
import 'package:tallee/l10n/generated/app_localizations.dart'; import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart'; import 'package:tallee/presentation/views/main_menu/settings_view/licenses_view.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart'; import 'package:tallee/presentation/widgets/dialog/custom_alert_dialog.dart';
import 'package:tallee/presentation/widgets/custom_alert_dialog.dart'; import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:tallee/presentation/widgets/tiles/settings_list_tile.dart';
import 'package:tallee/services/data_transfer_service.dart'; import 'package:tallee/services/data_transfer_service.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -122,25 +122,16 @@ class _SettingsViewState extends State<SettingsView> {
context: context, context: context,
builder: (context) => CustomAlertDialog( builder: (context) => CustomAlertDialog(
title: '${loc.delete_all_data}?', title: '${loc.delete_all_data}?',
content: loc.this_cannot_be_undone, content: Text(loc.this_cannot_be_undone),
actions: [ actions: [
AnimatedDialogButton( CustomDialogAction(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(
color: CustomTheme.textColor,
),
),
),
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text( text: loc.delete,
loc.delete, ),
style: const TextStyle( CustomDialogAction(
color: CustomTheme.secondaryColor, onPressed: () => Navigator.of(context).pop(false),
), buttonType: ButtonType.secondary,
), text: loc.cancel,
), ),
], ],
), ),

View File

@@ -1,22 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/enums.dart';
class AnimatedDialogButton extends StatefulWidget { class AnimatedDialogButton extends StatefulWidget {
/// A custom animated button widget that provides a scaling and opacity effect /// A custom animated button widget that provides a scaling and opacity effect
/// when pressed. /// when pressed.
/// - [onPressed]: Callback function that is triggered when the button is pressed. /// - [onPressed]: Callback function that is triggered when the button is pressed.
/// - [child]: The child widget to be displayed inside the button, typically a text or icon. /// - [buttonText]: The text to be displayed on the button.
/// - [buttonType]: The type of the button, which determines its styling.
/// - [buttonConstraints]: Optional constraints to control the button's size.
const AnimatedDialogButton({ const AnimatedDialogButton({
super.key, super.key,
required this.buttonText,
required this.onPressed, required this.onPressed,
required this.child, this.buttonConstraints,
this.buttonType = ButtonType.primary,
}); });
/// Callback function that is triggered when the button is pressed. final String buttonText;
final VoidCallback onPressed; final VoidCallback onPressed;
/// The child widget to be displayed inside the button, typically a text or icon. final BoxConstraints? buttonConstraints;
final Widget child;
final ButtonType buttonType;
@override @override
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState(); State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
@@ -27,6 +33,29 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textStyling = TextStyle(
color: widget.buttonType == ButtonType.primary
? Colors.black
: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
);
final buttonDecoration = widget.buttonType == ButtonType.primary
// Primary
? BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
)
: widget.buttonType == ButtonType.secondary
// Secondary
? BoxDecoration(
border: BoxBorder.all(color: Colors.white, width: 2),
borderRadius: BorderRadius.circular(12),
)
// Tertiary
: const BoxDecoration();
return GestureDetector( return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true), onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false), onTapUp: (_) => setState(() => _isPressed = false),
@@ -38,10 +67,18 @@ class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: _isPressed ? 0.6 : 1.0, opacity: _isPressed ? 0.6 : 1.0,
duration: const Duration(milliseconds: 100), duration: const Duration(milliseconds: 100),
child: Container( child: Center(
decoration: CustomTheme.standardBoxDecoration, child: Container(
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 6), constraints: widget.buttonConstraints,
child: widget.child, decoration: buttonDecoration,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
margin: const EdgeInsets.symmetric(vertical: 8),
child: Text(
widget.buttonText,
style: textStyling,
textAlign: TextAlign.center,
),
),
), ),
), ),
), ),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tallee/core/custom_theme.dart'; import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/presentation/widgets/dialog/custom_dialog_action.dart';
class CustomAlertDialog extends StatelessWidget { class CustomAlertDialog extends StatelessWidget {
/// A custom alert dialog widget that provides a os unspecific AlertDialog, /// A custom alert dialog widget that provides a os unspecific AlertDialog,
@@ -16,20 +17,23 @@ class CustomAlertDialog extends StatelessWidget {
}); });
final String title; final String title;
final String content; final Widget content;
final List<Widget> actions; final List<CustomDialogAction> actions;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: Text(title, style: const TextStyle(color: CustomTheme.textColor)), title: Text(
content: Text( title,
content, style: const TextStyle(
style: const TextStyle(color: CustomTheme.textColor), fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
), ),
content: content,
actions: actions, actions: actions,
backgroundColor: CustomTheme.boxColor, backgroundColor: CustomTheme.boxColor,
actionsAlignment: MainAxisAlignment.spaceAround, actionsAlignment: MainAxisAlignment.center,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: CustomTheme.standardBorderRadiusAll, borderRadius: CustomTheme.standardBorderRadiusAll,
side: const BorderSide(color: CustomTheme.boxBorderColor), side: const BorderSide(color: CustomTheme.boxBorderColor),

View File

@@ -0,0 +1,32 @@
import 'package:flutter/cupertino.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/presentation/widgets/buttons/animated_dialog_button.dart';
class CustomDialogAction extends StatelessWidget {
/// A custom dialog action widget that represents a button in a dialog.
/// - [text]: The text to be displayed on the button.
/// - [buttonType]: The type of the button, which determines its styling.
/// - [onPressed]: Callback function that is triggered when the button is pressed.
const CustomDialogAction({
super.key,
required this.onPressed,
required this.text,
this.buttonType = ButtonType.primary,
});
final String text;
final ButtonType buttonType;
final VoidCallback onPressed;
@override
Widget build(BuildContext context) {
return AnimatedDialogButton(
onPressed: onPressed,
buttonText: text,
buttonType: buttonType,
buttonConstraints: const BoxConstraints(minWidth: 300),
);
}
}

View File

@@ -79,24 +79,6 @@ class _MatchTileState extends State<MatchTile> {
], ],
), ),
const SizedBox(height: 4),
Container(
decoration: BoxDecoration(
color: CustomTheme.primaryColor,
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Text(
translateRulesetToString(match.game.ruleset, context),
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
if (group != null) ...[ if (group != null) ...[

View File

@@ -1,91 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
class ScoreListTile extends StatelessWidget {
/// A custom list tile widget that has a text field for inputting a score.
/// - [text]: The leading text to be displayed.
/// - [controller]: The controller for the text field to input the score.
const ScoreListTile({
super.key,
required this.text,
required this.controller,
/*
required this.onContainerTap,
*/
});
/// The text to display next to the radio button.
final String text;
final TextEditingController controller;
/// The callback invoked when the container is tapped.
/*
final ValueChanged<T> onContainerTap;
*/
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
padding: const EdgeInsets.symmetric(horizontal: 20),
decoration: const BoxDecoration(color: CustomTheme.boxColor),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
text,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w500),
),
SizedBox(
width: 100,
height: 40,
child: TextField(
controller: controller,
keyboardType: TextInputType.number,
maxLength: 4,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
cursorColor: CustomTheme.textColor,
decoration: InputDecoration(
hintText: loc.points,
counterText: '',
filled: true,
fillColor: CustomTheme.onBoxColor,
contentPadding: const EdgeInsets.symmetric(
horizontal: 0,
vertical: 0,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(
color: CustomTheme.textColor.withAlpha(100),
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: CustomTheme.primaryColor,
width: 2,
),
),
),
),
),
],
),
);
}
}