38 Commits

Author SHA1 Message Date
58d8d07b63 Merge remote-tracking branch 'origin/development' into feature/119-implementierung-der-games
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m41s
Pull Request Pipeline / lint (pull_request) Successful in 2m42s
# Conflicts:
#	lib/presentation/widgets/text_input/text_input_field.dart
2026-01-18 14:56:38 +01:00
c983ca22dd Merge remote-tracking branch 'origin/development' into feature/119-implementierung-der-games
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m33s
2026-01-18 14:55:56 +01:00
45b11359b3 Merge pull request 'Maximale Input in Textfelder gefixtx' (#176) from hotifx/fix-input-length into development
Reviewed-on: #176
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2026-01-18 13:49:53 +00:00
57fb8dbcc8 Merge branch 'development' into hotifx/fix-input-length
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2026-01-18 13:46:25 +00:00
7024699a61 implement create game view
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m53s
Pull Request Pipeline / lint (pull_request) Successful in 3m3s
2026-01-18 14:38:27 +01:00
5a30538aa5 Merge pull request 'Text Overflows durch Tastatur' (#173) from bug/162-text-overflows-durch-tastatur into development
Reviewed-on: #173
2026-01-18 11:23:04 +00:00
1e18105ce0 Merge remote-tracking branch 'origin/development' into bug/162-text-overflows-durch-tastatur
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
# Conflicts:
#	pubspec.yaml
2026-01-18 11:45:50 +01:00
9ce2ca0ceb Merge pull request 'Match braucht Game' (#174) from enhancement/103-match-braucht-game into development
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m15s
Pull Request Pipeline / lint (pull_request) Successful in 2m15s
Reviewed-on: #174
2026-01-17 22:03:55 +00:00
39e6e485ac remove newline
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-17 22:54:22 +01:00
e9633a898c bump version 2026-01-17 22:54:18 +01:00
94c3bad02b Removed counter
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m11s
2026-01-17 22:41:02 +01:00
ed2d672dee Implemented maxLength attribute 2026-01-17 22:40:56 +01:00
449639df0e Merge remote-tracking branch 'origin/development' into enhancement/103-match-braucht-game
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
# Conflicts:
#	pubspec.yaml
2026-01-17 21:35:42 +01:00
a713ccb59c Merge pull request 'Gruppenprofile implementieren' (#172) from feature/169-gruppenprofile-implementieren into development
Reviewed-on: #172
2026-01-17 20:34:26 +00:00
f5924c4758 adjusted spacing & changed runAlignment to alignment
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m8s
Pull Request Pipeline / lint (pull_request) Successful in 2m20s
2026-01-17 21:27:22 +01:00
5c9f44e947 bump build nr
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-17 21:12:53 +01:00
5f987806d6 remove ruleset 2026-01-17 21:12:43 +01:00
92c62000af remove margin from title_description_list_tile.dart 2026-01-17 21:12:17 +01:00
babe74f2be Merge remote-tracking branch 'origin/feature/169-gruppenprofile-implementieren' into feature/169-gruppenprofile-implementieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m15s
Pull Request Pipeline / lint (pull_request) Successful in 2m16s
2026-01-17 15:21:13 +01:00
8a38b9c3ea Updated dateformat to localize 2026-01-17 15:18:56 +01:00
ddd0a5d8bd Updated dateformat to localize
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m11s
Pull Request Pipeline / lint (pull_request) Successful in 2m12s
2026-01-17 15:17:33 +01:00
be9d1b52d2 Merge branch 'development' into feature/169-gruppenprofile-implementieren
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m19s
Pull Request Pipeline / lint (pull_request) Successful in 2m21s
# Conflicts:
#	pubspec.yaml
2026-01-17 14:56:26 +01:00
514e0f8064 Changed date format
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m21s
Pull Request Pipeline / lint (pull_request) Successful in 2m26s
2026-01-17 14:54:06 +01:00
ff47ef38c1 Changed alignment 2026-01-17 14:51:12 +01:00
fa2706395c Merge remote-tracking branch 'origin/development' into bug/162-text-overflows-durch-tastatur
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
# Conflicts:
#	pubspec.yaml
2026-01-17 14:44:49 +01:00
2d1ac3a17c Merge pull request 'Konstanten für Länge von Namen' (#170) from enhancement/158-konstanten-für-länge-von-namen into development
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
Reviewed-on: #170
Reviewed-by: Mathis Kirchner <mathis.kirchner.mk@gmail.com>
2026-01-17 13:42:50 +00:00
4fd8d2129b Merge remote-tracking branch 'origin/enhancement/158-konstanten-für-länge-von-namen' into enhancement/158-konstanten-für-länge-von-namen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m14s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2026-01-17 14:35:44 +01:00
db41f40a52 Edited comment 2026-01-17 14:35:21 +01:00
0e747710ab Edited comment
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m5s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2026-01-17 14:33:24 +01:00
9f44f02a35 Changed length to 32 2026-01-17 14:32:25 +01:00
3addaa0f9d Disable resizing when using keyboard
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m9s
2026-01-17 14:22:45 +01:00
abb0fcbbd6 Updated file name
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2026-01-17 00:57:28 +01:00
fc6eb2b9cf Added comments
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m7s
Pull Request Pipeline / lint (pull_request) Successful in 2m10s
2026-01-17 00:52:13 +01:00
6a0896d818 Implemented ColoredIconContainer 2026-01-17 00:52:09 +01:00
a8129eb134 Implemented first version of group profile 2026-01-16 23:34:47 +01:00
783f772da1 Implemented constants
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m41s
Pull Request Pipeline / lint (pull_request) Successful in 2m45s
2026-01-16 21:52:58 +01:00
b7930d5e2e Adjusted constant name 2026-01-16 21:41:42 +01:00
1b709707b5 Added constants 2026-01-16 21:41:20 +01:00
34 changed files with 896 additions and 430 deletions

View File

@@ -10,4 +10,5 @@ linter:
prefer_const_declarations: true prefer_const_declarations: true
prefer_const_literals_to_create_immutables: true prefer_const_literals_to_create_immutables: true
unnecessary_const: true unnecessary_const: true
lines_longer_than_80_chars: false lines_longer_than_80_chars: false
constant_identifier_names: false

View File

@@ -3,5 +3,23 @@ class Constants {
Constants._(); // Private constructor to prevent instantiation Constants._(); // Private constructor to prevent instantiation
/// Minimum duration of all app skeletons /// Minimum duration of all app skeletons
static Duration minimumSkeletonDuration = const Duration(milliseconds: 250); static const Duration MINIMUM_SKELETON_DURATION = Duration(milliseconds: 250);
/// Maximum length for player names
static const int MAX_PLAYER_NAME_LENGTH = 32;
/// Maximum length for group names
static const int MAX_GROUP_NAME_LENGTH = 32;
/// Maximum length for match names
static const int MAX_MATCH_NAME_LENGTH = 32;
/// Maximum length for game names
static const int MAX_GAME_NAME_LENGTH = 32;
/// Maximum length for team names
static const int MAX_TEAM_NAME_LENGTH = 32;
/// Maximum length for game descriptions
static const int MAX_GAME_DESCRIPTION_LENGTH = 256;
} }

View File

@@ -5,14 +5,32 @@ class CustomTheme {
CustomTheme._(); // Private constructor to prevent instantiation CustomTheme._(); // Private constructor to prevent instantiation
// ==================== Colors ==================== // ==================== Colors ====================
/// Primary color of the app theme
static Color primaryColor = const Color(0xFF7505E4); static Color primaryColor = const Color(0xFF7505E4);
/// Secondary color of the app theme
static Color secondaryColor = const Color(0xFFAFA2FF); static Color secondaryColor = const Color(0xFFAFA2FF);
/// Background color of the app theme
static Color backgroundColor = const Color(0xFF0B0B0B); static Color backgroundColor = const Color(0xFF0B0B0B);
/// Default color for boxes and containers
static Color boxColor = const Color(0xFF101010); static Color boxColor = const Color(0xFF101010);
static Color onBoxColor = const Color(0xFF181818);
/// Default border color for boxes and containers
static Color boxBorder = const Color(0xFF272727); static Color boxBorder = const Color(0xFF272727);
/// Color for boxes and containers displayed on boxes
static Color onBoxColor = const Color(0xFF181818);
/// Text color used throughout the app
static const Color textColor = Colors.white; static const Color textColor = Colors.white;
/// Selected color for the [NavbarItem]
static Color navBarItemSelectedColor = primaryColor.withGreen(100); static Color navBarItemSelectedColor = primaryColor.withGreen(100);
/// Unselected color for the [NavbarItem]
static Color navBarItemUnselectedColor = Colors.grey.shade400; static Color navBarItemUnselectedColor = Colors.grey.shade400;
// ==================== Border Radius ==================== // ==================== Border Radius ====================

23
lib/data/dto/game.dart Normal file
View File

@@ -0,0 +1,23 @@
import 'package:clock/clock.dart';
import 'package:uuid/uuid.dart';
class Game {
final String id;
final DateTime createdAt;
final String name;
final String? ruleset;
final String? description;
final int? color;
final String? icon;
Game({
String? id,
DateTime? createdAt,
required this.name,
this.ruleset,
this.description,
this.color,
this.icon,
}) : id = id ?? const Uuid().v4(),
createdAt = createdAt ?? clock.now();
}

View File

@@ -4,15 +4,18 @@
"all_players_selected": "Alle Spieler:innen ausgewählt", "all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Spiele", "amount_of_matches": "Anzahl der Spiele",
"app_name": "Game Tracker", "app_name": "Game Tracker",
"best_player": "Beste:r Spieler:in",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
"choose_ruleset": "Regelwerk wählen", "choose_ruleset": "Regelwerk wählen",
"could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden", "could_not_add_player": "Spieler:in {playerName} konnte nicht hinzugefügt werden",
"create_game": "Spielvorlage erstellen",
"create_group": "Gruppe erstellen", "create_group": "Gruppe erstellen",
"create_match": "Spiel erstellen", "create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen", "create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel erstellen", "create_new_match": "Neues Spiel erstellen",
"created_on": "Erstellt am",
"data": "Daten", "data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht", "data_successfully_deleted": "Daten erfolgreich gelöscht",
"data_successfully_exported": "Daten erfolgreich exportiert", "data_successfully_exported": "Daten erfolgreich exportiert",
@@ -20,11 +23,12 @@
"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": "Diese Gruppe löschen", "delete_game": "Spielvorlage löschen",
"delete_group": "Gruppe löschen",
"description": "Beschreibung",
"edit_game": "Spielvorlage bearbeiten",
"edit_group": "Gruppe bearbeiten", "edit_group": "Gruppe bearbeiten",
"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_editing_group": "Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen",
"error_reading_file": "Fehler beim Lesen der Datei", "error_reading_file": "Fehler beim Lesen der Datei",
"export_canceled": "Export abgebrochen", "export_canceled": "Export abgebrochen",
"export_data": "Daten exportieren", "export_data": "Daten exportieren",
@@ -33,6 +37,7 @@
"game_name": "Spielvorlagenname", "game_name": "Spielvorlagenname",
"group": "Gruppe", "group": "Gruppe",
"group_name": "Gruppenname", "group_name": "Gruppenname",
"group_profile": "Gruppenprofil",
"groups": "Gruppen", "groups": "Gruppen",
"home": "Startseite", "home": "Startseite",
"import_canceled": "Import abgebrochen", "import_canceled": "Import abgebrochen",
@@ -46,6 +51,7 @@
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel", "match_name": "Spieltitel",
"matches": "Spiele", "matches": "Spiele",
"members": "Mitglieder",
"most_points": "Höchste Punkte", "most_points": "Höchste Punkte",
"no_data_available": "Keine Daten verfügbar", "no_data_available": "Keine Daten verfügbar",
"no_groups_created_yet": "Noch keine Gruppen erstellt", "no_groups_created_yet": "Noch keine Gruppen erstellt",
@@ -61,6 +67,7 @@
"none": "Kein", "none": "Kein",
"none_group": "Keine", "none_group": "Keine",
"not_available": "Nicht verfügbar", "not_available": "Nicht verfügbar",
"played_matches": "Gespielte Spiele",
"player_name": "Spieler:innenname", "player_name": "Spieler:innenname",
"players": "Spieler:innen", "players": "Spieler:innen",
"players_count": "{count} Spieler", "players_count": "{count} Spieler",

View File

@@ -12,6 +12,9 @@
"@app_name": { "@app_name": {
"description": "The name of the App" "description": "The name of the App"
}, },
"@best_player": {
"description": "Label for best player statistic"
},
"@cancel": { "@cancel": {
"description": "Cancel button text" "description": "Cancel button text"
}, },
@@ -27,6 +30,9 @@
"@could_not_add_player": { "@could_not_add_player": {
"description": "Error message when adding a player fails" "description": "Error message when adding a player fails"
}, },
"@create_game": {
"description": "Button text to create a game"
},
"@create_group": { "@create_group": {
"description": "Button text to create a group" "description": "Button text to create a group"
}, },
@@ -34,10 +40,13 @@
"description": "Button text to create a match" "description": "Button text to create a match"
}, },
"@create_new_group": { "@create_new_group": {
"description": "Appbar text to create a new group" "description": "Button text to create a new group"
}, },
"@create_new_match": { "@create_new_match": {
"description": "Appbar text to create a new match" "description": "Button text to create a new match"
},
"@created_on": {
"description": "Label for creation date"
}, },
"@data": { "@data": {
"description": "Data label" "description": "Data label"
@@ -65,21 +74,24 @@
"@delete_all_data": { "@delete_all_data": {
"description": "Confirmation dialog for deleting all data" "description": "Confirmation dialog for deleting all data"
}, },
"@delete_group": { "@delete_game": {
"description": "Confirmation dialog for deleting a group" "description": "Button text to delete a game"
}, },
"@delete_group": {
"description": "Button text to delete a group"
},
"description": {
"description": "Description label"
},
"edit_game": {
"description": "Button text to edit a game"
},
"@edit_group": { "@edit_group": {
"description": "Button & Appbar label for editing a group" "description": "Button text to edit a group"
}, },
"@error_creating_group": { "@error_creating_group": {
"description": "Error message when group creation fails" "description": "Error message when group creation fails"
}, },
"@error_deleting_group": {
"description": "Error message when group deletion fails"
},
"@error_editing_group": {
"description": "Error message when group editing fails"
},
"@error_reading_file": { "@error_reading_file": {
"description": "Error message when file cannot be read" "description": "Error message when file cannot be read"
}, },
@@ -104,6 +116,9 @@
"@group_name": { "@group_name": {
"description": "Placeholder for group name input" "description": "Placeholder for group name input"
}, },
"@group_profile": {
"description": "Title for group profile view"
},
"@groups": { "@groups": {
"description": "Label for groups" "description": "Label for groups"
}, },
@@ -143,6 +158,9 @@
"@matches": { "@matches": {
"description": "Label for matches" "description": "Label for matches"
}, },
"@members": {
"description": "Label for group members"
},
"@most_points": { "@most_points": {
"description": "Title for most points ruleset" "description": "Title for most points ruleset"
}, },
@@ -188,6 +206,9 @@
"@not_available": { "@not_available": {
"description": "Abbreviation for not available" "description": "Abbreviation for not available"
}, },
"@played_matches": {
"description": "Label for played matches statistic"
},
"@player_name": { "@player_name": {
"description": "Placeholder for player name input" "description": "Placeholder for player name input"
}, },
@@ -293,14 +314,17 @@
"all_players_selected": "All players selected", "all_players_selected": "All players selected",
"amount_of_matches": "Amount of Matches", "amount_of_matches": "Amount of Matches",
"app_name": "Game Tracker", "app_name": "Game Tracker",
"best_player": "Best Player",
"cancel": "Cancel", "cancel": "Cancel",
"choose_game": "Choose Game", "choose_game": "Choose Game",
"choose_group": "Choose Group", "choose_group": "Choose Group",
"choose_ruleset": "Choose Ruleset", "choose_ruleset": "Choose Ruleset",
"could_not_add_player": "Could not add player", "could_not_add_player": "Could not add player",
"create_game": "Create Game",
"create_group": "Create Group", "create_group": "Create Group",
"create_match": "Create match", "create_match": "Create match",
"create_new_group": "Create new group", "create_new_group": "Create new group",
"created_on": "Created on",
"create_new_match": "Create new match", "create_new_match": "Create new match",
"data": "Data", "data": "Data",
"data_successfully_deleted": "Data successfully deleted", "data_successfully_deleted": "Data successfully deleted",
@@ -309,11 +333,12 @@
"days_ago": "{count} days ago", "days_ago": "{count} days ago",
"delete": "Delete", "delete": "Delete",
"delete_all_data": "Delete all data", "delete_all_data": "Delete all data",
"delete_group": "Delete this group", "delete_game": "Delete Game",
"delete_group": "Delete Group",
"description": "Description",
"edit_game": "Edit Game",
"edit_group": "Edit Group", "edit_group": "Edit Group",
"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_editing_group": "Error while editing group, please try again",
"error_reading_file": "Error reading file", "error_reading_file": "Error reading file",
"export_canceled": "Export canceled", "export_canceled": "Export canceled",
"export_data": "Export data", "export_data": "Export data",
@@ -322,6 +347,7 @@
"game_name": "Game Name", "game_name": "Game Name",
"group": "Group", "group": "Group",
"group_name": "Group name", "group_name": "Group name",
"group_profile": "Group Profile",
"groups": "Groups", "groups": "Groups",
"home": "Home", "home": "Home",
"import_canceled": "Import canceled", "import_canceled": "Import canceled",
@@ -335,6 +361,7 @@
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
"match_name": "Match name", "match_name": "Match name",
"matches": "Matches", "matches": "Matches",
"members": "Members",
"most_points": "Most Points", "most_points": "Most Points",
"no_data_available": "No data available", "no_data_available": "No data available",
"no_groups_created_yet": "No groups created yet", "no_groups_created_yet": "No groups created yet",
@@ -350,6 +377,7 @@
"none": "None", "none": "None",
"none_group": "None", "none_group": "None",
"not_available": "Not available", "not_available": "Not available",
"played_matches": "Played Matches",
"player_name": "Player name", "player_name": "Player name",
"players": "Players", "players": "Players",
"players_count": "{count} Players", "players_count": "{count} Players",

View File

@@ -98,6 +98,18 @@ abstract class AppLocalizations {
Locale('en'), Locale('en'),
]; ];
/// No description provided for @description.
///
/// In en, this message translates to:
/// **'Description'**
String get description;
/// No description provided for @edit_game.
///
/// In en, this message translates to:
/// **'Edit Game'**
String get edit_game;
/// Label for all players list /// Label for all players list
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -122,6 +134,12 @@ abstract class AppLocalizations {
/// **'Game Tracker'** /// **'Game Tracker'**
String get app_name; String get app_name;
/// Label for best player statistic
///
/// In en, this message translates to:
/// **'Best Player'**
String get best_player;
/// Cancel button text /// Cancel button text
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -152,6 +170,12 @@ abstract class AppLocalizations {
/// **'Could not add player'** /// **'Could not add player'**
String could_not_add_player(Object playerName); String could_not_add_player(Object playerName);
/// Button text to create a game
///
/// In en, this message translates to:
/// **'Create Game'**
String get create_game;
/// Button text to create a group /// Button text to create a group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -164,13 +188,19 @@ abstract class AppLocalizations {
/// **'Create match'** /// **'Create match'**
String get create_match; String get create_match;
/// Appbar text to create a new group /// Button text to create a new group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create new group'** /// **'Create new group'**
String get create_new_group; String get create_new_group;
/// Appbar text to create a new match /// Label for creation date
///
/// In en, this message translates to:
/// **'Created on'**
String get created_on;
/// Button text to create a new match
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create new match'** /// **'Create new match'**
@@ -218,13 +248,19 @@ abstract class AppLocalizations {
/// **'Delete all data'** /// **'Delete all data'**
String get delete_all_data; String get delete_all_data;
/// Confirmation dialog for deleting a group /// Button text to delete a game
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete this group'** /// **'Delete Game'**
String get delete_game;
/// Button text to delete a group
///
/// In en, this message translates to:
/// **'Delete Group'**
String get delete_group; String get delete_group;
/// Button & Appbar label for editing a group /// Button text to edit a group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Edit Group'** /// **'Edit Group'**
@@ -236,18 +272,6 @@ abstract class AppLocalizations {
/// **'Error while creating group, please try again'** /// **'Error while creating group, please try again'**
String get error_creating_group; String get error_creating_group;
/// Error message when group deletion fails
///
/// In en, this message translates to:
/// **'Error while deleting group, please try again'**
String get error_deleting_group;
/// Error message when group editing fails
///
/// In en, this message translates to:
/// **'Error while editing group, please try again'**
String get error_editing_group;
/// Error message when file cannot be read /// Error message when file cannot be read
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -296,6 +320,12 @@ abstract class AppLocalizations {
/// **'Group name'** /// **'Group name'**
String get group_name; String get group_name;
/// Title for group profile view
///
/// In en, this message translates to:
/// **'Group Profile'**
String get group_profile;
/// Label for groups /// Label for groups
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -374,6 +404,12 @@ abstract class AppLocalizations {
/// **'Matches'** /// **'Matches'**
String get matches; String get matches;
/// Label for group members
///
/// In en, this message translates to:
/// **'Members'**
String get members;
/// Title for most points ruleset /// Title for most points ruleset
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -464,6 +500,12 @@ abstract class AppLocalizations {
/// **'Not available'** /// **'Not available'**
String get not_available; String get not_available;
/// Label for played matches statistic
///
/// In en, this message translates to:
/// **'Played Matches'**
String get played_matches;
/// Placeholder for player name input /// Placeholder for player name input
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -8,6 +8,12 @@ import 'app_localizations.dart';
class AppLocalizationsDe extends AppLocalizations { class AppLocalizationsDe extends AppLocalizations {
AppLocalizationsDe([String locale = 'de']) : super(locale); AppLocalizationsDe([String locale = 'de']) : super(locale);
@override
String get description => 'Beschreibung';
@override
String get edit_game => 'Spielvorlage bearbeiten';
@override @override
String get all_players => 'Alle Spieler:innen'; String get all_players => 'Alle Spieler:innen';
@@ -20,6 +26,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get app_name => 'Game Tracker'; String get app_name => 'Game Tracker';
@override
String get best_player => 'Beste:r Spieler:in';
@override @override
String get cancel => 'Abbrechen'; String get cancel => 'Abbrechen';
@@ -37,6 +46,9 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Spieler:in $playerName konnte nicht hinzugefügt werden'; return 'Spieler:in $playerName konnte nicht hinzugefügt werden';
} }
@override
String get create_game => 'Spielvorlage erstellen';
@override @override
String get create_group => 'Gruppe erstellen'; String get create_group => 'Gruppe erstellen';
@@ -46,6 +58,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get create_new_group => 'Neue Gruppe erstellen'; String get create_new_group => 'Neue Gruppe erstellen';
@override
String get created_on => 'Erstellt am';
@override @override
String get create_new_match => 'Neues Spiel erstellen'; String get create_new_match => 'Neues Spiel erstellen';
@@ -73,7 +88,10 @@ 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 => 'Diese Gruppe löschen'; String get delete_game => 'Spielvorlage löschen';
@override
String get delete_group => 'Gruppe löschen';
@override @override
String get edit_group => 'Gruppe bearbeiten'; String get edit_group => 'Gruppe bearbeiten';
@@ -82,14 +100,6 @@ class AppLocalizationsDe extends AppLocalizations {
String get error_creating_group => String get error_creating_group =>
'Fehler beim Erstellen der Gruppe, bitte erneut versuchen'; 'Fehler beim Erstellen der Gruppe, bitte erneut versuchen';
@override
String get error_deleting_group =>
'Fehler beim Löschen der Gruppe, bitte erneut versuchen';
@override
String get error_editing_group =>
'Fehler beim Bearbeiten der Gruppe, bitte erneut versuchen';
@override @override
String get error_reading_file => 'Fehler beim Lesen der Datei'; String get error_reading_file => 'Fehler beim Lesen der Datei';
@@ -114,6 +124,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get group_name => 'Gruppenname'; String get group_name => 'Gruppenname';
@override
String get group_profile => 'Gruppenprofil';
@override @override
String get groups => 'Gruppen'; String get groups => 'Gruppen';
@@ -153,6 +166,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get matches => 'Spiele'; String get matches => 'Spiele';
@override
String get members => 'Mitglieder';
@override @override
String get most_points => 'Höchste Punkte'; String get most_points => 'Höchste Punkte';
@@ -199,6 +215,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get not_available => 'Nicht verfügbar'; String get not_available => 'Nicht verfügbar';
@override
String get played_matches => 'Gespielte Spiele';
@override @override
String get player_name => 'Spieler:innenname'; String get player_name => 'Spieler:innenname';

View File

@@ -8,6 +8,12 @@ import 'app_localizations.dart';
class AppLocalizationsEn extends AppLocalizations { class AppLocalizationsEn extends AppLocalizations {
AppLocalizationsEn([String locale = 'en']) : super(locale); AppLocalizationsEn([String locale = 'en']) : super(locale);
@override
String get description => 'Description';
@override
String get edit_game => 'Edit Game';
@override @override
String get all_players => 'All players'; String get all_players => 'All players';
@@ -20,6 +26,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get app_name => 'Game Tracker'; String get app_name => 'Game Tracker';
@override
String get best_player => 'Best Player';
@override @override
String get cancel => 'Cancel'; String get cancel => 'Cancel';
@@ -37,6 +46,9 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Could not add player'; return 'Could not add player';
} }
@override
String get create_game => 'Create Game';
@override @override
String get create_group => 'Create Group'; String get create_group => 'Create Group';
@@ -46,6 +58,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get create_new_group => 'Create new group'; String get create_new_group => 'Create new group';
@override
String get created_on => 'Created on';
@override @override
String get create_new_match => 'Create new match'; String get create_new_match => 'Create new match';
@@ -73,7 +88,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get delete_all_data => 'Delete all data'; String get delete_all_data => 'Delete all data';
@override @override
String get delete_group => 'Delete this group'; String get delete_game => 'Delete Game';
@override
String get delete_group => 'Delete Group';
@override @override
String get edit_group => 'Edit Group'; String get edit_group => 'Edit Group';
@@ -82,14 +100,6 @@ class AppLocalizationsEn extends AppLocalizations {
String get error_creating_group => String get error_creating_group =>
'Error while creating group, please try again'; 'Error while creating group, please try again';
@override
String get error_deleting_group =>
'Error while deleting group, please try again';
@override
String get error_editing_group =>
'Error while editing group, please try again';
@override @override
String get error_reading_file => 'Error reading file'; String get error_reading_file => 'Error reading file';
@@ -114,6 +124,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get group_name => 'Group name'; String get group_name => 'Group name';
@override
String get group_profile => 'Group Profile';
@override @override
String get groups => 'Groups'; String get groups => 'Groups';
@@ -153,6 +166,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get matches => 'Matches'; String get matches => 'Matches';
@override
String get members => 'Members';
@override @override
String get most_points => 'Most Points'; String get most_points => 'Most Points';
@@ -199,6 +215,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get not_available => 'Not available'; String get not_available => 'Not available';
@override
String get played_matches => 'Played Matches';
@override @override
String get player_name => 'Player name'; String get player_name => 'Player name';

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/constants.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/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
@@ -47,6 +48,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return ScaffoldMessenger(
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_group)), appBar: AppBar(title: Text(loc.create_new_group)),
body: SafeArea( body: SafeArea(
@@ -58,6 +60,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
child: TextInputField( child: TextInputField(
controller: _groupNameController, controller: _groupNameController,
hintText: loc.group_name, hintText: loc.group_name,
maxLength: Constants.MAX_GROUP_NAME_LENGTH,
), ),
), ),
Expanded( Expanded(

View File

@@ -1,182 +0,0 @@
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/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.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:provider/provider.dart';
class GroupDetailView extends StatefulWidget {
const GroupDetailView({super.key, this.groupToEdit});
/// The group to edit, if any
final Group? groupToEdit;
@override
State<GroupDetailView> createState() => _GroupDetailViewState();
}
class _GroupDetailViewState extends State<GroupDetailView> {
late final AppDatabase db;
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// Controller for the group name input field
final _groupNameController = TextEditingController();
/// List of currently selected players
List<Player> selectedPlayers = [];
/// List of initially selected players (when editing a group)
List<Player> initialSelectedPlayers = [];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
if(widget.groupToEdit != null) {
_groupNameController.text = widget.groupToEdit!.name;
setState(() {
initialSelectedPlayers = widget.groupToEdit!.members;
selectedPlayers = widget.groupToEdit!.members;
});
}
_groupNameController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_groupNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
key: _scaffoldMessengerKey,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async {
if(widget.groupToEdit != null) {
showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(loc.delete_group),
content: Text(loc.this_cannot_be_undone),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(loc.cancel),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(loc.delete),
),
],
),
).then((confirmed) async {
if (confirmed == true && context.mounted) {
bool success = await db.groupDao.deleteGroup(groupId: widget.groupToEdit!.id);
if (!context.mounted) return;
if (success) {
Navigator.pop(context);
} else {
if (!mounted) return;
showSnackbar(message: loc.error_deleting_group);
}
}
});
}
},)],),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: CustomTheme.standardMargin,
child: TextInputField(
controller: _groupNameController,
hintText: loc.group_name,
),
),
Expanded(
child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers,
onChanged: (value) {
setState(() {
selectedPlayers = [...value];
});
},
),
),
CustomWidthButton(
text: widget.groupToEdit == null ? loc.create_group : loc.edit_group,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed:
(_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null
: () async {
late bool success;
if (widget.groupToEdit == null) {
success = await db.groupDao.addGroup(
group: Group(
name: _groupNameController.text.trim(),
members: selectedPlayers,
),
);
} else {
//TODO: Implement group editing in database
/*
success = await db.groupDao.updateGroup(
group: Group(
id: widget.groupToEdit!.id,
name: _groupNameController.text.trim(),
members: selectedPlayers,
),
);
*/
success = false;
}
if (!context.mounted) return;
if (success) {
Navigator.pop(context);
} else {
showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group);
}
},
),
const SizedBox(height: 20),
],
),
),
),
);
}
/// Displays a snackbar with the given message and optional action.
///
/// [message] The message to display in the snackbar.
void showSnackbar({
required String message,
}) {
final messenger = _scaffoldMessengerKey.currentState;
if (messenger != null) {
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.boxColor,
),
);
}
}
}

