4 Commits

Author SHA1 Message Date
5da12939cb Updated Dart version
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m12s
Pull Request Pipeline / lint (pull_request) Successful in 2m18s
2026-01-12 20:30:57 +01:00
0d20b5847f Updated versions
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m6s
Pull Request Pipeline / lint (pull_request) Successful in 2m19s
2026-01-12 20:29:18 +01:00
973e327232 Merge remote-tracking branch 'origin/development' into enhancement/81-readme-vervollstaendigen 2026-01-12 20:29:06 +01:00
0f79495775 Implemented badges
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 2m25s
Pull Request Pipeline / test (pull_request) Successful in 2m27s
2025-12-06 15:42:02 +01:00
51 changed files with 775 additions and 1581 deletions

View File

@@ -1,7 +1,15 @@
# Game Tracker # Game Tracker
![Version](https://img.shields.io/badge/Version-0.3.0-orange) ![Flutter](https://img.shields.io/badge/Created_by-Liquid_Development-027DFD?logo=data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iRWJlbmVfMSIgZGF0YS1uYW1lPSJFYmVuZSAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA3MjUuNDggODk3LjMiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICNmZmY7CiAgICAgIH0KICAgIDwvc3R5bGU+CiAgPC9kZWZzPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTcwNS4yNiw3MDEuOTJsNi40LDExLjA4Yy0xLjk1LTMuODEtNC4wOS03LjUxLTYuNC0xMS4wOFoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik02MDIuMzksODk3LjI1aC03LjIxYzEuMi4wMywyLjQuMDUsMy42MS4wNXMyLjQxLS4wMiwzLjYxLS4wNVoiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik0wLDY5NS4zOGwyLjY4LTQuNjRjLS45MywxLjUyLTEuODIsMy4wNy0yLjY4LDQuNjRaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNjgyLjU1LDcyMy40NWw2LjA1LDEwLjQ5Yy0xLjc5LTMuNjQtMy44MS03LjE1LTYuMDUtMTAuNDlaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzcuNzIsNzMzLjI4bDUuMy05LjE4Yy0xLjk0LDIuOTQtMy43MSw2LjAxLTUuMyw5LjE4WiIvPgogIDxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTcxMS42Niw3MTMuMDFsLTYuNC0xMS4wOC0yMjAuNDYtMzgxLjg0aDBWMTAxLjg0YzIwLjY3LTYuOTgsMzUuNTYtMjYuNTIsMzUuNTYtNDkuNTQsMC0yOC44OC0yMy40MS01Mi4zLTUyLjMtNTIuM2gtMjA5LjQ4Yy0yOC44OCwwLTUyLjMsMjMuNDEtNTIuMyw1Mi4zLDAsMjIuNzEsMTQuNDgsNDIuMDMsMzQuNyw0OS4yNXYyMTguNTRsLS4zMy41OEwxOC44OSw3MDQuNzlsLTIuNjgsNC42NGMtOS45OSwxOC4xMi0xNS42OCwzOC45Ni0xNS42OCw2MS4xMiwwLDY5Ljk3LDU2LjY0LDEyNi43LDEyNi41MSwxMjYuN2g0NzUuMzVjNjguMy0xLjkxLDEyMy4wOS01Ny44OCwxMjMuMDktMTI2LjY0LDAtMjAuNzQtNC45OS00MC4zMi0xMy44Mi01Ny42Wk02MDguNTYsODYyLjUzSDExNy40M2MtNDkuMzcsMC04OS4zOS00MC4wMi04OS4zOS04OS4zOSwwLTE0LjM2LDMuMzktMjcuOTMsOS40MS0zOS45Nmw1LjMtOS4xOCwyMzMuMi00MDMuOTJoLS4wOFYxMDQuNTloMTcuODFjOS40NywwLDE3LjE1LTcuNjgsMTcuMTUtMTcuMTVzLTcuNjgtMTcuMTUtMTcuMTUtMTcuMTVoLTM1LjU5di0uMDJjLTkuNzItLjI2LTE3LjUyLTguMi0xNy41Mi0xNy45OHM3LjgtMTcuNzIsMTcuNTItMTcuOTh2LS4wMmgyMDkuMjZjOS45NCwwLDE4LDguMDYsMTgsMThzLTguMDYsMTgtMTgsMThoLTM0LjQ4Yy05LjQ3LDAtMTcuMTUsNy42OC0xNy4xNSwxNy4xNXM3LjY4LDE3LjE1LDE3LjE1LDE3LjE1aDE3LjA0djIxNS40OWguMDdsMjMyLjgyLDQwMy4yNiw2LjA2LDEwLjVjNS44MiwxMS44Niw5LjA5LDI1LjIsOS4wOSwzOS4zLDAsNDkuMzctNDAuMDIsODkuMzktODkuMzksODkuMzlaIi8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNMzgxLjY4LDU0NS4zOGMtMy4wOCwxLjY4LTYuMTgsMy4zLTkuMzIsNC44NiwzLjA3LTEuNjcsNi4xOC0zLjI5LDkuMzItNC44NloiLz4KICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik01ODMuNDIsNTUxLjE5bC0yMC42Ny0zNS44Yy0xMy42OS0xLjg0LTI3LjY3LTIuNzktNDEuODYtMi43OS0xNy45OSwwLTM1LjYyLDEuNTMtNTIuNzgsNC40Ni0zMC41Niw1LjIxLTU5LjYsMTQuODktODYuNDIsMjguMzMtMy4wOCwxLjY4LTYuMTgsMy4zLTkuMzIsNC44Ni00MS44OCwyMC45OS04OS4xNiwzMi43OS0xMzkuMTksMzIuNzktMzQuODUsMC02OC4zNS01Ljc0LTk5LjYzLTE2LjMxLDAsMCwwLC4wMiwwLC4wMmwtMTYuNTIsMjguNjFjMzcuMDEsMTUuNTMsNzcuNjUsMjQuMTIsMTIwLjMsMjQuMTIsMTcuOTgsMCwzNS42MS0xLjUzLDUyLjc2LTQuNDYsMzIuNzctNS41OSw2My43OC0xNi4zMSw5Mi4yLTMxLjI5Ljg3LS40NiwxLjczLS45MiwyLjYtMS40LDQzLjI5LTIyLjgyLDkyLjYyLTM1Ljc0LDE0NC45Ni0zNS43NCwxOC4yOCwwLDM2LjE4LDEuNTksNTMuNTksNC42MWwtLjAyLS4wMloiLz4KICA8Zz4KICAgIDxjaXJjbGUgY2xhc3M9ImNscy0xIiBjeD0iNTg3LjY0IiBjeT0iODAzLjQiIHI9IjE4Ljk2Ii8+CiAgICA8cGF0aCBjbGFzcz0iY2xzLTEiIGQ9Ik01MTUuNTIsNzg0LjQzSDEwMy41NWMtMTAuOTIsMC0xOS43Niw4LjQ5LTE5Ljc2LDE4Ljk2czguODUsMTguOTYsMTkuNzYsMTguOTZoNDExLjk3YzEwLjkyLDAsMTkuNzYtOC40OSwxOS43Ni0xOC45NnMtOC44NS0xOC45Ni0xOS43Ni0xOC45NloiLz4KICA8L2c+CiAgPGNpcmNsZSBjbGFzcz0iY2xzLTEiIGN4PSIyODMuMzIiIGN5PSI0NjcuNTkiIHI9IjE4Ljk2Ii8+CiAgPGNpcmNsZSBjbGFzcz0iY2xzLTEiIGN4PSIzMjYuMjMiIGN5PSIzNjYuMjUiIHI9IjE4Ljk2Ii8+CiAgPHBhdGggY2xhc3M9ImNscy0xIiBkPSJNNDA2LjU2LDM4NS4yMmMtMjQuMDYsMC00My41NiwxOS41LTQzLjU2LDQzLjU2czE5LjUsNDMuNTYsNDMuNTYsNDMuNTYsNDMuNTYtMTkuNSw0My41Ni00My41Ni0xOS41LTQzLjU2LTQzLjU2LTQzLjU2Wk00MDYuNTYsNDQ3Ljc0Yy0xMC40NywwLTE4Ljk2LTguNDktMTguOTYtMTguOTZzOC40OS0xOC45NiwxOC45Ni0xOC45NiwxOC45Niw4LjQ5LDE4Ljk2LDE4Ljk2LTguNDksMTguOTYtMTguOTYsMTguOTZaIi8+Cjwvc3ZnPg==)
![Flutter](https://img.shields.io/badge/Flutter-3.32.1-blue?logo=flutter)
![Dart](https://img.shields.io/badge/Dart-3.8.1-blue?logo=dart) ![Version](https://img.shields.io/badge/App--Version-Alpha-orange)
![Flutter](https://img.shields.io/badge/Flutter-3.38.6-027DFD?logo=flutter)
![Dart](https://img.shields.io/badge/Dart-3.10.7-027DFD?logo=dart)
### Versions Supported
![iOS18](https://img.shields.io/badge/iOS-18.7.1-white?logo=apple)
![iOS26](https://img.shields.io/badge/iOS-26.2-white?logo=apple)
![Android16](https://img.shields.io/badge/Android-16-3DDC84?logo=android)
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games. A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.

View File

@@ -11,4 +11,3 @@ linter:
prefer_const_literals_to_create_immutables: true prefer_const_literals_to_create_immutables: true
unnecessary_const: true unnecessary_const: true
lines_longer_than_80_chars: false lines_longer_than_80_chars: false
constant_identifier_names: false

View File

@@ -1,11 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Returns a platform-adaptive page route based on the current platform.
/// - On iOS, it returns a [CupertinoPageRoute].
/// - On other platforms, it returns a [MaterialPageRoute].
Route<T> adaptivePageRoute<T>({ Route<T> adaptivePageRoute<T>({
required Widget Function(BuildContext) builder, required Widget Function(BuildContext) builder,
bool fullscreenDialog = false, bool fullscreenDialog = false,

View File

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

View File

@@ -1,38 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Theme class that defines colors, border radius, padding, and decorations
class CustomTheme { 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);
/// Unselected color for the [NavbarItem]
static Color navBarItemUnselectedColor = Colors.grey.shade400;
// ==================== Border Radius ==================== // ==================== Border Radius ====================
static const double standardBorderRadius = 12.0; static const double standardBorderRadius = 12.0;
static BorderRadius get standardBorderRadiusAll => static BorderRadius get standardBorderRadiusAll =>

View File

@@ -4,7 +4,6 @@
"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",
@@ -14,7 +13,6 @@
"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",
@@ -22,13 +20,7 @@
"days_ago": "vor {count} Tagen", "days_ago": "vor {count} Tagen",
"delete": "Löschen", "delete": "Löschen",
"delete_all_data": "Alle Daten löschen", "delete_all_data": "Alle Daten löschen",
"delete_group": "Diese Gruppe löschen",
"edit_group": "Gruppe bearbeiten",
"delete_group": "Gruppe löschen",
"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 +29,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 +42,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 +57,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",
@@ -90,7 +79,7 @@
"stats": "Statistiken", "stats": "Statistiken",
"successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt", "successfully_added_player": "Spieler:in {playerName} erfolgreich hinzugefügt",
"there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht", "there_is_no_group_matching_your_search": "Es gibt keine Gruppe, die deiner Suche entspricht",
"this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden.", "this_cannot_be_undone": "Dies kann nicht rückgängig gemacht werden",
"today_at": "Heute um", "today_at": "Heute um",
"undo": "Rückgängig", "undo": "Rückgängig",
"unknown_exception": "Unbekannter Fehler (siehe Konsole)", "unknown_exception": "Unbekannter Fehler (siehe Konsole)",

View File

@@ -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"
}, },
@@ -37,13 +34,10 @@
"description": "Button text to create a match" "description": "Button text to create a match"
}, },
"@create_new_group": { "@create_new_group": {
"description": "Appbar text to create a new group" "description": "Button text to create a new group"
}, },
"@create_new_match": { "@create_new_match": {
"description": "Appbar text to create a new match" "description": "Button text to create a new match"
},
"@created_on": {
"description": "Label for creation date"
}, },
"@data": { "@data": {
"description": "Data label" "description": "Data label"
@@ -71,21 +65,9 @@
"@delete_all_data": { "@delete_all_data": {
"description": "Confirmation dialog for deleting all data" "description": "Confirmation dialog for deleting all data"
}, },
"@delete_group": {
"description": "Confirmation dialog for deleting a group"
},
"@edit_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"
}, },
@@ -110,9 +92,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"
}, },
@@ -152,9 +131,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"
}, },
@@ -200,9 +176,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"
}, },
@@ -308,7 +281,6 @@
"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",
@@ -317,7 +289,6 @@
"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",
@@ -326,11 +297,7 @@
"days_ago": "{count} days ago", "days_ago": "{count} days ago",
"delete": "Delete", "delete": "Delete",
"delete_all_data": "Delete all data", "delete_all_data": "Delete all data",
"delete_group": "Delete 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",
@@ -339,7 +306,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",
@@ -353,7 +319,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",
@@ -369,7 +334,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",
@@ -392,7 +356,7 @@
"stats": "Stats", "stats": "Stats",
"successfully_added_player": "Successfully added player {playerName}", "successfully_added_player": "Successfully added player {playerName}",
"there_is_no_group_matching_your_search": "There is no group matching your search", "there_is_no_group_matching_your_search": "There is no group matching your search",
"this_cannot_be_undone": "This can't be undone.", "this_cannot_be_undone": "This can't be undone",
"today_at": "Today at", "today_at": "Today at",
"undo": "Undo", "undo": "Undo",
"unknown_exception": "Unknown Exception (see console)", "unknown_exception": "Unknown Exception (see console)",

View File

@@ -122,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,19 +164,13 @@ abstract class AppLocalizations {
/// **'Create match'** /// **'Create match'**
String get create_match; String get create_match;
/// Appbar text to create a new group /// Button text to create a new group
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create new group'** /// **'Create new group'**
String get create_new_group; String get create_new_group;
/// Label for creation date /// Button text to create a new match
///
/// In en, this message translates to:
/// **'Created on'**
String get created_on;
/// Appbar text to create a new match
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Create new match'** /// **'Create new match'**
@@ -230,36 +218,12 @@ abstract class AppLocalizations {
/// **'Delete all data'** /// **'Delete all data'**
String get delete_all_data; String get delete_all_data;
/// Confirmation dialog for deleting a group
///
/// In en, this message translates to:
/// **'Delete Group'**
String get delete_group;
/// Button & Appbar label for editing a group
///
/// In en, this message translates to:
/// **'Edit Group'**
String get edit_group;
/// Error message when group creation fails /// Error message when group creation fails
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'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:
@@ -308,12 +272,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:
@@ -392,12 +350,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:
@@ -488,12 +440,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:
@@ -629,7 +575,7 @@ abstract class AppLocalizations {
/// Warning message for irreversible actions /// Warning message for irreversible actions
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'This can\'t be undone.'** /// **'This can\'t be undone'**
String get this_cannot_be_undone; String get this_cannot_be_undone;
/// Date format for today /// Date format for today

View File

@@ -20,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';
@@ -49,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';
@@ -78,24 +72,10 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get delete_all_data => 'Alle Daten löschen'; String get delete_all_data => 'Alle Daten löschen';
@override
String get delete_group => 'Gruppe löschen';
@override
String get edit_group => 'Gruppe bearbeiten';
@override @override
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';
@@ -120,9 +100,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';
@@ -162,9 +139,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';
@@ -211,9 +185,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';
@@ -291,7 +262,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get this_cannot_be_undone => String get this_cannot_be_undone =>
'Dies kann nicht rückgängig gemacht werden.'; 'Dies kann nicht rückgängig gemacht werden';
@override @override
String get today_at => 'Heute um'; String get today_at => 'Heute um';

View File

@@ -20,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';
@@ -49,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';
@@ -78,24 +72,10 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get delete_all_data => 'Delete all data'; String get delete_all_data => 'Delete all data';
@override
String get delete_group => 'Delete Group';
@override
String get edit_group => 'Edit Group';
@override @override
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';
@@ -120,9 +100,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';
@@ -162,9 +139,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';
@@ -211,9 +185,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';
@@ -290,7 +261,7 @@ class AppLocalizationsEn extends AppLocalizations {
'There is no group matching your search'; 'There is no group matching your search';
@override @override
String get this_cannot_be_undone => 'This can\'t be undone.'; String get this_cannot_be_undone => 'This can\'t be undone';
@override @override
String get today_at => 'Today at'; String get today_at => 'Today at';

View File

@@ -1,10 +1,8 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/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/group_view/group_view.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/groups_view.dart';
import 'package:game_tracker/presentation/views/main_menu/home_view.dart'; import 'package:game_tracker/presentation/views/main_menu/home_view.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_view.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view/settings_view.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/settings_view.dart';
@@ -12,8 +10,6 @@ import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart';
import 'package:game_tracker/presentation/widgets/navbar_item.dart'; import 'package:game_tracker/presentation/widgets/navbar_item.dart';
class CustomNavigationBar extends StatefulWidget { class CustomNavigationBar extends StatefulWidget {
/// A custom navigation bar widget that provides tabbed navigation
/// between different views: Home, Matches, Groups, and Statistics.
const CustomNavigationBar({super.key}); const CustomNavigationBar({super.key});
@override @override
@@ -75,66 +71,18 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
body: tabs[currentIndex], body: tabs[currentIndex],
extendBody: true, extendBody: true,
bottomNavigationBar: SizedBox( bottomNavigationBar: SafeArea(
height: 70 + MediaQuery.of(context).padding.bottom, minimum: const EdgeInsets.only(bottom: 30),
child: Stack(
children: [
// Dynamically generated blur layers for ultra-smooth transition
...List.generate(34, (index) {
// Use cubic curve for an even more natural, smoother transition
final progress = index / 34.0; // 0.0 to 1.0
final cubic = progress * progress * progress; // cubic curve
final blurStrength =
0.5 + (cubic * 50.0); // Very smooth from 0.5 to 50.5
// Height goes completely from 100% to 0% (all the way down)
// With extra density at the bottom for softer transition
final heightFactor = index < 25
// First 25 layers: 100% to 30%
? 1.0 - (progress * 0.7)
// Last 10 layers: 30% to 0% (denser)
: 0.3 - ((index - 25) / 34.0);
return Positioned(
left: 0,
right: 0,
bottom: 0,
height:
(70 + MediaQuery.of(context).padding.bottom) *
heightFactor.clamp(0.05, 1.0),
child: ClipRect(
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: blurStrength,
sigmaY: blurStrength,
),
child: Container(color: Colors.transparent),
),
),
);
}),
// Gradient overlay
Positioned.fill(
child: Container( child: Container(
margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( borderRadius: BorderRadius.circular(24),
begin: Alignment.bottomCenter, color: CustomTheme.primaryColor,
end: Alignment.topCenter,
colors: [
CustomTheme.boxColor.withValues(alpha: 1),
CustomTheme.boxColor.withValues(alpha: 0.5),
CustomTheme.boxColor.withValues(alpha: 0.2),
CustomTheme.boxColor.withValues(alpha: 0.0),
],
stops: const [0.0, 0.4, 0.8, 1],
), ),
), child: ClipRRect(
), borderRadius: BorderRadius.circular(24),
),
// Navbar content
SafeArea(
child: SizedBox( child: SizedBox(
height: 70, height: 60,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround, mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[ children: <Widget>[
@@ -170,7 +118,6 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
), ),
), ),
), ),
],
), ),
), ),
); );

View File

@@ -11,10 +11,7 @@ import 'package:game_tracker/presentation/widgets/text_input/text_input_field.da
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CreateGroupView extends StatefulWidget { class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key, this.groupToEdit}); const CreateGroupView({super.key});
/// The group to edit, if any
final Group? groupToEdit;
@override @override
State<CreateGroupView> createState() => _CreateGroupViewState(); State<CreateGroupView> createState() => _CreateGroupViewState();
@@ -23,29 +20,16 @@ class CreateGroupView extends StatefulWidget {
class _CreateGroupViewState extends State<CreateGroupView> { class _CreateGroupViewState extends State<CreateGroupView> {
late final AppDatabase db; late final AppDatabase db;
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
/// Controller for the group name input field /// Controller for the group name input field
final _groupNameController = TextEditingController(); final _groupNameController = TextEditingController();
/// List of currently selected players /// List of currently selected players
List<Player> selectedPlayers = []; List<Player> selectedPlayers = [];
/// List of initially selected players (when editing a group)
List<Player> initialSelectedPlayers = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); 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(() { _groupNameController.addListener(() {
setState(() {}); setState(() {});
}); });
@@ -61,42 +45,9 @@ class _CreateGroupViewState extends State<CreateGroupView> {
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(widget.groupToEdit == null ? loc.create_new_group : loc.edit_group), actions: widget.groupToEdit == null ? [] : [IconButton(icon: const Icon(Icons.delete), onPressed: () async { appBar: AppBar(title: Text(loc.create_new_group)),
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( body: SafeArea(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
@@ -110,7 +61,6 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
Expanded( Expanded(
child: PlayerSelection( child: PlayerSelection(
initialSelectedPlayers: initialSelectedPlayers,
onChanged: (value) { onChanged: (value) {
setState(() { setState(() {
selectedPlayers = [...value]; selectedPlayers = [...value];
@@ -119,7 +69,7 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
), ),
CustomWidthButton( CustomWidthButton(
text: widget.groupToEdit == null ? loc.create_group : loc.edit_group, text: loc.create_group,
sizeRelativeToWidth: 0.95, sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary, buttonType: ButtonType.primary,
onPressed: onPressed:
@@ -127,34 +77,29 @@ class _CreateGroupViewState extends State<CreateGroupView> {
(selectedPlayers.length < 2)) (selectedPlayers.length < 2))
? null ? null
: () async { : () async {
late Group? updatedGroup; bool success = await db.groupDao.addGroup(
late bool success;
if (widget.groupToEdit == null) {
success = await db.groupDao.addGroup(
group: Group( group: Group(
name: _groupNameController.text.trim(), name: _groupNameController.text.trim(),
members: selectedPlayers, members: selectedPlayers,
), ),
); );
} else {
updatedGroup = Group(
id: widget.groupToEdit!.id,
name: _groupNameController.text.trim(),
members: selectedPlayers,
);
//TODO: Implement group editing in database
/*
success = await db.groupDao.updateGroup(
group: updatedGroup,
);
*/
success = true;
}
if (!context.mounted) return; if (!context.mounted) return;
if (success) { if (success) {
Navigator.pop(context, updatedGroup); Navigator.pop(context);
} else { } else {
showSnackbar(message: widget.groupToEdit == null ? loc.error_creating_group : loc.error_editing_group); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
AppLocalizations.of(
context,
).error_creating_group,
style: const TextStyle(color: Colors.white),
),
),
),
);
} }
}, },
), ),
@@ -165,21 +110,4 @@ class _CreateGroupViewState extends State<CreateGroupView> {
), ),
); );
} }
/// Displays a snackbar with the given message and optional action.
///
/// [message] The message to display in the snackbar.
void showSnackbar({
required String message,
}) {
final messenger = _scaffoldMessengerKey.currentState;
if (messenger != null) {
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.boxColor,
),
);
}
}
} }

View File

@@ -1,279 +0,0 @@
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/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/views/main_menu/group_view/create_group_view.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 GroupDetailView extends StatefulWidget {
/// A view that displays the profile of a group
/// - [group]: The group to display
const GroupDetailView({
super.key,
required this.group,
required this.callback,
});
/// The group to display
final Group group;
final VoidCallback callback;
@override
State<GroupDetailView> createState() => _GroupDetailViewState();
}
class _GroupDetailViewState extends State<GroupDetailView> {
late final AppDatabase db;
bool isLoading = true;
late Group _group;
/// Total matches played in this group
int totalMatches = 0;
String bestPlayer = '';
@override
void initState() {
super.initState();
_group = widget.group;
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: _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(
_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(_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: _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,
_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: () async {
final updatedGroup = await Navigator.push<Group?>(
context,
adaptivePageRoute(
builder: (context) {
return CreateGroupView(
groupToEdit: _group,
);
},
),
);
if (updatedGroup != null && mounted) {
setState(() {
_group = updatedGroup;
});
_loadStatistics();
widget.callback();
}
},
),
),
],
),
),
);
}
/// 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 == _group.id).toList();
setState(() {
totalMatches = groupMatches.length;
bestPlayer = _getBestPlayer(groupMatches);
isLoading = false;
});
}
/// Determines the best player in the group based on match wins
String _getBestPlayer(List<Match> matches) {
final bestPlayerCounts = <Player, int>{};
// Count wins for each player
for (var match in matches) {
if (match.winner != null) {
bestPlayerCounts.update(
match.winner!,
(value) => value + 1,
ifAbsent: () => 1,
);
}
}
// Sort players by win count
final sortedPlayers = bestPlayerCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
// Get the best player
bestPlayer = sortedPlayers.isNotEmpty ? sortedPlayers.first.key.name : '-';
return bestPlayer;
}
}

