Compare commits
15 Commits
feature/11
...
f1df067824
| Author | SHA1 | Date | |
|---|---|---|---|
| f1df067824 | |||
| 8b7300eac3 | |||
| 919a38afe5 | |||
| def31acfb1 | |||
| 016c1ceb6e | |||
| 1b297d15b0 | |||
| ed642e3d4f | |||
| 7cc3873a31 | |||
| b1e9bb3aeb | |||
| caf60d046b | |||
| 38663c6b67 | |||
| ee84c60ba6 | |||
| b6dd0541ae | |||
| 525acec1d3 | |||
| 45a419cae7 |
@@ -19,7 +19,4 @@ class Constants {
|
|||||||
|
|
||||||
/// Maximum length for team names
|
/// Maximum length for team names
|
||||||
static const int MAX_TEAM_NAME_LENGTH = 32;
|
static const int MAX_TEAM_NAME_LENGTH = 32;
|
||||||
|
|
||||||
/// Maximum length for game descriptions
|
|
||||||
static const int MAX_GAME_DESCRIPTION_LENGTH = 256;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,32 +5,14 @@ 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);
|
||||||
|
|
||||||
/// Default border color for boxes and containers
|
|
||||||
static Color boxBorder = const Color(0xFF272727);
|
|
||||||
|
|
||||||
/// Color for boxes and containers displayed on boxes
|
|
||||||
static Color onBoxColor = const Color(0xFF181818);
|
static Color onBoxColor = const Color(0xFF181818);
|
||||||
|
static Color boxBorder = const Color(0xFF272727);
|
||||||
/// 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 ====================
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
@@ -4,18 +4,15 @@
|
|||||||
"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",
|
||||||
@@ -23,12 +20,11 @@
|
|||||||
"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_game": "Spielvorlage löschen",
|
"delete_group": "Diese Gruppe 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",
|
||||||
@@ -37,7 +33,6 @@
|
|||||||
"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",
|
||||||
@@ -51,7 +46,6 @@
|
|||||||
"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",
|
||||||
@@ -67,7 +61,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -12,9 +12,6 @@
|
|||||||
"@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"
|
||||||
},
|
},
|
||||||
@@ -30,9 +27,6 @@
|
|||||||
"@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"
|
||||||
},
|
},
|
||||||
@@ -40,13 +34,10 @@
|
|||||||
"description": "Button text to create a match"
|
"description": "Button text to create a match"
|
||||||
},
|
},
|
||||||
"@create_new_group": {
|
"@create_new_group": {
|
||||||
"description": "Button text to create a new group"
|
"description": "Appbar text to create a new group"
|
||||||
},
|
},
|
||||||
"@create_new_match": {
|
"@create_new_match": {
|
||||||
"description": "Button text to create a new match"
|
"description": "Appbar text to create a new match"
|
||||||
},
|
|
||||||
"@created_on": {
|
|
||||||
"description": "Label for creation date"
|
|
||||||
},
|
},
|
||||||
"@data": {
|
"@data": {
|
||||||
"description": "Data label"
|
"description": "Data label"
|
||||||
@@ -74,24 +65,21 @@
|
|||||||
"@delete_all_data": {
|
"@delete_all_data": {
|
||||||
"description": "Confirmation dialog for deleting all data"
|
"description": "Confirmation dialog for deleting all data"
|
||||||
},
|
},
|
||||||
"@delete_game": {
|
|
||||||
"description": "Button text to delete a game"
|
|
||||||
},
|
|
||||||
"@delete_group": {
|
"@delete_group": {
|
||||||
"description": "Button text to delete a group"
|
"description": "Confirmation dialog for deleting a group"
|
||||||
},
|
},
|
||||||
"description": {
|
|
||||||
"description": "Description label"
|
|
||||||
},
|
|
||||||
"edit_game": {
|
|
||||||
"description": "Button text to edit a game"
|
|
||||||
},
|
|
||||||
"@edit_group": {
|
"@edit_group": {
|
||||||
"description": "Button text to edit a group"
|
"description": "Button & Appbar label for editing 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"
|
||||||
},
|
},
|
||||||
@@ -116,9 +104,6 @@
|
|||||||
"@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"
|
||||||
},
|
},
|
||||||
@@ -158,9 +143,6 @@
|
|||||||
"@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"
|
||||||
},
|
},
|
||||||
@@ -206,9 +188,6 @@
|
|||||||
"@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"
|
||||||
},
|
},
|
||||||
@@ -314,17 +293,14 @@
|
|||||||
"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",
|
||||||
@@ -333,12 +309,11 @@
|
|||||||
"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_game": "Delete Game",
|
"delete_group": "Delete this group",
|
||||||
"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",
|
||||||
@@ -347,7 +322,6 @@
|
|||||||
"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",
|
||||||
@@ -361,7 +335,6 @@
|
|||||||
"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",
|
||||||
@@ -377,7 +350,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -98,18 +98,6 @@ 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:
|
||||||
@@ -134,12 +122,6 @@ 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:
|
||||||
@@ -170,12 +152,6 @@ 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:
|
||||||
@@ -188,19 +164,13 @@ abstract class AppLocalizations {
|
|||||||
/// **'Create match'**
|
/// **'Create match'**
|
||||||
String get create_match;
|
String get create_match;
|
||||||
|
|
||||||
/// Button text to create a new group
|
/// Appbar 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;
|
||||||
|
|
||||||
/// Label for creation date
|
/// Appbar text to create a new match
|
||||||
///
|
|
||||||
/// 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'**
|
||||||
@@ -248,19 +218,13 @@ abstract class AppLocalizations {
|
|||||||
/// **'Delete all data'**
|
/// **'Delete all data'**
|
||||||
String get delete_all_data;
|
String get delete_all_data;
|
||||||
|
|
||||||
/// Button text to delete a game
|
/// Confirmation dialog for deleting a group
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Delete Game'**
|
/// **'Delete this group'**
|
||||||
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 text to edit a group
|
/// Button & Appbar label for editing a group
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
/// **'Edit Group'**
|
/// **'Edit Group'**
|
||||||
@@ -272,6 +236,18 @@ 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:
|
||||||
@@ -320,12 +296,6 @@ 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:
|
||||||
@@ -404,12 +374,6 @@ 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:
|
||||||
@@ -500,12 +464,6 @@ 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:
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ 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';
|
||||||
|
|
||||||
@@ -26,9 +20,6 @@ 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';
|
||||||
|
|
||||||
@@ -46,9 +37,6 @@ 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';
|
||||||
|
|
||||||
@@ -58,9 +46,6 @@ 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';
|
||||||
|
|
||||||
@@ -88,10 +73,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_game => 'Spielvorlage löschen';
|
String get delete_group => 'Diese Gruppe löschen';
|
||||||
|
|
||||||
@override
|
|
||||||
String get delete_group => 'Gruppe löschen';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get edit_group => 'Gruppe bearbeiten';
|
String get edit_group => 'Gruppe bearbeiten';
|
||||||
@@ -100,6 +82,14 @@ 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';
|
||||||
|
|
||||||
@@ -124,9 +114,6 @@ 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';
|
||||||
|
|
||||||
@@ -166,9 +153,6 @@ 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';
|
||||||
|
|
||||||
@@ -215,9 +199,6 @@ 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';
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ 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';
|
||||||
|
|
||||||
@@ -26,9 +20,6 @@ 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';
|
||||||
|
|
||||||
@@ -46,9 +37,6 @@ 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';
|
||||||
|
|
||||||
@@ -58,9 +46,6 @@ 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';
|
||||||
|
|
||||||
@@ -88,10 +73,7 @@ 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_game => 'Delete Game';
|
String get delete_group => 'Delete this group';
|
||||||
|
|
||||||
@override
|
|
||||||
String get delete_group => 'Delete Group';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get edit_group => 'Edit Group';
|
String get edit_group => 'Edit Group';
|
||||||
@@ -100,6 +82,14 @@ 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';
|
||||||
|
|
||||||
@@ -124,9 +114,6 @@ 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';
|
||||||
|
|
||||||
@@ -166,9 +153,6 @@ 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';
|
||||||
|
|
||||||
@@ -215,9 +199,6 @@ 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';
|
||||||
|
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
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/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 CreateGroupView extends StatefulWidget {
|
|
||||||
/// A view that allows the user to create a new group
|
|
||||||
const CreateGroupView({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CreateGroupView> createState() => _CreateGroupViewState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateGroupViewState extends State<CreateGroupView> {
|
|
||||||
late final AppDatabase db;
|
|
||||||
|
|
||||||
/// Controller for the group name input field
|
|
||||||
final _groupNameController = TextEditingController();
|
|
||||||
|
|
||||||
/// List of currently selected players
|
|
||||||
List<Player> selectedPlayers = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
db = Provider.of<AppDatabase>(context, listen: false);
|
|
||||||
_groupNameController.addListener(() {
|
|
||||||
setState(() {});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_groupNameController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final loc = AppLocalizations.of(context);
|
|
||||||
return ScaffoldMessenger(
|
|
||||||
child: Scaffold(
|
|
||||||
resizeToAvoidBottomInset: false,
|
|
||||||
backgroundColor: CustomTheme.backgroundColor,
|
|
||||||
appBar: AppBar(title: Text(loc.create_new_group)),
|
|
||||||
body: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
margin: CustomTheme.standardMargin,
|
|
||||||
child: TextInputField(
|
|
||||||
controller: _groupNameController,
|
|
||||||
hintText: loc.group_name,
|
|
||||||
maxLength: Constants.MAX_GROUP_NAME_LENGTH,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: PlayerSelection(
|
|
||||||
onChanged: (value) {
|
|
||||||
setState(() {
|
|
||||||
selectedPlayers = [...value];
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
CustomWidthButton(
|
|
||||||
text: loc.create_group,
|
|
||||||
sizeRelativeToWidth: 0.95,
|
|
||||||
buttonType: ButtonType.primary,
|
|
||||||
onPressed:
|
|
||||||
(_groupNameController.text.isEmpty ||
|
|
||||||
(selectedPlayers.length < 2))
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
bool success = await db.groupDao.addGroup(
|
|
||||||
group: Group(
|
|
||||||
name: _groupNameController.text.trim(),
|
|
||||||
members: selectedPlayers,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!context.mounted) return;
|
|
||||||
if (success) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
backgroundColor: CustomTheme.boxColor,
|
|
||||||
content: Center(
|
|
||||||
child: Text(
|
|
||||||
AppLocalizations.of(
|
|
||||||
context,
|
|
||||||
).error_creating_group,
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,8 +6,7 @@ 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/create_group_view.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/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';
|
||||||
@@ -75,22 +74,19 @@ class _GroupsViewState extends State<GroupsView> {
|
|||||||
height: MediaQuery.paddingOf(context).bottom - 20,
|
height: MediaQuery.paddingOf(context).bottom - 20,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return GroupTile(
|
return GroupTile(group: groups[index], onTap: () async {
|
||||||
group: groups[index],
|
await Navigator.push(
|
||||||
onTap: () async {
|
context,
|
||||||
await Navigator.push(
|
adaptivePageRoute(
|
||||||
context,
|
builder: (context) {
|
||||||
adaptivePageRoute(
|
return GroupDetailView(groupToEdit: groups[index]);
|
||||||
builder: (context) {
|
},
|
||||||
return GroupProfileView(
|
),
|
||||||
group: groups[index],
|
);
|
||||||
callback: loadGroups,
|
setState(() {
|
||||||
);
|
loadGroups();
|
||||||
},
|
});
|
||||||
),
|
});
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -105,7 +101,7 @@ class _GroupsViewState extends State<GroupsView> {
|
|||||||
context,
|
context,
|
||||||
adaptivePageRoute(
|
adaptivePageRoute(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return const CreateGroupView();
|
return const GroupDetailView();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -121,9 +117,6 @@ 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.MINIMUM_SKELETON_DURATION),
|
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -46,7 +43,6 @@ 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),
|
||||||
@@ -54,17 +50,6 @@ 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(
|
||||||
@@ -99,7 +84,7 @@ class _ChooseGameViewState extends State<ChooseGameView> {
|
|||||||
context,
|
context,
|
||||||
),
|
),
|
||||||
isHighlighted: selectedGameIndex == index,
|
isHighlighted: selectedGameIndex == index,
|
||||||
onTap: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedGameIndex == index) {
|
if (selectedGameIndex == index) {
|
||||||
selectedGameIndex = -1;
|
selectedGameIndex = -1;
|
||||||
@@ -108,16 +93,6 @@ 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: () {},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ 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),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ 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
|
||||||
@@ -12,13 +13,17 @@ 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;
|
||||||
@@ -28,6 +33,7 @@ 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);
|
||||||
@@ -68,7 +74,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(
|
||||||
onTap: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selectedRulesetIndex == index) {
|
if (selectedRulesetIndex == index) {
|
||||||
selectedRulesetIndex = -1;
|
selectedRulesetIndex = -1;
|
||||||
@@ -90,4 +96,4 @@ class _ChooseRulesetViewState extends State<ChooseRulesetView> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,6 +10,7 @@ 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,6 +58,13 @@ 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;
|
||||||
@@ -64,6 +72,12 @@ 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();
|
||||||
@@ -96,8 +110,15 @@ 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),
|
||||||
@@ -107,8 +128,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,12 +161,39 @@ 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
|
||||||
@@ -230,6 +278,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ 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 {
|
||||||
@@ -30,11 +29,19 @@ class LicenseDetailView extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const ColoredIconContainer(
|
Container(
|
||||||
icon: Icons.description,
|
margin: const EdgeInsetsGeometry.only(right: 15),
|
||||||
margin: EdgeInsetsGeometry.only(right: 15),
|
width: 60,
|
||||||
containerSize: 60,
|
height: 60,
|
||||||
iconSize: 30,
|
decoration: BoxDecoration(
|
||||||
|
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,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ 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();
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -70,6 +70,7 @@ 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +85,6 @@ 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.contains(player);
|
return !selectedPlayers.any((p) => p.id == player.id);
|
||||||
}).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,9 +109,7 @@ 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.contains(
|
final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id);
|
||||||
player,
|
|
||||||
);
|
|
||||||
return nameMatches && isNotSelected;
|
return nameMatches && isNotSelected;
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@@ -126,46 +124,49 @@ class _PlayerSelectionState extends State<PlayerSelection> {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 50,
|
height: 50,
|
||||||
child: selectedPlayers.isEmpty
|
child: AppSkeleton(
|
||||||
? Center(child: Text(loc.no_players_selected))
|
enabled: isLoading,
|
||||||
: SingleChildScrollView(
|
child: selectedPlayers.isEmpty
|
||||||
scrollDirection: Axis.horizontal,
|
? Center(child: Text(loc.no_players_selected))
|
||||||
child: Row(
|
: SingleChildScrollView(
|
||||||
children: [
|
scrollDirection: Axis.horizontal,
|
||||||
for (var player in selectedPlayers)
|
child: Row(
|
||||||
Padding(
|
children: [
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
for (var player in selectedPlayers)
|
||||||
child: TextIconTile(
|
Padding(
|
||||||
text: player.name,
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
onIconTap: () {
|
child: TextIconTile(
|
||||||
setState(() {
|
text: player.name,
|
||||||
// Removes the player from the selection and notifies the parent.
|
onIconTap: () {
|
||||||
selectedPlayers.remove(player);
|
setState(() {
|
||||||
widget.onChanged([...selectedPlayers]);
|
// Removes the player from the selection and notifies the parent.
|
||||||
|
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(
|
||||||
@@ -245,7 +246,21 @@ 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];
|
||||||
suggestedPlayers = [...loadedPlayers];
|
if (widget.initialSelectedPlayers != null) {
|
||||||
|
// 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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
class CustomSearchBar extends StatelessWidget {
|
class CustomSearchBar extends StatelessWidget {
|
||||||
@@ -21,7 +22,6 @@ 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.
|
||||||
@@ -48,19 +48,15 @@ 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
|
/// Enforce maximum length on the input text
|
||||||
if (maxLength != null) {
|
const maxLength = Constants.MAX_PLAYER_NAME_LENGTH;
|
||||||
if (controller.text.length > maxLength!) {
|
if (controller.text.length > maxLength) {
|
||||||
controller.text = controller.text.substring(0, maxLength);
|
controller.text = controller.text.substring(0, maxLength);
|
||||||
controller.selection = TextSelection.fromPosition(
|
controller.selection = TextSelection.fromPosition(
|
||||||
TextPosition(offset: controller.text.length),
|
TextPosition(offset: controller.text.length),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return SearchBar(
|
return SearchBar(
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
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 {
|
||||||
@@ -8,18 +7,12 @@ class TextInputField extends StatelessWidget {
|
|||||||
/// - [onChanged]: Optional 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.
|
/// - [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.maxLength,
|
||||||
this.maxLines = 1,
|
|
||||||
this.minLines = 1,
|
|
||||||
this.showCounterText = false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/// The controller for the text input field.
|
/// The controller for the text input field.
|
||||||
@@ -34,30 +27,17 @@ class TextInputField extends StatelessWidget {
|
|||||||
/// Optional parameter for maximum length of the input text.
|
/// Optional parameter for maximum length of the input text.
|
||||||
final int? maxLength;
|
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,
|
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),
|
||||||
|
|||||||
@@ -3,17 +3,12 @@ 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 StatefulWidget {
|
class GroupTile extends StatelessWidget {
|
||||||
/// 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]: Callback function to be executed when the tile is tapped.
|
/// - [onTap]: An optional callback function to handle tap events.
|
||||||
const GroupTile({
|
const GroupTile({super.key, required this.group, this.isHighlighted = false, this.onTap});
|
||||||
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;
|
||||||
@@ -21,22 +16,17 @@ class GroupTile extends StatefulWidget {
|
|||||||
/// Whether the tile should be highlighted.
|
/// Whether the tile should be highlighted.
|
||||||
final bool isHighlighted;
|
final bool isHighlighted;
|
||||||
|
|
||||||
/// Callback function to be executed when the tile is tapped.
|
/// Callback function to handle tap events.
|
||||||
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: widget.onTap,
|
onTap: 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: widget.isHighlighted
|
decoration: isHighlighted
|
||||||
? CustomTheme.highlightedBoxDecoration
|
? CustomTheme.highlightedBoxDecoration
|
||||||
: CustomTheme.standardBoxDecoration,
|
: CustomTheme.standardBoxDecoration,
|
||||||
duration: const Duration(milliseconds: 150),
|
duration: const Duration(milliseconds: 150),
|
||||||
@@ -48,7 +38,7 @@ class _GroupTileState extends State<GroupTile> {
|
|||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.group.name,
|
group.name,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -59,7 +49,7 @@ class _GroupTileState extends State<GroupTile> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${widget.group.members.length}',
|
'${group.members.length}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
@@ -79,7 +69,7 @@ class _GroupTileState extends State<GroupTile> {
|
|||||||
runSpacing: 8.0,
|
runSpacing: 8.0,
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
for (var member in [
|
for (var member in [
|
||||||
...widget.group.members,
|
...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),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ 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.
|
||||||
@@ -38,9 +37,6 @@ 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();
|
||||||
}
|
}
|
||||||
@@ -55,7 +51,7 @@ class _InfoTileState extends State<InfoTile> {
|
|||||||
decoration: CustomTheme.standardBoxDecoration,
|
decoration: CustomTheme.standardBoxDecoration,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: widget.horizontalAlignment,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ 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.
|
||||||
@@ -30,10 +29,18 @@ class LicenseTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const ColoredIconContainer(
|
Container(
|
||||||
icon: Icons.description,
|
width: 50,
|
||||||
containerSize: 50,
|
height: 50,
|
||||||
iconSize: 32,
|
decoration: BoxDecoration(
|
||||||
|
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(
|
||||||
|
|||||||
@@ -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 '${loc.created_on} ${DateFormat.yMMMd(Localizations.localeOf(context).toString()).format(dateTime)}';
|
return DateFormat('MMM d, yyyy').format(dateTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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.
|
||||||
@@ -47,10 +46,18 @@ class SettingsListTile extends StatelessWidget {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
ColoredIconContainer(
|
Container(
|
||||||
icon: icon,
|
width: 44,
|
||||||
containerSize: 44,
|
height: 44,
|
||||||
iconSize: 28,
|
decoration: BoxDecoration(
|
||||||
|
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)),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ 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.
|
||||||
@@ -14,8 +13,7 @@ class TitleDescriptionListTile extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.description,
|
required this.description,
|
||||||
this.onTap,
|
this.onPressed,
|
||||||
this.onLongPress,
|
|
||||||
this.isHighlighted = false,
|
this.isHighlighted = false,
|
||||||
this.badgeText,
|
this.badgeText,
|
||||||
this.badgeColor,
|
this.badgeColor,
|
||||||
@@ -28,10 +26,7 @@ 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? onTap;
|
final VoidCallback? onPressed;
|
||||||
|
|
||||||
/// 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;
|
||||||
@@ -45,8 +40,7 @@ class TitleDescriptionListTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onPressed,
|
||||||
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),
|
||||||
@@ -79,6 +73,7 @@ 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,
|
||||||
|
|||||||
@@ -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.10+248
|
version: 0.0.7+216
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
|
|||||||
Reference in New Issue
Block a user