View File

@@ -0,0 +1,271 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart';
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class GroupProfileView extends StatefulWidget {
/// A view that displays the profile of a group
/// - [group]: The group to display
const GroupProfileView({
super.key,
required this.group,
required this.callback,
});
/// The group to display
final Group group;
final VoidCallback callback;
@override
State<GroupProfileView> createState() => _GroupProfileViewState();
}
class _GroupProfileViewState extends State<GroupProfileView> {
late final AppDatabase db;
bool isLoading = true;
/// Total matches played in this group
int totalMatches = 0;
String bestPlayer = '';
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_loadStatistics();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
title: Text(loc.group_profile),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () async {
showDialog<bool>(
context: context,
builder: (context) => CustomAlertDialog(
title: '${loc.delete_group}?',
content: loc.this_cannot_be_undone,
actions: [
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(false),
child: Text(
loc.cancel,
style: const TextStyle(color: CustomTheme.textColor),
),
),
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(true),
child: Text(
loc.delete,
style: TextStyle(color: CustomTheme.secondaryColor),
),
),
],
),
).then((confirmed) async {
if (confirmed! && context.mounted) {
await db.groupDao.deleteGroup(groupId: widget.group.id);
if (!context.mounted) return;
Navigator.pop(context);
widget.callback.call();
}
});
},
),
],
),
body: SafeArea(
child: Stack(
alignment: Alignment.center,
children: [
ListView(
padding: const EdgeInsets.only(
left: 12,
right: 12,
top: 20,
bottom: 100,
),
children: [
const Center(
child: ColoredIconContainer(
icon: Icons.group,
containerSize: 55,
iconSize: 38,
),
),
const SizedBox(height: 10),
Text(
widget.group.name,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 5),
Text(
'${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(widget.group.createdAt)}',
style: const TextStyle(
fontSize: 12,
color: CustomTheme.textColor,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
InfoTile(
title: loc.members,
icon: Icons.people,
horizontalAlignment: CrossAxisAlignment.start,
content: Wrap(
alignment: WrapAlignment.start,
crossAxisAlignment: WrapCrossAlignment.start,
spacing: 12,
runSpacing: 8,
children: widget.group.members.map((member) {
return TextIconTile(
text: member.name,
iconEnabled: false,
);
}).toList(),
),
),
const SizedBox(height: 15),
InfoTile(
title: loc.statistics,
icon: Icons.bar_chart,
content: AppSkeleton(
enabled: isLoading,
child: Column(
children: [
_buildStatRow(
loc.members,
widget.group.members.length.toString(),
),
_buildStatRow(
loc.played_matches,
totalMatches.toString(),
),
_buildStatRow(loc.best_player, bestPlayer),
],
),
),
),
],
),
Positioned(
bottom: MediaQuery.paddingOf(context).bottom,
child: MainMenuButton(
text: loc.edit_group,
icon: Icons.edit,
onPressed: () {
// TODO: Uncomment when GroupDetailView is implemented
/*
await Navigator.push(
context,
adaptivePageRoute(
builder: (context) {
return const GroupDetailView();
},
),
);*/
print('Edit Group pressed');
},
),
),
],
),
),
);
}
/// Builds a single statistic row with a label and value
/// - [label]: The label of the statistic
/// - [value]: The value of the statistic
Widget _buildStatRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Text(
label,
style: const TextStyle(
fontSize: 16,
color: CustomTheme.textColor,
),
),
],
),
Text(
value,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
);
}
/// Loads statistics for this group
Future<void> _loadStatistics() async {
final matches = await db.matchDao.getAllMatches();
final groupMatches = matches
.where((match) => match.group?.id == widget.group.id)
.toList();
setState(() {
totalMatches = groupMatches.length;
bestPlayer = _getBestPlayer(groupMatches);
isLoading = false;
});
}
/// Determines the best player in the group based on match wins
String _getBestPlayer(List<Match> matches) {
final bestPlayerCounts = <Player, int>{};
// Count wins for each player
for (var match in matches) {
if (match.winner != null) {
bestPlayerCounts.update(
match.winner!,
(value) => value + 1,
ifAbsent: () => 1,
);
}
}
// Sort players by win count
final sortedPlayers = bestPlayerCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
// Get the best player
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
return bestPlayer;
}
}

View File

@@ -6,7 +6,8 @@ import 'package:game_tracker/data/db/database.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/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/group_detail_view.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/group_profile_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/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
@@ -74,19 +75,22 @@ class _GroupsViewState extends State<GroupsView> {
height: MediaQuery.paddingOf(context).bottom - 20, height: MediaQuery.paddingOf(context).bottom - 20,
); );
} }
return GroupTile(group: groups[index], onTap: () async { return GroupTile(
await Navigator.push( group: groups[index],
context, onTap: () async {
adaptivePageRoute( await Navigator.push(
builder: (context) { context,
return GroupDetailView(groupToEdit: groups[index]); adaptivePageRoute(
}, builder: (context) {
), return GroupProfileView(
); group: groups[index],
setState(() { callback: loadGroups,
loadGroups(); );
}); },
}); ),
);
},
);
}, },
), ),
), ),
@@ -101,7 +105,7 @@ class _GroupsViewState extends State<GroupsView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return const GroupDetailView(); return const CreateGroupView();
}, },
), ),
); );
@@ -117,9 +121,12 @@ class _GroupsViewState extends State<GroupsView> {
} }
void loadGroups() { void loadGroups() {
setState(() {
isLoading = true;
});
Future.wait([ Future.wait([
db.groupDao.getAllGroups(), db.groupDao.getAllGroups(),
Future.delayed(Constants.minimumSkeletonDuration), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) { ]).then((results) {
loadedGroups = results[0] as List<Group>; loadedGroups = results[0] as List<Group>;
setState(() { setState(() {

View File

@@ -195,7 +195,7 @@ class _HomeViewState extends State<HomeView> {
db.matchDao.getMatchCount(), db.matchDao.getMatchCount(),
db.groupDao.getGroupCount(), db.groupDao.getGroupCount(),
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
Future.delayed(Constants.minimumSkeletonDuration), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) { ]).then((results) {
matchCount = results[0] as int; matchCount = results[0] as int;
groupCount = results[1] as int; groupCount = results[1] as int;

View File

@@ -1,7 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.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/data/dto/game.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/game_view/create_game_view.dart';
import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.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'; import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
@@ -43,6 +46,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),
@@ -50,6 +54,17 @@ class _ChooseGameViewState extends State<ChooseGameView> {
Navigator.of(context).pop(selectedGameIndex); Navigator.of(context).pop(selectedGameIndex);
}, },
), ),
actions: [IconButton(
icon: const Icon(Icons.add),
onPressed: () async {
await Navigator.push(context, adaptivePageRoute(
builder: (context) => CreateGameView(
callback: () {}, //TODO: implement callback
),
)
);
},
)],
title: Text(loc.choose_game), title: Text(loc.choose_game),
), ),
body: PopScope( body: PopScope(
@@ -84,7 +99,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
context, context,
), ),
isHighlighted: selectedGameIndex == index, isHighlighted: selectedGameIndex == index,
onPressed: () async { onTap: () async {
setState(() { setState(() {
if (selectedGameIndex == index) { if (selectedGameIndex == index) {
selectedGameIndex = -1; selectedGameIndex = -1;
@@ -93,6 +108,16 @@ class _ChooseGameViewState extends State<ChooseGameView> {
} }
}); });
}, },
onLongPress: () async {
await Navigator.push(context, adaptivePageRoute(
builder: (context) => CreateGameView(
//TODO: implement callback & giving real game to create game view
gameToEdit: Game(name: 'Cabo', description: '', ruleset: 'Highest Points'),
callback: () {},
),
)
);
},
); );
}, },
), ),

View File

@@ -43,6 +43,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return Scaffold( return Scaffold(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back_ios), icon: const Icon(Icons.arrow_back_ios),

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.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/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
@@ -9,7 +10,6 @@ import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.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_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_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/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/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart'; import 'package:game_tracker/presentation/widgets/player_selection.dart';
@@ -57,13 +57,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// the [ChooseGroupView] /// the [ChooseGroupView]
String selectedGroupId = ''; 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 index of the currently selected game in [games] to mark it in
/// the [ChooseGameView] /// the [ChooseGameView]
int selectedGameIndex = -1; int selectedGameIndex = -1;
@@ -71,12 +64,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// The currently selected players /// The currently selected players
List<Player>? selectedPlayers; List<Player>? selectedPlayers;
/// List of available rulesets with their localized string representations
late final List<(Ruleset, String)> _rulesets;
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -109,15 +96,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
super.didChangeDependencies(); super.didChangeDependencies();
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
hintText ??= loc.match_name; hintText ??= loc.match_name;
_rulesets = [
(Ruleset.singleWinner, loc.ruleset_single_winner),
(Ruleset.singleLoser, loc.ruleset_single_loser),
(Ruleset.mostPoints, loc.ruleset_most_points),
(Ruleset.leastPoints, loc.ruleset_least_points),
];
} }
// TODO: Replace when games are implemented
List<(String, String, Ruleset)> games = [ List<(String, String, Ruleset)> games = [
('Example Game 1', 'This is a description', Ruleset.leastPoints), ('Example Game 1', 'This is a description', Ruleset.leastPoints),
('Example Game 2', '', Ruleset.singleWinner), ('Example Game 2', '', Ruleset.singleWinner),
@@ -127,8 +107,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return ScaffoldMessenger(
key: _scaffoldMessengerKey,
child: Scaffold( child: Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_match)), appBar: AppBar(title: Text(loc.create_new_match)),
body: SafeArea( body: SafeArea(
@@ -140,6 +120,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: TextInputField( child: TextInputField(
controller: _matchNameController, controller: _matchNameController,
hintText: hintText ?? '', hintText: hintText ?? '',
maxLength: Constants.MAX_MATCH_NAME_LENGTH,
), ),
), ),
ChooseTile( ChooseTile(
@@ -159,39 +140,12 @@ class _CreateMatchViewState extends State<CreateMatchView> {
setState(() { setState(() {
if (selectedGameIndex != -1) { if (selectedGameIndex != -1) {
hintText = games[selectedGameIndex].$1; hintText = games[selectedGameIndex].$1;
selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
} else { } else {
hintText = loc.match_name; hintText = loc.match_name;
selectedRuleset = null;
} }
}); });
}, },
), ),
ChooseTile(
title: loc.ruleset,
trailingText: selectedRuleset == null
? loc.none
: translateRulesetToString(selectedRuleset!, context),
onPressed: () async {
selectedRuleset = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseRulesetView(
rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
if (!mounted) return;
selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
selectedGameIndex = -1;
setState(() {});
},
),
ChooseTile( ChooseTile(
title: loc.group, title: loc.group,
trailingText: selectedGroup == null trailingText: selectedGroup == null
@@ -276,7 +230,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// - Either a group is selected OR at least 2 players are selected /// - Either a group is selected OR at least 2 players are selected
bool _enableCreateGameButton() { bool _enableCreateGameButton() {
return (selectedGroup != null || return (selectedGroup != null ||
(selectedPlayers != null && selectedPlayers!.length > 1)) && (selectedPlayers != null && selectedPlayers!.length > 1));
selectedRuleset != null;
} }
} }

View File

@@ -3,7 +3,6 @@ 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/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget { class ChooseRulesetView extends StatefulWidget {
/// A view that allows the user to choose a ruleset from a list of available rulesets /// A view that allows the user to choose a ruleset from a list of available rulesets
/// - [rulesets]: A list of tuples containing the ruleset and its description /// - [rulesets]: A list of tuples containing the ruleset and its description
@@ -13,17 +12,13 @@ class ChooseRulesetView extends StatefulWidget {
required this.rulesets, required this.rulesets,
required this.initialRulesetIndex, required this.initialRulesetIndex,
}); });
/// A list of tuples containing the ruleset and its description /// A list of tuples containing the ruleset and its description
final List<(Ruleset, String)> rulesets; final List<(Ruleset, String)> rulesets;
/// The index of the initially selected ruleset /// The index of the initially selected ruleset
final int initialRulesetIndex; final int initialRulesetIndex;
@override @override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState(); State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
} }
class _ChooseRulesetViewState extends State<ChooseRulesetView> { class _ChooseRulesetViewState extends State<ChooseRulesetView> {
/// Currently selected ruleset index /// Currently selected ruleset index
late int selectedRulesetIndex; late int selectedRulesetIndex;
@@ -33,7 +28,6 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
selectedRulesetIndex = widget.initialRulesetIndex; selectedRulesetIndex = widget.initialRulesetIndex;
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -74,7 +68,7 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
itemCount: widget.rulesets.length, itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile( return TitleDescriptionListTile(
onPressed: () async { onTap: () async {
setState(() { setState(() {
if (selectedRulesetIndex == index) { if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1; selectedRulesetIndex = -1;
@@ -96,4 +90,4 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
), ),
); );
} }
} }

View File

@@ -0,0 +1,140 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/dto/game.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/game_view/choose_ruleset_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
class CreateGameView extends StatefulWidget {
const CreateGameView({super.key, this.gameToEdit, required this.callback});
final Game? gameToEdit;
final VoidCallback callback;
@override
State<CreateGameView> createState() => _CreateGameViewState();
}
class _CreateGameViewState extends State<CreateGameView> {
Ruleset? selectedRuleset;
int selectedRulesetIndex = -1;
late List<(Ruleset, String)> _rulesets;
final _gameNameController = TextEditingController();
final _descriptionController = TextEditingController();
@override
void initState() {
super.initState();
_gameNameController.addListener(() => setState(() {}));
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final loc = AppLocalizations.of(context);
_rulesets = [
(Ruleset.singleWinner, loc.ruleset_single_winner),
(Ruleset.singleLoser, loc.ruleset_single_loser),
(Ruleset.mostPoints, loc.ruleset_most_points),
(Ruleset.leastPoints, loc.ruleset_least_points),
];
if (widget.gameToEdit != null) {
_gameNameController.text = widget.gameToEdit!.name;
_descriptionController.text = widget.gameToEdit!.description ?? '';
// TODO: Handle ruleset initialization from gameToEdit
}
}
@override
void dispose() {
_gameNameController.dispose();
_descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
var loc = AppLocalizations.of(context);
final isEditing = widget.gameToEdit != null;
return ScaffoldMessenger(
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
title: Text(isEditing ? loc.edit_game : loc.create_game),
),
body: SafeArea(
child: Column(
children: [
Container(
margin: CustomTheme.tileMargin,
child: TextInputField(
controller: _gameNameController,
maxLength: Constants.MAX_MATCH_NAME_LENGTH,
hintText: loc.game_name,
),
),
ChooseTile(
title: loc.ruleset,
trailingText: selectedRuleset == null
? loc.none
: translateRulesetToString(selectedRuleset!, context),
onPressed: () async {
final result = await Navigator.of(context).push<Ruleset?>(
adaptivePageRoute(
builder: (context) => ChooseRulesetView(
rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
if (mounted) {
setState(() {
selectedRuleset = result;
selectedRulesetIndex =
result == null ? -1 : _rulesets.indexWhere((r) => r.$1 == result);
});
}
},
),
Container(
margin: CustomTheme.tileMargin,
child: TextInputField(
controller: _descriptionController,
hintText: loc.description,
minLines: 6,
maxLines: 6,
maxLength: Constants.MAX_GAME_DESCRIPTION_LENGTH,
showCounterText: true,
),
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(12.0),
child: CustomWidthButton(
text: isEditing ? loc.edit_group : loc.create_game,
sizeRelativeToWidth: 1,
buttonType: ButtonType.primary,
onPressed: _gameNameController.text.trim().isNotEmpty && selectedRulesetIndex != -1
? () {
//TODO: Handle saving to db & updating game selection view
Navigator.of(context).pop();
}
: null,
),
),
],
),
),
),
);
}
}

View File

@@ -130,7 +130,7 @@ class _MatchViewState extends State<MatchView> {
void loadGames() { void loadGames() {
Future.wait([ Future.wait([
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
Future.delayed(Constants.minimumSkeletonDuration), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) { ]).then((results) {
if (mounted) { if (mounted) {
setState(() { setState(() {

View File

@@ -2,6 +2,7 @@ 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/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart';
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class LicenseDetailView extends StatelessWidget { class LicenseDetailView extends StatelessWidget {
@@ -29,19 +30,11 @@ class LicenseDetailView extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Container( const ColoredIconContainer(
margin: const EdgeInsetsGeometry.only(right: 15), icon: Icons.description,
width: 60, margin: EdgeInsetsGeometry.only(right: 15),
height: 60, containerSize: 60,
decoration: BoxDecoration( iconSize: 30,
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.description,
color: CustomTheme.primaryColor,
size: 30,
),
), ),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,

View File

@@ -31,7 +31,6 @@ class _SettingsViewState extends State<SettingsView> {
version: 'n.A.', version: 'n.A.',
buildNumber: 'n.A.', buildNumber: 'n.A.',
); );
@override @override
void initState() { void initState() {
super.initState(); super.initState();

View File

@@ -106,7 +106,7 @@ class _StatisticsViewState extends State<StatisticsView> {
Future.wait([ Future.wait([
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
db.playerDao.getAllPlayers(), db.playerDao.getAllPlayers(),
Future.delayed(Constants.minimumSkeletonDuration), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) async { ]).then((results) async {
if (!mounted) return; if (!mounted) return;
final matches = results[0] as List<Match>; final matches = results[0] as List<Match>;

View File

@@ -0,0 +1,57 @@
import 'package:flutter/cupertino.dart';
import 'package:game_tracker/core/custom_theme.dart';
class ColoredIconContainer extends StatelessWidget {
/// A customizable container widget that displays an icon with a colored background.
/// - [icon]: The icon to be displayed inside the container.
/// - [containerSize]: The size of the container (width and height).
/// - [iconSize]: The size of the icon inside the container.
/// - [margin]: Optional margin around the container.
/// - [padding]: Optional padding inside the container.
const ColoredIconContainer({
super.key,
required this.icon,
this.containerSize = 44,
this.iconSize = 28,
this.margin,
this.padding,
});
/// The icon to be displayed inside the container.
final IconData icon;
/// The size of the container (width and height).
final double containerSize;
/// The size of the icon inside the container.
final double iconSize;
/// Optional margin around the container.
final EdgeInsetsGeometry? margin;
/// Optional padding inside the container.
final EdgeInsetsGeometry? padding;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(
width: containerSize,
height: containerSize,
margin: margin,
padding: padding,
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: iconSize,
color: CustomTheme.primaryColor.withGreen(40),
),
),
],
);
}
}

View File

@@ -70,7 +70,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
suggestedPlayers = skeletonData; suggestedPlayers = skeletonData;
selectedPlayers = widget.initialSelectedPlayers ?? [];
loadPlayerList(); loadPlayerList();
} }
@@ -85,6 +84,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CustomSearchBar( CustomSearchBar(
maxLength: Constants.MAX_PLAYER_NAME_LENGTH,
controller: _searchBarController, controller: _searchBarController,
constraints: const BoxConstraints(maxHeight: 45, minHeight: 45), constraints: const BoxConstraints(maxHeight: 45, minHeight: 45),
hintText: loc.search_for_players, hintText: loc.search_for_players,
@@ -100,7 +100,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (value.isEmpty) { if (value.isEmpty) {
// If the search is empty, it shows all unselected players. // If the search is empty, it shows all unselected players.
suggestedPlayers = allPlayers.where((player) { suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.any((p) => p.id == player.id); return !selectedPlayers.contains(player);
}).toList(); }).toList();
} else { } else {
// If there is input, it filters by name match (case-insensitive) and ensures // If there is input, it filters by name match (case-insensitive) and ensures
@@ -109,7 +109,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
final bool nameMatches = player.name.toLowerCase().contains( final bool nameMatches = player.name.toLowerCase().contains(
value.toLowerCase(), value.toLowerCase(),
); );
final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id); final bool isNotSelected = !selectedPlayers.contains(
player,
);
return nameMatches && isNotSelected; return nameMatches && isNotSelected;
}).toList(); }).toList();
} }
@@ -124,49 +126,46 @@ class _PlayerSelectionState extends State<PlayerSelection> {
const SizedBox(height: 10), const SizedBox(height: 10),
SizedBox( SizedBox(
height: 50, height: 50,
child: AppSkeleton( child: selectedPlayers.isEmpty
enabled: isLoading, ? Center(child: Text(loc.no_players_selected))
child: selectedPlayers.isEmpty : SingleChildScrollView(
? Center(child: Text(loc.no_players_selected)) scrollDirection: Axis.horizontal,
: SingleChildScrollView( child: Row(
scrollDirection: Axis.horizontal, children: [
child: Row( for (var player in selectedPlayers)
children: [ Padding(
for (var player in selectedPlayers) padding: const EdgeInsets.only(right: 8.0),
Padding( child: TextIconTile(
padding: const EdgeInsets.only(right: 8.0), text: player.name,
child: TextIconTile( onIconTap: () {
text: player.name, setState(() {
onIconTap: () { // Removes the player from the selection and notifies the parent.
setState(() { selectedPlayers.remove(player);
// Removes the player from the selection and notifies the parent. widget.onChanged([...selectedPlayers]);
selectedPlayers.remove(player);
widget.onChanged([...selectedPlayers]);
// Get the current search query // Get the current search query
final currentSearch = _searchBarController final currentSearch = _searchBarController
.text .text
.toLowerCase(); .toLowerCase();
// 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 `suggestedPlayers` 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( suggestedPlayers.sort(
(a, b) => a.name.compareTo(b.name), (a, b) => a.name.compareTo(b.name),
); );
} }
}); });
}, },
),
), ),
], ),
), ],
), ),
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
@@ -221,7 +220,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
void loadPlayerList() { void loadPlayerList() {
_allPlayersFuture = Future.wait([ _allPlayersFuture = Future.wait([
db.playerDao.getAllPlayers(), db.playerDao.getAllPlayers(),
Future.delayed(Constants.minimumSkeletonDuration), Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) => results[0] as List<Player>); ]).then((results) => results[0] as List<Player>);
if (mounted) { if (mounted) {
_allPlayersFuture.then((loadedPlayers) { _allPlayersFuture.then((loadedPlayers) {
@@ -246,21 +245,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
// Otherwise, use the loaded players from the database. // 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];
if (widget.initialSelectedPlayers != null) { suggestedPlayers = [...loadedPlayers];
// Excludes already selected players from the suggested players list.
suggestedPlayers = loadedPlayers.where((p) => !widget.initialSelectedPlayers!.any((ip) => ip.id == p.id)).toList();
// Ensures that only players available for selection are pre-selected.
selectedPlayers = widget.initialSelectedPlayers!
.where(
(p) => allPlayers.any(
(available) => available.id == p.id,
),
)
.toList();
} else {
// If no initial selection, all loaded players are suggested.
suggestedPlayers = [...loadedPlayers];
}
} }
isLoading = false; isLoading = false;
}); });

View File

@@ -21,6 +21,7 @@ class CustomSearchBar extends StatelessWidget {
this.onTrailingButtonPressed, this.onTrailingButtonPressed,
this.onChanged, this.onChanged,
this.constraints, this.constraints,
this.maxLength,
}); });
/// The controller for the search bar's text input. /// The controller for the search bar's text input.
@@ -47,8 +48,21 @@ class CustomSearchBar extends StatelessWidget {
/// The constraints for the search bar. /// The constraints for the search bar.
final BoxConstraints? constraints; final BoxConstraints? constraints;
/// Optional parameter for maximum length of the input text.
final int? maxLength;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
/// Enforce maximum length on the input text
if (maxLength != null) {
if (controller.text.length > maxLength!) {
controller.text = controller.text.substring(0, maxLength);
controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length),
);
}
}
return SearchBar( return SearchBar(
controller: controller, controller: controller,
constraints: constraints:

View File

@@ -1,37 +1,63 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
class TextInputField extends StatelessWidget { class TextInputField extends StatelessWidget {
/// A custom text input field widget that encapsulates a [TextField] with specific styling. /// A custom text input field widget that encapsulates a [TextField] with specific styling.
/// - [controller]: The controller for the text input field. /// - [controller]: The controller for the text input field.
/// - [onChanged]: The callback invoked when the text in the field changes. /// - [onChanged]: Optional callback invoked when the text in the field changes.
/// - [hintText]: The hint text displayed in the text input field when it is empty /// - [hintText]: The hint text displayed in the text input field when it is empty
/// - [maxLength]: Optional parameter for maximum length of the input text.
/// - [maxLines]: The maximum number of lines for the text input field. Defaults to 1.
/// - [minLines]: The minimum number of lines for the text input field. Defaults to 1.
/// - [showCounterText]: Whether to show the counter text in the text input field. Defaults to false.
const TextInputField({ const TextInputField({
super.key, super.key,
required this.controller, required this.controller,
required this.hintText, required this.hintText,
this.onChanged, this.onChanged,
this.maxLength,
this.maxLines = 1,
this.minLines = 1,
this.showCounterText = false
}); });
/// The controller for the text input field. /// The controller for the text input field.
final TextEditingController controller; final TextEditingController controller;
/// The callback invoked when the text in the field changes. /// Optional callback invoked when the text in the field changes.
final ValueChanged<String>? onChanged; final ValueChanged<String>? onChanged;
/// The hint text displayed in the text input field when it is empty. /// The hint text displayed in the text input field when it is empty.
final String hintText; final String hintText;
/// Optional parameter for maximum length of the input text.
final int? maxLength;
/// The maximum number of lines for the text input field.
final int? maxLines;
/// The minimum number of lines for the text input field.
final int? minLines;
/// Whether to show the counter text in the text input field.
final bool showCounterText;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(
controller: controller, controller: controller,
onChanged: onChanged, onChanged: onChanged,
maxLength: maxLength,
maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds,
maxLines: maxLines,
minLines: minLines,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: CustomTheme.boxColor, fillColor: CustomTheme.boxColor,
hintText: hintText, hintText: hintText,
hintStyle: const TextStyle(fontSize: 18), hintStyle: const TextStyle(fontSize: 18),
counterText: showCounterText ? null : '',
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
borderSide: BorderSide(color: CustomTheme.boxBorder), borderSide: BorderSide(color: CustomTheme.boxBorder),

View File

@@ -3,12 +3,17 @@ 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/tiles/text_icon_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
class GroupTile extends StatelessWidget { class GroupTile extends StatefulWidget {
/// A tile widget that displays information about a group, including its name and members. /// A tile widget that displays information about a group, including its name and members.
/// - [group]: The group data to be displayed. /// - [group]: The group data to be displayed.
/// - [isHighlighted]: Whether the tile should be highlighted. /// - [isHighlighted]: Whether the tile should be highlighted.
/// - [onTap]: An optional callback function to handle tap events. /// - [onTap]: Callback function to be executed when the tile is tapped.
const GroupTile({super.key, required this.group, this.isHighlighted = false, this.onTap}); const GroupTile({
super.key,
required this.group,
this.isHighlighted = false,
this.onTap,
});
/// The group data to be displayed. /// The group data to be displayed.
final Group group; final Group group;
@@ -16,17 +21,22 @@ class GroupTile extends StatelessWidget {
/// Whether the tile should be highlighted. /// Whether the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
/// Callback function to handle tap events. /// Callback function to be executed when the tile is tapped.
final VoidCallback? onTap; final VoidCallback? onTap;
@override
State<GroupTile> createState() => _GroupTileState();
}
class _GroupTileState extends State<GroupTile> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onTap, onTap: widget.onTap,
child: AnimatedContainer( child: AnimatedContainer(
margin: CustomTheme.standardMargin, margin: CustomTheme.standardMargin,
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10), padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 10),
decoration: isHighlighted decoration: widget.isHighlighted
? CustomTheme.highlightedBoxDecoration ? CustomTheme.highlightedBoxDecoration
: CustomTheme.standardBoxDecoration, : CustomTheme.standardBoxDecoration,
duration: const Duration(milliseconds: 150), duration: const Duration(milliseconds: 150),
@@ -38,7 +48,7 @@ class GroupTile extends StatelessWidget {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
group.name, widget.group.name,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -49,7 +59,7 @@ class GroupTile extends StatelessWidget {
Row( Row(
children: [ children: [
Text( Text(
'${group.members.length}', '${widget.group.members.length}',
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 18, fontSize: 18,
@@ -69,7 +79,7 @@ class GroupTile extends StatelessWidget {
runSpacing: 8.0, runSpacing: 8.0,
children: <Widget>[ children: <Widget>[
for (var member in [ for (var member in [
...group.members, ...widget.group.members,
]..sort((a, b) => a.name.compareTo(b.name))) ]..sort((a, b) => a.name.compareTo(b.name)))
TextIconTile(text: member.name, iconEnabled: false), TextIconTile(text: member.name, iconEnabled: false),
], ],

View File

@@ -17,6 +17,7 @@ class InfoTile extends StatefulWidget {
this.padding, this.padding,
this.height, this.height,
this.width, this.width,
this.horizontalAlignment = CrossAxisAlignment.center,
}); });
/// The title text displayed on the tile. /// The title text displayed on the tile.
@@ -37,6 +38,9 @@ class InfoTile extends StatefulWidget {
/// Optional width for the tile. /// Optional width for the tile.
final double? width; final double? width;
/// The main axis alignment for the content.
final CrossAxisAlignment horizontalAlignment;
@override @override
State<InfoTile> createState() => _InfoTileState(); State<InfoTile> createState() => _InfoTileState();
} }
@@ -51,7 +55,7 @@ class _InfoTileState extends State<InfoTile> {
decoration: CustomTheme.standardBoxDecoration, decoration: CustomTheme.standardBoxDecoration,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: widget.horizontalAlignment,
children: [ children: [
Row( Row(
children: [ children: [

View File

@@ -2,6 +2,7 @@ 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/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/license_detail_view.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart';
import 'package:game_tracker/presentation/widgets/colored_icon_container.dart';
class LicenseTile extends StatelessWidget { class LicenseTile extends StatelessWidget {
/// A tile widget that displays information about a software package license. /// A tile widget that displays information about a software package license.
@@ -29,18 +30,10 @@ class LicenseTile extends StatelessWidget {
), ),
child: Row( child: Row(
children: [ children: [
Container( const ColoredIconContainer(
width: 50, icon: Icons.description,
height: 50, containerSize: 50,
decoration: BoxDecoration( iconSize: 32,
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.description,
color: CustomTheme.primaryColor,
size: 32,
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Expanded( Expanded(

View File

@@ -230,7 +230,7 @@ class _MatchTileState extends State<MatchTile> {
} else if (difference.inDays < 7) { } else if (difference.inDays < 7) {
return loc.days_ago(difference.inDays); return loc.days_ago(difference.inDays);
} else { } else {
return DateFormat('MMM d, yyyy').format(dateTime); return '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
} }
} }

View File

@@ -1,5 +1,6 @@
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/presentation/widgets/colored_icon_container.dart';
class SettingsListTile extends StatelessWidget { class SettingsListTile extends StatelessWidget {
/// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget. /// A customizable settings list tile widget that displays an icon, title, and an optional suffix widget.
@@ -46,18 +47,10 @@ class SettingsListTile extends StatelessWidget {
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( ColoredIconContainer(
width: 44, icon: icon,
height: 44, containerSize: 44,
decoration: BoxDecoration( iconSize: 28,
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 28,
color: CustomTheme.primaryColor.withGreen(40),
),
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text(title, style: const TextStyle(fontSize: 18)), Text(title, style: const TextStyle(fontSize: 18)),

View File

@@ -6,6 +6,7 @@ class TitleDescriptionListTile extends StatelessWidget {
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
/// - [description]: The description text displayed below the title. /// - [description]: The description text displayed below the title.
/// - [onPressed]: The callback invoked when the tile is tapped. /// - [onPressed]: The callback invoked when the tile is tapped.
/// - [onLongPress]: The callback invoked when the tile is tapped.
/// - [isHighlighted]: A boolean to determine if the tile should be highlighted. /// - [isHighlighted]: A boolean to determine if the tile should be highlighted.
/// - [badgeText]: Optional text to display in a badge on the right side of the title. /// - [badgeText]: Optional text to display in a badge on the right side of the title.
/// - [badgeColor]: Optional color for the badge background. /// - [badgeColor]: Optional color for the badge background.
@@ -13,7 +14,8 @@ class TitleDescriptionListTile extends StatelessWidget {
super.key, super.key,
required this.title, required this.title,
required this.description, required this.description,
this.onPressed, this.onTap,
this.onLongPress,
this.isHighlighted = false, this.isHighlighted = false,
this.badgeText, this.badgeText,
this.badgeColor, this.badgeColor,
@@ -26,7 +28,10 @@ class TitleDescriptionListTile extends StatelessWidget {
final String description; final String description;
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback? onPressed; final VoidCallback? onTap;
/// The callback invoked when the tile is long-pressed.
final VoidCallback? onLongPress;
/// A boolean to determine if the tile should be highlighted. /// A boolean to determine if the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
@@ -40,7 +45,8 @@ class TitleDescriptionListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
onTap: onPressed, onTap: onTap,
onLongPress: onLongPress,
child: AnimatedContainer( child: AnimatedContainer(
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 10),
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12), padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 12),
@@ -73,7 +79,6 @@ class TitleDescriptionListTile extends StatelessWidget {
const Spacer(), const Spacer(),
Container( Container(
constraints: const BoxConstraints(maxWidth: 115), constraints: const BoxConstraints(maxWidth: 115),
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
vertical: 2, vertical: 2,
horizontal: 6, horizontal: 6,

View File

@@ -1,7 +1,7 @@
name: game_tracker name: game_tracker
description: "Game Tracking App for Card Games" description: "Game Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.7+214 version: 0.0.10+248
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1