View File

@@ -6,16 +6,14 @@ import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/group_detail_view.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_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/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class GroupsView extends StatefulWidget { class GroupsView extends StatefulWidget {
/// A view that displays a list of groups
const GroupsView({super.key}); const GroupsView({super.key});
@override @override
@@ -75,31 +73,16 @@ class _GroupsViewState extends State<GroupsView> {
height: MediaQuery.paddingOf(context).bottom - 20, height: MediaQuery.paddingOf(context).bottom - 20,
); );
} }
return GroupTile( return GroupTile(group: groups[index]);
group: groups[index],
onTap: () async {
await Navigator.push(
context,
adaptivePageRoute(
builder: (context) {
return GroupDetailView(
group: groups[index],
callback: loadGroups,
);
},
),
);
},
);
}, },
), ),
), ),
), ),
Positioned( Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20, bottom: MediaQuery.paddingOf(context).bottom,
child: MainMenuButton( child: CustomWidthButton(
text: loc.create_group, text: loc.create_group,
icon: Icons.group_add, sizeRelativeToWidth: 0.90,
onPressed: () async { onPressed: () async {
await Navigator.push( await Navigator.push(
context, context,
@@ -121,12 +104,9 @@ 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.minimumSkeletonDuration),
]).then((results) { ]).then((results) {
loadedGroups = results[0] as List<Group>; loadedGroups = results[0] as List<Group>;
setState(() { setState(() {

View File

@@ -15,8 +15,6 @@ import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class HomeView extends StatefulWidget { class HomeView extends StatefulWidget {
/// The main home view of the application, displaying quick info,
/// recent matches, and quick create options.
const HomeView({super.key}); const HomeView({super.key});
@override @override
@@ -195,7 +193,7 @@ class _HomeViewState extends State<HomeView> {
db.matchDao.getMatchCount(), db.matchDao.getMatchCount(),
db.groupDao.getGroupCount(), db.groupDao.getGroupCount(),
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION), Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) { ]).then((results) {
matchCount = results[0] as int; matchCount = results[0] as int;
groupCount = results[1] as int; groupCount = results[1] as int;

View File

@@ -6,21 +6,15 @@ import 'package:game_tracker/presentation/widgets/text_input/custom_search_bar.d
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 ChooseGameView extends StatefulWidget { class ChooseGameView extends StatefulWidget {
/// A view that allows the user to choose a game from a list of available games final List<(String, String, Ruleset)> games;
/// - [games]: A list of tuples containing the game name, description and ruleset final int initialGameIndex;
/// - [initialGameIndex]: The index of the initially selected game
const ChooseGameView({ const ChooseGameView({
super.key, super.key,
required this.games, required this.games,
required this.initialGameIndex, required this.initialGameIndex,
}); });
/// A list of tuples containing the game name, description and ruleset
final List<(String, String, Ruleset)> games;
/// The index of the initially selected game
final int initialGameIndex;
@override @override
State<ChooseGameView> createState() => _ChooseGameViewState(); State<ChooseGameView> createState() => _ChooseGameViewState();
} }
@@ -43,7 +37,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),

View File

@@ -7,21 +7,15 @@ import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
class ChooseGroupView extends StatefulWidget { class ChooseGroupView extends StatefulWidget {
/// A view that allows the user to choose a group from a list of groups. final List<Group> groups;
/// - [groups]: A list of available groups to choose from final String initialGroupId;
/// - [initialGroupId]: The ID of the initially selected group
const ChooseGroupView({ const ChooseGroupView({
super.key, super.key,
required this.groups, required this.groups,
required this.initialGroupId, required this.initialGroupId,
}); });
/// A list of available groups to choose from
final List<Group> groups;
/// The ID of the initially selected group
final String initialGroupId;
@override @override
State<ChooseGroupView> createState() => _ChooseGroupViewState(); State<ChooseGroupView> createState() => _ChooseGroupViewState();
} }
@@ -43,7 +37,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),
@@ -150,8 +143,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
(group) => (group) =>
group.name.toLowerCase().contains(query.toLowerCase()) || group.name.toLowerCase().contains(query.toLowerCase()) ||
group.members.any( group.members.any(
(player) => (player) => player.name.toLowerCase().contains(query.toLowerCase()),
player.name.toLowerCase().contains(query.toLowerCase()),
), ),
), ),
); );

View File

@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/title_description_list_tile.dart';
class ChooseRulesetView extends StatefulWidget {
final List<(Ruleset, String)> rulesets;
final int initialRulesetIndex;
const ChooseRulesetView({
super.key,
required this.rulesets,
required this.initialRulesetIndex,
});
@override
State<ChooseRulesetView> createState() => _ChooseRulesetViewState();
}
class _ChooseRulesetViewState extends State<ChooseRulesetView> {
/// Currently selected ruleset index
late int selectedRulesetIndex;
@override
void initState() {
selectedRulesetIndex = widget.initialRulesetIndex;
super.initState();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return DefaultTabController(
length: 2,
initialIndex: 0,
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
),
title: Text(loc.choose_ruleset),
),
body: PopScope(
// This fixes that the Android Back Gesture didn't return the
// selectedRulesetIndex and therefore the selected Ruleset wasn't saved
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) {
if (didPop) {
return;
}
Navigator.of(context).pop(
selectedRulesetIndex == -1
? null
: widget.rulesets[selectedRulesetIndex].$1,
);
},
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 85),
itemCount: widget.rulesets.length,
itemBuilder: (BuildContext context, int index) {
return TitleDescriptionListTile(
onPressed: () async {
setState(() {
if (selectedRulesetIndex == index) {
selectedRulesetIndex = -1;
} else {
selectedRulesetIndex = index;
}
});
},
title: translateRulesetToString(
widget.rulesets[index].$1,
context,
),
description: widget.rulesets[index].$2,
isHighlighted: selectedRulesetIndex == index,
);
},
),
),
),
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
@@ -10,6 +9,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';
@@ -18,12 +18,8 @@ import 'package:game_tracker/presentation/widgets/tiles/choose_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class CreateMatchView extends StatefulWidget { class CreateMatchView extends StatefulWidget {
/// A view that allows creating a new match
/// [onWinnerChanged]: Optional callback invoked when the winner is changed
const CreateMatchView({super.key, this.onWinnerChanged});
/// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
const CreateMatchView({super.key, this.onWinnerChanged});
@override @override
State<CreateMatchView> createState() => _CreateMatchViewState(); State<CreateMatchView> createState() => _CreateMatchViewState();
@@ -57,6 +53,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,8 +67,8 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// The currently selected players /// The currently selected players
List<Player>? selectedPlayers; List<Player>? selectedPlayers;
/// GlobalKey for ScaffoldMessenger to show snackbars /// List of available rulesets with their localized string representations
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>(); late final List<(Ruleset, String)> _rulesets;
@override @override
void initState() { void initState() {
@@ -99,8 +102,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),
@@ -110,9 +120,7 @@ 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(
@@ -124,7 +132,6 @@ class _CreateMatchViewState extends State<CreateMatchView> {
child: TextInputField( child: TextInputField(
controller: _matchNameController, controller: _matchNameController,
hintText: hintText ?? '', hintText: hintText ?? '',
maxLength: Constants.MAX_MATCH_NAME_LENGTH,
), ),
), ),
ChooseTile( ChooseTile(
@@ -144,12 +151,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
@@ -168,8 +202,7 @@ class _CreateMatchViewState extends State<CreateMatchView> {
if (selectedGroup != null) { if (selectedGroup != null) {
filteredPlayerList = playerList filteredPlayerList = playerList
.where( .where(
(p) => (p) => !selectedGroup!.members.any((m) => m.id == p.id),
!selectedGroup!.members.any((m) => m.id == p.id),
) )
.toList(); .toList();
} else { } else {
@@ -234,6 +267,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;
} }
} }

View File

@@ -8,17 +8,11 @@ import 'package:game_tracker/presentation/widgets/tiles/custom_radio_list_tile.d
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MatchResultView extends StatefulWidget { class MatchResultView extends StatefulWidget {
/// A view that allows selecting and saving the winner of a match
/// [match]: The match for which the winner is to be selected
/// [onWinnerChanged]: Optional callback invoked when the winner is changed
const MatchResultView({super.key, required this.match, this.onWinnerChanged});
/// The match for which the winner is to be selected
final Match match; final Match match;
/// Optional callback invoked when the winner is changed
final VoidCallback? onWinnerChanged; final VoidCallback? onWinnerChanged;
const MatchResultView({super.key, required this.match, this.onWinnerChanged});
@override @override
State<MatchResultView> createState() => _MatchResultViewState(); State<MatchResultView> createState() => _MatchResultViewState();
} }

View File

@@ -1,7 +1,6 @@
import 'dart:core' hide Match; import 'dart:core' hide Match;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttericon/rpg_awesome_icons.dart';
import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
@@ -13,13 +12,12 @@ import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_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/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/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MatchView extends StatefulWidget { class MatchView extends StatefulWidget {
/// A view that displays a list of matches
const MatchView({super.key}); const MatchView({super.key});
@override @override
@@ -106,10 +104,10 @@ class _MatchViewState extends State<MatchView> {
), ),
), ),
Positioned( Positioned(
bottom: MediaQuery.paddingOf(context).bottom + 20, bottom: MediaQuery.paddingOf(context).bottom,
child: MainMenuButton( child: CustomWidthButton(
text: 'Spiel erstellen', text: loc.create_match,
icon: RpgAwesome.clovers_card, sizeRelativeToWidth: 0.90,
onPressed: () async { onPressed: () async {
Navigator.push( Navigator.push(
context, context,
@@ -130,7 +128,7 @@ class _MatchViewState extends State<MatchView> {
void loadGames() { void loadGames() {
Future.wait([ Future.wait([
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION), Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) { ]).then((results) {
if (mounted) { if (mounted) {
setState(() { setState(() {

View File

@@ -2,17 +2,13 @@ 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 {
/// A detailed view displaying information about a software package license.
/// - [package]: The package data to be displayed.
const LicenseDetailView({super.key, required this.package});
/// The package data to be displayed.
final Package package; final Package package;
const LicenseDetailView({super.key, required this.package});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
@@ -30,11 +26,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,

View File

@@ -5,7 +5,6 @@ import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses
import 'package:game_tracker/presentation/widgets/tiles/license_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/license_tile.dart';
class LicensesView extends StatelessWidget { class LicensesView extends StatelessWidget {
/// A view that displays a list of open source licenses used in the app
const LicensesView({super.key}); const LicensesView({super.key});
@override @override

View File

@@ -7,8 +7,6 @@ import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses_view.dart'; import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses_view.dart';
import 'package:game_tracker/presentation/widgets/buttons/animated_dialog_button.dart';
import 'package:game_tracker/presentation/widgets/custom_alert_dialog.dart';
import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/settings_list_tile.dart';
import 'package:game_tracker/services/data_transfer_service.dart'; import 'package:game_tracker/services/data_transfer_service.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
@@ -16,8 +14,6 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class SettingsView extends StatefulWidget { class SettingsView extends StatefulWidget {
/// The settings view of the application, allowing users to manage data
/// and view legal information.
const SettingsView({super.key}); const SettingsView({super.key});
@override @override
@@ -31,7 +27,6 @@ class _SettingsViewState extends State<SettingsView> {
version: 'n.A.', version: 'n.A.',
buildNumber: 'n.A.', buildNumber: 'n.A.',
); );
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -42,9 +37,7 @@ class _SettingsViewState extends State<SettingsView> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return ScaffoldMessenger(
child: Builder( child: Scaffold(
builder: (scaffoldMessengerContext) {
return Scaffold(
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
body: SingleChildScrollView( body: SingleChildScrollView(
@@ -64,11 +57,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10),
left: 16,
top: 10,
bottom: 10,
),
child: Text( child: Text(
textAlign: TextAlign.start, textAlign: TextAlign.start,
loc.data, loc.data,
@@ -84,18 +73,13 @@ class _SettingsViewState extends State<SettingsView> {
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async { onPressed: () async {
final String json = final String json =
await DataTransferService.getAppDataAsJson( await DataTransferService.getAppDataAsJson(context);
scaffoldMessengerContext,
);
final result = await DataTransferService.exportData( final result = await DataTransferService.exportData(
json, json,
'game_tracker-data', 'game_tracker-data',
); );
if (!scaffoldMessengerContext.mounted) return; if (!context.mounted) return;
showExportSnackBar( showExportSnackBar(context: context, result: result);
context: scaffoldMessengerContext,
result: result,
);
}, },
), ),
SettingsListTile( SettingsListTile(
@@ -103,14 +87,9 @@ class _SettingsViewState extends State<SettingsView> {
icon: Icons.download, icon: Icons.download,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async { onPressed: () async {
final result = await DataTransferService.importData( final result = await DataTransferService.importData(context);
scaffoldMessengerContext, if (!context.mounted) return;
); showImportSnackBar(context: context, result: result);
if (!scaffoldMessengerContext.mounted) return;
showImportSnackBar(
context: scaffoldMessengerContext,
result: result,
);
}, },
), ),
SettingsListTile( SettingsListTile(
@@ -120,27 +99,17 @@ class _SettingsViewState extends State<SettingsView> {
onPressed: () { onPressed: () {
showDialog<bool>( showDialog<bool>(
context: context, context: context,
builder: (context) => CustomAlertDialog( builder: (context) => AlertDialog(
title: '${loc.delete_all_data}?', title: Text('${loc.delete_all_data}?'),
content: loc.this_cannot_be_undone, content: Text(loc.this_cannot_be_undone),
actions: [ actions: [
AnimatedDialogButton( TextButton(
onPressed: () => Navigator.of(context).pop(false), onPressed: () => Navigator.of(context).pop(false),
child: Text( child: Text(loc.cancel),
loc.cancel,
style: const TextStyle(
color: CustomTheme.textColor,
), ),
), TextButton(
),
AnimatedDialogButton(
onPressed: () => Navigator.of(context).pop(true), onPressed: () => Navigator.of(context).pop(true),
child: Text( child: Text(loc.delete),
loc.delete,
style: TextStyle(
color: CustomTheme.secondaryColor,
),
),
), ),
], ],
), ),
@@ -148,7 +117,7 @@ class _SettingsViewState extends State<SettingsView> {
if (confirmed == true && context.mounted) { if (confirmed == true && context.mounted) {
DataTransferService.deleteAllData(context); DataTransferService.deleteAllData(context);
showSnackbar( showSnackbar(
context: scaffoldMessengerContext, context: context,
message: AppLocalizations.of( message: AppLocalizations.of(
context, context,
).data_successfully_deleted, ).data_successfully_deleted,
@@ -158,11 +127,7 @@ class _SettingsViewState extends State<SettingsView> {
}, },
), ),
Padding( Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10),
left: 16,
top: 10,
bottom: 10,
),
child: Text( child: Text(
textAlign: TextAlign.start, textAlign: TextAlign.start,
loc.legal, loc.legal,
@@ -211,9 +176,7 @@ class _SettingsViewState extends State<SettingsView> {
GestureDetector( GestureDetector(
child: const Icon(Icons.language), child: const Icon(Icons.language),
onTap: () => { onTap: () => {
launchUrl( launchUrl(Uri.parse('https://liquid-dev.de')),
Uri.parse('https://liquid-dev.de'),
),
}, },
), ),
GestureDetector( GestureDetector(
@@ -262,8 +225,6 @@ class _SettingsViewState extends State<SettingsView> {
], ],
), ),
), ),
);
},
), ),
); );
} }
@@ -324,11 +285,10 @@ class _SettingsViewState extends State<SettingsView> {
Duration duration = const Duration(seconds: 3), Duration duration = const Duration(seconds: 3),
VoidCallback? action, VoidCallback? action,
}) { }) {
if (!context.mounted) return;
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
ScaffoldMessenger.of(context).hideCurrentSnackBar(); final messenger = ScaffoldMessenger.of(context);
ScaffoldMessenger.of(context).showSnackBar( messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar( SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)), content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.onBoxColor, backgroundColor: CustomTheme.onBoxColor,

View File

@@ -10,7 +10,6 @@ import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class StatisticsView extends StatefulWidget { class StatisticsView extends StatefulWidget {
/// A view that displays player statistics
const StatisticsView({super.key}); const StatisticsView({super.key});
@override @override
@@ -106,7 +105,7 @@ class _StatisticsViewState extends State<StatisticsView> {
Future.wait([ Future.wait([
db.matchDao.getAllMatches(), db.matchDao.getAllMatches(),
db.playerDao.getAllPlayers(), db.playerDao.getAllPlayers(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION), Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) async { ]).then((results) async {
if (!mounted) return; if (!mounted) return;
final matches = results[0] as List<Match>; final matches = results[0] as List<Match>;

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
class AppSkeleton extends StatefulWidget {
/// A widget that provides a skeleton loading effect to its child widget tree. /// A widget that provides a skeleton loading effect to its child widget tree.
/// - [child]: The widget tree to apply the skeleton effect to. /// - [child]: The widget tree to apply the skeleton effect to.
/// - [enabled]: A boolean to enable or disable the skeleton effect. /// - [enabled]: A boolean to enable or disable the skeleton effect.
/// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher. /// - [fixLayoutBuilder]: A boolean to fix the layout builder for AnimatedSwitcher.
class AppSkeleton extends StatefulWidget {
const AppSkeleton({ const AppSkeleton({
super.key, super.key,
required this.child, required this.child,

View File

@@ -1,50 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class AnimatedDialogButton extends StatefulWidget {
/// A custom animated button widget that provides a scaling and opacity effect
/// when pressed.
/// - [onPressed]: Callback function that is triggered when the button is pressed.
/// - [child]: The child widget to be displayed inside the button, typically a text or icon.
const AnimatedDialogButton({
super.key,
required this.onPressed,
required this.child,
});
/// Callback function that is triggered when the button is pressed.
final VoidCallback onPressed;
/// The child widget to be displayed inside the button, typically a text or icon.
final Widget child;
@override
State<AnimatedDialogButton> createState() => _AnimatedDialogButtonState();
}
class _AnimatedDialogButtonState extends State<AnimatedDialogButton> {
bool _isPressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _isPressed = true),
onTapUp: (_) => setState(() => _isPressed = false),
onTapCancel: () => setState(() => _isPressed = false),
onTap: widget.onPressed,
child: AnimatedScale(
scale: _isPressed ? 0.95 : 1.0,
duration: const Duration(milliseconds: 100),
child: AnimatedOpacity(
opacity: _isPressed ? 0.6 : 1.0,
duration: const Duration(milliseconds: 100),
child: Container(
decoration: CustomTheme.standardBoxDecoration,
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 6),
child: widget.child,
),
),
),
);
}
}

View File

@@ -2,13 +2,13 @@ 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/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
class CustomWidthButton extends StatelessWidget {
/// A custom button widget that is designed to have a width relative to the screen size. /// A custom button widget that is designed to have a width relative to the screen size.
/// It supports three types of buttons: primary, secondary, and text buttons. /// It supports three types of buttons: primary, secondary, and text buttons.
/// - [text]: The text to display on the button. /// - [text]: The text to display on the button.
/// - [buttonType]: The type of button to display. Defaults to [ButtonType.primary]. /// - [buttonType]: The type of button to display. Defaults to [ButtonType.primary].
/// - [sizeRelativeToWidth]: The size of the button relative to the width of the screen. /// - [sizeRelativeToWidth]: The size of the button relative to the width of the screen.
/// - [onPressed]: The callback to be invoked when the button is pressed. /// - [onPressed]: The callback to be invoked when the button is pressed.
class CustomWidthButton extends StatelessWidget {
const CustomWidthButton({ const CustomWidthButton({
super.key, super.key,
required this.text, required this.text,

View File

@@ -1,98 +0,0 @@
import 'package:flutter/material.dart';
class MainMenuButton extends StatefulWidget {
/// A button for the main menu with an optional icon and a press animation.
/// - [text]: The text of the button.
/// - [icon]: The icon of the button.
/// - [onPressed]: The callback to be invoked when the button is pressed.
const MainMenuButton({
super.key,
required this.text,
this.icon,
required this.onPressed,
});
/// The text of the button.
final String text;
/// The icon of the button.
final IconData? icon;
/// The callback to be invoked when the button is pressed.
final void Function() onPressed;
@override
State<MainMenuButton> createState() => _MainMenuButtonState();
}
class _MainMenuButtonState extends State<MainMenuButton>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 100),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 0.95).animate(
CurvedAnimation(parent: _animationController, curve: Curves.easeInOut),
);
}
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: _scaleAnimation,
child: GestureDetector(
onTapDown: (_) {
_animationController.forward();
},
onTapUp: (_) async {
await _animationController.reverse();
if (mounted) {
widget.onPressed();
}
},
onTapCancel: () {
_animationController.reverse();
},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(30),
),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (widget.icon != null) ...[
Icon(widget.icon, size: 26, color: Colors.black),
const SizedBox(width: 7),
],
Text(
widget.text,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
),
),
),
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
}

View File

@@ -1,10 +1,10 @@
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';
class QuickCreateButton extends StatefulWidget {
/// A button widget designed for quick creating matches in the [HomeView] /// A button widget designed for quick creating matches in the [HomeView]
/// - [text]: The text to display on the button. /// - [text]: The text to display on the button.
/// - [onPressed]: The callback to be invoked when the button is pressed. /// - [onPressed]: The callback to be invoked when the button is pressed.
class QuickCreateButton extends StatefulWidget {
const QuickCreateButton({ const QuickCreateButton({
super.key, super.key,
required this.text, required this.text,

View File

@@ -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),
),
),
],
);
}
}

View File

@@ -1,39 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class CustomAlertDialog extends StatelessWidget {
/// A custom alert dialog widget that provides a os unspecific AlertDialog,
/// with consistent colors, borders, and layout that match the app's custom theme.
/// - [title]: The title text displayed at the top of the dialog.
/// - [content]: The main content text displayed in the body of the dialog.
/// - [actions]: A list of action widgets (typically buttons) displayed at the bottom
/// of the dialog. These actions are horizontally spaced around the dialog's width.
const CustomAlertDialog({
super.key,
required this.title,
required this.content,
required this.actions,
});
final String title;
final String content;
final List<Widget> actions;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(title, style: const TextStyle(color: CustomTheme.textColor)),
content: Text(
content,
style: const TextStyle(color: CustomTheme.textColor),
),
actions: actions,
backgroundColor: CustomTheme.boxColor,
actionsAlignment: MainAxisAlignment.spaceAround,
shape: RoundedRectangleBorder(
borderRadius: CustomTheme.standardBorderRadiusAll,
side: BorderSide(color: CustomTheme.boxBorder),
),
);
}
}

View File

@@ -1,13 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
class NavbarItem extends StatefulWidget {
/// A navigation bar item widget that represents a single tab in a navigation bar. /// A navigation bar item widget that represents a single tab in a navigation bar.
/// - [index]: The index of the tab. /// - [index]: The index of the tab.
/// - [isSelected]: A boolean indicating whether the tab is currently selected. /// - [isSelected]: A boolean indicating whether the tab is currently selected.
/// - [icon]: The icon to display for the tab. /// - [icon]: The icon to display for the tab.
/// - [label]: The label to display for the tab. /// - [label]: The label to display for the tab.
/// - [onTabTapped]: The callback to be invoked when the tab is tapped. /// - [onTabTapped]: The callback to be invoked when the tab is tapped.
class NavbarItem extends StatefulWidget {
const NavbarItem({ const NavbarItem({
super.key, super.key,
required this.index, required this.index,
@@ -36,45 +35,7 @@ class NavbarItem extends StatefulWidget {
State<NavbarItem> createState() => _NavbarItemState(); State<NavbarItem> createState() => _NavbarItemState();
} }
class _NavbarItemState extends State<NavbarItem> class _NavbarItemState extends State<NavbarItem> {
with SingleTickerProviderStateMixin {
/// Animation controller for the scale animation
late AnimationController _animationController;
/// Scale animation for the icon when selected
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOutBack,
),
);
if (widget.isSelected) {
_animationController.forward();
}
}
// Retrigger animation on selection change
@override
void didUpdateWidget(NavbarItem oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isSelected && !oldWidget.isSelected) {
_animationController.forward();
} else if (!widget.isSelected && oldWidget.isSelected) {
_animationController.reverse();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(
@@ -87,29 +48,19 @@ class _NavbarItemState extends State<NavbarItem>
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
ScaleTransition( Icon(
scale: widget.isSelected
? _scaleAnimation
: const AlwaysStoppedAnimation(1.0),
child: Icon(
widget.icon, widget.icon,
color: widget.isSelected color: widget.isSelected ? Colors.white : Colors.black,
? CustomTheme.navBarItemSelectedColor
: CustomTheme.navBarItemUnselectedColor,
size: 32,
),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
widget.label, widget.label,
style: TextStyle( style: TextStyle(
color: widget.isSelected color: widget.isSelected ? Colors.white : Colors.black,
? CustomTheme.navBarItemSelectedColor fontSize: 12,
: CustomTheme.navBarItemUnselectedColor,
fontSize: widget.isSelected ? 12 : 11,
fontWeight: widget.isSelected fontWeight: widget.isSelected
? FontWeight.bold ? FontWeight.bold
: FontWeight.w500, : FontWeight.normal,
), ),
), ),
], ],
@@ -118,10 +69,4 @@ class _NavbarItemState extends State<NavbarItem>
), ),
); );
} }
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
} }

View File

@@ -11,14 +11,14 @@ import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class PlayerSelection extends StatefulWidget {
/// A widget that allows users to select players from a list, /// A widget that allows users to select players from a list,
/// with search functionality and the ability to add new players. /// with search functionality and the ability to add new players.
/// - [availablePlayers]: An optional list of players to choose from. If null, /// - [availablePlayers]: An optional list of players to choose from. If null, all
/// all players from the database are used. /// players from the database are used.
/// - [initialSelectedPlayers]: An optional list of players that should be pre-selected. /// - [initialSelectedPlayers]: An optional list of players that should be pre-selected.
/// - [onChanged]: A callback function that is invoked whenever the selection /// - [onChanged]: A callback function that is invoked whenever the selection changes,
/// changes, providing the updated list of selected players. /// providing the updated list of selected players.
class PlayerSelection extends StatefulWidget {
const PlayerSelection({ const PlayerSelection({
super.key, super.key,
this.availablePlayers, this.availablePlayers,
@@ -70,7 +70,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
suggestedPlayers = skeletonData; suggestedPlayers = skeletonData;
selectedPlayers = widget.initialSelectedPlayers ?? [];
loadPlayerList(); loadPlayerList();
} }
@@ -100,7 +99,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (value.isEmpty) { if (value.isEmpty) {
// If the search is empty, it shows all unselected players. // If the search is empty, it shows all unselected players.
suggestedPlayers = allPlayers.where((player) { suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.any((p) => p.id == player.id); return !selectedPlayers.contains(player);
}).toList(); }).toList();
} else { } else {
// If there is input, it filters by name match (case-insensitive) and ensures // If there is input, it filters by name match (case-insensitive) and ensures
@@ -109,7 +108,9 @@ class _PlayerSelectionState extends State<PlayerSelection> {
final bool nameMatches = player.name.toLowerCase().contains( final bool nameMatches = player.name.toLowerCase().contains(
value.toLowerCase(), value.toLowerCase(),
); );
final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id); final bool isNotSelected = !selectedPlayers.contains(
player,
);
return nameMatches && isNotSelected; return nameMatches && isNotSelected;
}).toList(); }).toList();
} }
@@ -124,8 +125,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
const SizedBox(height: 10), const SizedBox(height: 10),
SizedBox( SizedBox(
height: 50, height: 50,
child: AppSkeleton(
enabled: isLoading,
child: selectedPlayers.isEmpty child: selectedPlayers.isEmpty
? Center(child: Text(loc.no_players_selected)) ? Center(child: Text(loc.no_players_selected))
: SingleChildScrollView( : SingleChildScrollView(
@@ -167,7 +166,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
), ),
), ),
), ),
),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
loc.all_players, loc.all_players,
@@ -183,7 +181,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
icon: Icons.info, icon: Icons.info,
title: loc.info, title: loc.info,
message: _getInfoText(context), message: _getInfoText(context),
fullscreen: false,
), ),
child: ListView.builder( child: ListView.builder(
itemCount: suggestedPlayers.length, itemCount: suggestedPlayers.length,
@@ -221,7 +218,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
void loadPlayerList() { void loadPlayerList() {
_allPlayersFuture = Future.wait([ _allPlayersFuture = Future.wait([
db.playerDao.getAllPlayers(), db.playerDao.getAllPlayers(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION), Future.delayed(Constants.minimumSkeletonDuration),
]).then((results) => results[0] as List<Player>); ]).then((results) => results[0] as List<Player>);
if (mounted) { if (mounted) {
_allPlayersFuture.then((loadedPlayers) { _allPlayersFuture.then((loadedPlayers) {
@@ -246,22 +243,8 @@ class _PlayerSelectionState extends State<PlayerSelection> {
// Otherwise, use the loaded players from the database. // Otherwise, use the loaded players from the database.
loadedPlayers.sort((a, b) => a.name.compareTo(b.name)); loadedPlayers.sort((a, b) => a.name.compareTo(b.name));
allPlayers = [...loadedPlayers]; allPlayers = [...loadedPlayers];
if (widget.initialSelectedPlayers != null) {
// 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]; suggestedPlayers = [...loadedPlayers];
} }
}
isLoading = false; isLoading = false;
}); });
}); });
@@ -311,7 +294,6 @@ class _PlayerSelectionState extends State<PlayerSelection> {
/// [message] - The message to display in the snackbar. /// [message] - The message to display in the snackbar.
void showSnackBarMessage(String message) { void showSnackBarMessage(String message) {
if (!context.mounted) return; if (!context.mounted) return;
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
backgroundColor: CustomTheme.boxColor, backgroundColor: CustomTheme.boxColor,

View File

@@ -1,8 +1,6 @@
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 {
/// A custom search bar widget that encapsulates a [SearchBar] with additional customization options. /// A custom search bar widget that encapsulates a [SearchBar] with additional customization options.
/// - [controller]: The controller for the search bar's text input. /// - [controller]: The controller for the search bar's text input.
/// - [hintText]: The hint text displayed in the search bar when it is empty. /// - [hintText]: The hint text displayed in the search bar when it is empty.
@@ -12,6 +10,7 @@ class CustomSearchBar extends StatelessWidget {
/// - [onTrailingButtonPressed]: The callback invoked when the trailing button is pressed. /// - [onTrailingButtonPressed]: The callback invoked when the trailing button is pressed.
/// - [onChanged]: The callback invoked when the text in the search bar changes. /// - [onChanged]: The callback invoked when the text in the search bar changes.
/// - [constraints]: The constraints for the search bar. /// - [constraints]: The constraints for the search bar.
class CustomSearchBar extends StatelessWidget {
const CustomSearchBar({ const CustomSearchBar({
super.key, super.key,
required this.controller, required this.controller,
@@ -50,15 +49,6 @@ class CustomSearchBar extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
/// Enforce maximum length on the input text
const maxLength = Constants.MAX_PLAYER_NAME_LENGTH;
if (controller.text.length > maxLength) {
controller.text = controller.text.substring(0, maxLength);
controller.selection = TextSelection.fromPosition(
TextPosition(offset: controller.text.length),
);
}
return SearchBar( return SearchBar(
controller: controller, controller: controller,
constraints: constraints:

View File

@@ -1,38 +1,32 @@
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';
class TextInputField extends StatelessWidget {
/// A custom text input field widget that encapsulates a [TextField] with specific styling. /// A custom text input field widget that encapsulates a [TextField] with specific styling.
/// - [controller]: The controller for the text input field. /// - [controller]: The controller for the text input field.
/// - [onChanged]: Optional callback invoked when the text in the field changes. /// - [onChanged]: The 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. class TextInputField extends StatelessWidget {
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,
}); });
/// The controller for the text input field. /// The controller for the text input field.
final TextEditingController controller; final TextEditingController controller;
/// Optional callback invoked when the text in the field changes. /// The callback invoked when the text in the field changes.
final ValueChanged<String>? onChanged; final ValueChanged<String>? onChanged;
/// The hint text displayed in the text input field when it is empty. /// The hint text displayed in the text input field when it is empty.
final String hintText; final String hintText;
/// Optional parameter for maximum length of the input text.
final int? maxLength;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return TextField( return TextField(
controller: controller, controller: controller,
onChanged: onChanged, onChanged: onChanged,
maxLength: maxLength,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: CustomTheme.boxColor, fillColor: CustomTheme.boxColor,

View File

@@ -1,11 +1,11 @@
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';
class ChooseTile extends StatefulWidget {
/// A tile widget that allows users to choose an option by tapping on it. /// A tile widget that allows users to choose an option by tapping on it.
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
/// - [trailingText]: Optional trailing text displayed on the tile. /// - [trailingText]: Optional trailing text displayed on the tile.
/// - [onPressed]: The callback invoked when the tile is tapped. /// - [onPressed]: The callback invoked when the tile is tapped.
class ChooseTile extends StatefulWidget {
const ChooseTile({ const ChooseTile({
super.key, super.key,
required this.title, required this.title,

View File

@@ -1,11 +1,11 @@
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';
class CustomRadioListTile<T> extends StatelessWidget {
/// A custom radio list tile widget that encapsulates a [Radio] button with additional styling and functionality. /// A custom radio list tile widget that encapsulates a [Radio] button with additional styling and functionality.
/// - [text]: The text to display next to the radio button. /// - [text]: The text to display next to the radio button.
/// - [value]: The value associated with the radio button. /// - [value]: The value associated with the radio button.
/// - [onContainerTap]: The callback invoked when the container is tapped. /// - [onContainerTap]: The callback invoked when the container is tapped.
class CustomRadioListTile<T> extends StatelessWidget {
const CustomRadioListTile({ const CustomRadioListTile({
super.key, super.key,
required this.text, required this.text,

View File

@@ -3,17 +3,11 @@ 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 {
/// 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. class GroupTile extends StatelessWidget {
const GroupTile({ const GroupTile({super.key, required this.group, this.isHighlighted = false});
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 +15,12 @@ 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.
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 AnimatedContainer(
onTap: widget.onTap,
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 +32,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 +43,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 +63,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),
], ],
@@ -87,7 +71,6 @@ class _GroupTileState extends State<GroupTile> {
const SizedBox(height: 2.5), const SizedBox(height: 2.5),
], ],
), ),
),
); );
} }
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
class InfoTile extends StatefulWidget {
/// A tile widget that displays a title with an icon and some content below it. /// A tile widget that displays a title with an icon and some content below it.
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
/// - [icon]: The icon displayed next to the title. /// - [icon]: The icon displayed next to the title.
@@ -9,6 +8,7 @@ class InfoTile extends StatefulWidget {
/// - [padding]: Optional padding for the tile content. /// - [padding]: Optional padding for the tile content.
/// - [height]: Optional height for the tile. /// - [height]: Optional height for the tile.
/// - [width]: Optional width for the tile. /// - [width]: Optional width for the tile.
class InfoTile extends StatefulWidget {
const InfoTile({ const InfoTile({
super.key, super.key,
required this.title, required this.title,
@@ -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: [

View File

@@ -2,16 +2,12 @@ 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.
/// - [package]: The package data to be displayed.
const LicenseTile({super.key, required this.package});
/// The package data to be displayed.
final Package package; final Package package;
const LicenseTile({super.key, required this.package});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@@ -30,10 +26,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(

View File

@@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:skeletonizer/skeletonizer.dart';
class MatchSummaryTile extends StatefulWidget {
final String matchTitle;
final String game;
final String ruleset;
final String players;
final String winner;
const MatchSummaryTile({
super.key,
required this.matchTitle,
required this.game,
required this.ruleset,
required this.players,
required this.winner,
});
@override
State<MatchSummaryTile> createState() => _MatchSummaryTileState();
}
class _MatchSummaryTileState extends State<MatchSummaryTile> {
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
widget.matchTitle,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(width: 5),
Text(
widget.game,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
const SizedBox(height: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
height: 20,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: CustomTheme.primaryColor,
),
child: Skeleton.ignore(
child: Text(
widget.ruleset,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
),
Center(
heightFactor: 1.5,
child: Text(
widget.players,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 4),
width: 220,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
color: Colors.yellow.shade300,
),
child: Skeleton.ignore(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.emoji_events, color: Colors.black, size: 20),
Text(
widget.winner,
textAlign: TextAlign.center,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
),
),
),
],
);
}
}

View File

@@ -6,13 +6,13 @@ import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
class MatchTile extends StatefulWidget {
/// A tile widget that displays information about a match, including its name, /// A tile widget that displays information about a match, including its name,
/// creation date, associated group, winner, and players. /// creation date, associated group, winner, and players.
/// - [match]: The match data to be displayed. /// - [match]: The match data to be displayed.
/// - [onTap]: The callback invoked when the tile is tapped. /// - [onTap]: The callback invoked when the tile is tapped.
/// - [width]: Optional width for the tile. /// - [width]: Optional width for the tile.
/// - [compact]: Whether to display the tile in a compact mode /// - [compact]: Whether to display the tile in a compact mode
class MatchTile extends StatefulWidget {
const MatchTile({ const MatchTile({
super.key, super.key,
required this.match, required this.match,
@@ -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);
} }
} }

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
class QuickInfoTile extends StatefulWidget {
/// A tile widget that displays a title with an icon and a numeric value below it. /// A tile widget that displays a title with an icon and a numeric value below it.
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
/// - [icon]: The icon displayed next to the title. /// - [icon]: The icon displayed next to the title.
@@ -9,6 +8,7 @@ class QuickInfoTile extends StatefulWidget {
/// - [height]: Optional height for the tile. /// - [height]: Optional height for the tile.
/// - [width]: Optional width for the tile. /// - [width]: Optional width for the tile.
/// - [padding]: Optional padding for the tile content. /// - [padding]: Optional padding for the tile content.
class QuickInfoTile extends StatefulWidget {
const QuickInfoTile({ const QuickInfoTile({
super.key, super.key,
required this.title, required this.title,

View File

@@ -1,13 +1,12 @@
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 {
/// 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.
/// - [icon]: The icon displayed on the left side of the tile. /// - [icon]: The icon displayed on the left side of the tile.
/// - [title]: The title text displayed next to the icon. /// - [title]: The title text displayed next to the icon.
/// - [suffixWidget]: An optional widget displayed on the right side of the tile. /// - [suffixWidget]: An optional widget displayed on the right side of the tile.
/// - [onPressed]: The callback invoked when the tile is tapped. /// - [onPressed]: The callback invoked when the tile is tapped.
class SettingsListTile extends StatelessWidget {
const SettingsListTile({ const SettingsListTile({
super.key, super.key,
required this.icon, required this.icon,
@@ -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)),

View File

@@ -4,7 +4,6 @@ import 'package:flutter/material.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/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
class StatisticsTile extends StatelessWidget {
/// A tile widget that displays statistical data using horizontal bars. /// A tile widget that displays statistical data using horizontal bars.
/// - [icon]: The icon displayed next to the title. /// - [icon]: The icon displayed next to the title.
/// - [title]: The title text displayed on the tile. /// - [title]: The title text displayed on the tile.
@@ -12,6 +11,7 @@ class StatisticsTile extends StatelessWidget {
/// - [values]: A list of tuples containing labels and their corresponding numeric values. /// - [values]: A list of tuples containing labels and their corresponding numeric values.
/// - [itemCount]: The maximum number of items to display. /// - [itemCount]: The maximum number of items to display.
/// - [barColor]: The color of the bars representing the values. /// - [barColor]: The color of the bars representing the values.
class StatisticsTile extends StatelessWidget {
const StatisticsTile({ const StatisticsTile({
super.key, super.key,
required this.icon, required this.icon,

View File

@@ -1,11 +1,11 @@
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';
class TextIconListTile extends StatelessWidget {
/// A list tile widget that displays text with an optional icon button. /// A list tile widget that displays text with an optional icon button.
/// - [text]: The text to display in the tile. /// - [text]: The text to display in the tile.
/// - [onPressed]: The callback to be invoked when the icon is pressed. /// - [onPressed]: The callback to be invoked when the icon is pressed.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed. /// - [iconEnabled]: A boolean to determine if the icon should be displayed.
class TextIconListTile extends StatelessWidget {
const TextIconListTile({ const TextIconListTile({
super.key, super.key,
required this.text, required this.text,

View File

@@ -1,11 +1,11 @@
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';
class TextIconTile extends StatelessWidget {
/// A tile widget that displays text with an optional icon that can be tapped. /// A tile widget that displays text with an optional icon that can be tapped.
/// - [text]: The text to display in the tile. /// - [text]: The text to display in the tile.
/// - [iconEnabled]: A boolean to determine if the icon should be displayed. /// - [iconEnabled]: A boolean to determine if the icon should be displayed.
/// - [onIconTap]: The callback to be invoked when the icon is tapped. /// - [onIconTap]: The callback to be invoked when the icon is tapped.
class TextIconTile extends StatelessWidget {
const TextIconTile({ const TextIconTile({
super.key, super.key,
required this.text, required this.text,

View File

@@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
class TitleDescriptionListTile extends StatelessWidget {
/// A list tile widget that displays a title and description, with optional highlighting and badge. /// A list tile widget that displays a title and description, with optional highlighting and badge.
/// - [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.
@@ -9,6 +8,7 @@ class TitleDescriptionListTile extends StatelessWidget {
/// - [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.
class TitleDescriptionListTile extends StatelessWidget {
const TitleDescriptionListTile({ const TitleDescriptionListTile({
super.key, super.key,
required this.title, required this.title,
@@ -73,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,

View File

@@ -1,16 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class TopCenteredMessage extends StatelessWidget {
/// A widget that displays a message centered at the top of the screen with an icon, title, and message. /// A widget that displays a message centered at the top of the screen with an icon, title, and message.
/// - [icon]: The icon to display above the title. /// - [icon]: The icon to display above the title.
/// - [title]: The title text to display. /// - [title]: The title text to display.
/// - [message]: The message text to display below the title. /// - [message]: The message text to display below the title.
class TopCenteredMessage extends StatelessWidget {
const TopCenteredMessage({ const TopCenteredMessage({
super.key, super.key,
required this.icon, required this.icon,
required this.title, required this.title,
required this.message, required this.message,
this.fullscreen = true,
}); });
/// The icon to display above the title. /// The icon to display above the title.
@@ -22,18 +21,13 @@ class TopCenteredMessage extends StatelessWidget {
/// The message text to display below the title. /// The message text to display below the title.
final String message; final String message;
final bool fullscreen;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
padding: fullscreen ? const EdgeInsets.only(top: 100) : null, padding: const EdgeInsets.only(top: 100),
margin: const EdgeInsets.symmetric(horizontal: 10), margin: const EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
child: Column( child: Column(
mainAxisAlignment: fullscreen
? MainAxisAlignment.start
: MainAxisAlignment.center,
children: [ children: [
Icon(icon, size: 45), Icon(icon, size: 45),
const SizedBox(height: 10), const SizedBox(height: 10),

View File

@@ -1,7 +1,7 @@
name: game_tracker name: game_tracker
description: "Game Tracking App for Card Games" description: "Game Tracking App for Card Games"
publish_to: 'none' publish_to: 'none'
version: 0.0.9+242 version: 0.0.5+143
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -17,7 +17,6 @@ dependencies:
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
file_picker: ^10.3.6 file_picker: ^10.3.6
file_saver: ^0.3.1 file_saver: ^0.3.1
fluttericon: ^2.0.0
font_awesome_flutter: ^10.12.0 font_awesome_flutter: ^10.12.0
intl: any intl: any
json_schema: ^5.2.2 json_schema: ^5.2.2
@@ -28,6 +27,7 @@ dependencies:
url_launcher: ^6.3.2 url_launcher: ^6.3.2
uuid: ^4.5.2 uuid: ^4.5.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter