6 Commits

Author SHA1 Message Date
caf60d046b fix merge mistake
All checks were successful
Pull Request Pipeline / lint (pull_request) Successful in 3m9s
Pull Request Pipeline / test (pull_request) Successful in 2m30s
2026-01-10 22:20:14 +01:00
38663c6b67 Merge remote-tracking branch 'origin/development' into feature/118-bearbeiten-und-löschen-von-gruppen
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m33s
Pull Request Pipeline / lint (pull_request) Successful in 2m44s
# Conflicts:
#	lib/l10n/arb/app_de.arb
#	lib/l10n/arb/app_en.arb
#	lib/l10n/generated/app_localizations_de.dart
#	lib/presentation/views/main_menu/settings_view.dart
2026-01-10 22:19:19 +01:00
ee84c60ba6 remove questionmark
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m2s
Pull Request Pipeline / lint (pull_request) Successful in 2m5s
2026-01-10 22:13:26 +01:00
b6dd0541ae rename CreateGroupView to GroupDetailView for clarity and consistency
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m3s
Pull Request Pipeline / lint (pull_request) Successful in 2m4s
2026-01-10 22:11:28 +01:00
525acec1d3 Merge branch 'development' into feature/118-bearbeiten-und-löschen-von-gruppen
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Failing after 2m3s
2026-01-10 20:48:25 +00:00
45a419cae7 implement group edit view
Some checks failed
Pull Request Pipeline / test (pull_request) Successful in 2m1s
Pull Request Pipeline / lint (pull_request) Failing after 2m3s
2026-01-10 20:14:37 +01:00
23 changed files with 446 additions and 7972 deletions

27
.gitignore vendored
View File

@@ -150,9 +150,25 @@ app.*.symbols
.gclient_previous_custom_vars .gclient_previous_custom_vars
.gclient_previous_sync_commits .gclient_previous_sync_commits
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/ .swiftpm/
migrate_working_dir/ migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in # The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line # VS Code which you may wish to be included in version control, so this line
@@ -160,8 +176,17 @@ migrate_working_dir/
#.vscode/ #.vscode/
# Flutter/Dart/Pub related # Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/ /build/
# Symbolication related
app.*.symbols
# Obfuscation related # Obfuscation related
app.*.map.json app.*.map.json
@@ -171,5 +196,3 @@ app.*.map.json
/android/app/profile /android/app/profile
/android/app/release /android/app/release
/devtools_options.yaml /devtools_options.yaml
untranslated_messages.json

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins { plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false id("com.android.application") version "8.7.3" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false id("org.jetbrains.kotlin.android") version "2.1.0" apply false
} }

View File

@@ -24,11 +24,6 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
<string>http</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>

View File

@@ -3,4 +3,3 @@ template-arb-file: app_en.arb
output-localization-file: app_localizations.dart output-localization-file: app_localizations.dart
output-dir: lib/l10n/generated output-dir: lib/l10n/generated
nullable-getter: false nullable-getter: false
untranslated-messages-file: lib/l10n/untranslated_messages.json

View File

@@ -3,7 +3,6 @@
"all_players": "Alle Spieler:innen", "all_players": "Alle Spieler:innen",
"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",
"cancel": "Abbrechen", "cancel": "Abbrechen",
"choose_game": "Spielvorlage wählen", "choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen", "choose_group": "Gruppe wählen",
@@ -19,7 +18,11 @@
"days_ago": "vor {count} Tagen", "days_ago": "vor {count} Tagen",
"delete": "Löschen", "delete": "Löschen",
"delete_all_data": "Alle Daten löschen", "delete_all_data": "Alle Daten löschen",
"delete_group": "Diese 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",
@@ -34,10 +37,6 @@
"import_data": "Daten importieren", "import_data": "Daten importieren",
"info": "Info", "info": "Info",
"invalid_schema": "Ungültiges Schema", "invalid_schema": "Ungültiges Schema",
"licenses": "Lizenzen",
"no_licenses_found": "Keine Lizenzen gefunden",
"no_license_text_available": "Kein Lizenztext verfügbar",
"error": "Fehler",
"least_points": "Niedrigste Punkte", "least_points": "Niedrigste Punkte",
"match_in_progress": "Spiel läuft...", "match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel", "match_name": "Spieltitel",

View File

@@ -34,10 +34,10 @@
"description": "Button text to create a match" "description": "Button text to create a match"
}, },
"@create_new_group": { "@create_new_group": {
"description": "Button text to create a new group" "description": "Appbar text to create a new group"
}, },
"@create_new_match": { "@create_new_match": {
"description": "Button text to create a new match" "description": "Appbar text to create a new match"
}, },
"@data_successfully_deleted": { "@data_successfully_deleted": {
"description": "Success message after deleting data" "description": "Success message after deleting data"
@@ -62,9 +62,21 @@
"@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"
}, },
@@ -107,18 +119,6 @@
"@invalid_schema": { "@invalid_schema": {
"description": "Error message for invalid schema" "description": "Error message for invalid schema"
}, },
"@licenses": {
"description": "Licenses menu item"
},
"@no_licenses_found": {
"description": "Message when no licenses are found"
},
"@no_license_text_available": {
"description": "Message when no license text is available"
},
"@error": {
"description": "Error label"
},
"@least_points": { "@least_points": {
"description": "Title for least points ruleset" "description": "Title for least points ruleset"
}, },
@@ -290,7 +290,11 @@
"days_ago": "{count} days ago", "days_ago": "{count} days ago",
"delete": "Delete", "delete": "Delete",
"delete_all_data": "Delete all data", "delete_all_data": "Delete all data",
"delete_group": "Delete this 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",
@@ -305,10 +309,6 @@
"import_data": "Import data", "import_data": "Import data",
"info": "Info", "info": "Info",
"invalid_schema": "Invalid Schema", "invalid_schema": "Invalid Schema",
"licenses": "Licenses",
"no_licenses_found": "No licenses found",
"no_license_text_available": "No license text available",
"error": "Error",
"least_points": "Least Points", "least_points": "Least Points",
"match_in_progress": "Match in progress...", "match_in_progress": "Match in progress...",
"match_name": "Match name", "match_name": "Match name",

View File

@@ -212,12 +212,36 @@ 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 this 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:
@@ -302,30 +326,6 @@ abstract class AppLocalizations {
/// **'Invalid Schema'** /// **'Invalid Schema'**
String get invalid_schema; String get invalid_schema;
/// Licenses menu item
///
/// In en, this message translates to:
/// **'Licenses'**
String get licenses;
/// Message when no licenses are found
///
/// In en, this message translates to:
/// **'No licenses found'**
String get no_licenses_found;
/// Message when no license text is available
///
/// In en, this message translates to:
/// **'No license text available'**
String get no_license_text_available;
/// Error label
///
/// In en, this message translates to:
/// **'Error'**
String get error;
/// Title for least points ruleset /// Title for least points ruleset
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -69,10 +69,24 @@ 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 => 'Diese 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';
@@ -115,18 +129,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get invalid_schema => 'Ungültiges Schema'; String get invalid_schema => 'Ungültiges Schema';
@override
String get licenses => 'Lizenzen';
@override
String get no_licenses_found => 'Keine Lizenzen gefunden';
@override
String get no_license_text_available => 'Kein Lizenztext verfügbar';
@override
String get error => 'Fehler';
@override @override
String get least_points => 'Niedrigste Punkte'; String get least_points => 'Niedrigste Punkte';

View File

@@ -69,10 +69,24 @@ 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 this 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';
@@ -115,18 +129,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get invalid_schema => 'Invalid Schema'; String get invalid_schema => 'Invalid Schema';
@override
String get licenses => 'Licenses';
@override
String get no_licenses_found => 'No licenses found';
@override
String get no_license_text_available => 'No license text available';
@override
String get error => 'Error';
@override @override
String get least_points => 'Least Points'; String get least_points => 'Least Points';

View File

@@ -5,7 +5,7 @@ import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/groups_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.dart';
import 'package:game_tracker/presentation/views/main_menu/statistics_view.dart'; 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';

View File

@@ -1,113 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart';
import 'package:game_tracker/presentation/widgets/player_selection.dart';
import 'package:game_tracker/presentation/widgets/text_input/text_input_field.dart';
import 'package:provider/provider.dart';
class CreateGroupView extends StatefulWidget {
const CreateGroupView({super.key});
@override
State<CreateGroupView> createState() => _CreateGroupViewState();
}
class _CreateGroupViewState extends State<CreateGroupView> {
late final AppDatabase db;
/// Controller for the group name input field
final _groupNameController = TextEditingController();
/// List of currently selected players
List<Player> selectedPlayers = [];
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
_groupNameController.addListener(() {
setState(() {});
});
}
@override
void dispose() {
_groupNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_group)),
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
margin: CustomTheme.standardMargin,
child: TextInputField(
controller: _groupNameController,
hintText: loc.group_name,
),
),
Expanded(
child: PlayerSelection(
onChanged: (value) {
setState(() {
selectedPlayers = [...value];
});
},
),
),
CustomWidthButton(
text: loc.create_group,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed:
(_groupNameController.text.isEmpty ||
(selectedPlayers.length < 2))
? null
: () async {
bool success = await db.groupDao.addGroup(
group: Group(
name: _groupNameController.text.trim(),
members: selectedPlayers,
),
);
if (!context.mounted) return;
if (success) {
Navigator.pop(context);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: CustomTheme.boxColor,
content: Center(
child: Text(
AppLocalizations.of(
context,
).error_creating_group,
style: const TextStyle(color: Colors.white),
),
),
),
);
}
},
),
const SizedBox(height: 20),
],
),
),
),
);
}
}

View File

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

View File

@@ -6,7 +6,7 @@ import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/dto/group.dart'; import 'package:game_tracker/data/dto/group.dart';
import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/data/dto/player.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/group_detail_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.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/tiles/group_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart';
@@ -73,7 +73,19 @@ class _GroupsViewState extends State<GroupsView> {
height: MediaQuery.paddingOf(context).bottom - 20, height: MediaQuery.paddingOf(context).bottom - 20,
); );
} }
return GroupTile(group: groups[index]); return GroupTile(group: groups[index], onTap: () async {
await Navigator.push(
context,
adaptivePageRoute(
builder: (context) {
return GroupDetailView(groupToEdit: groups[index]);
},
),
);
setState(() {
loadGroups();
});
});
}, },
), ),
), ),
@@ -88,7 +100,7 @@ class _GroupsViewState extends State<GroupsView> {
context, context,
adaptivePageRoute( adaptivePageRoute(
builder: (context) { builder: (context) {
return const CreateGroupView(); return const GroupDetailView();
}, },
), ),
); );

View File

@@ -70,6 +70,9 @@ class _CreateMatchViewState extends State<CreateMatchView> {
/// List of available rulesets with their localized string representations /// List of available rulesets with their localized string representations
late final List<(Ruleset, String)> _rulesets; late final List<(Ruleset, String)> _rulesets;
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@@ -120,6 +123,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(
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
appBar: AppBar(title: Text(loc.create_new_match)), appBar: AppBar(title: Text(loc.create_new_match)),

View File

@@ -1,17 +1,10 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/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/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:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsView extends StatefulWidget { class SettingsView extends StatefulWidget {
const SettingsView({super.key}); const SettingsView({super.key});
@@ -21,6 +14,10 @@ class SettingsView extends StatefulWidget {
} }
class _SettingsViewState extends State<SettingsView> { class _SettingsViewState extends State<SettingsView> {
/// GlobalKey for ScaffoldMessenger to show snackbars
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
PackageInfo _packageInfo = PackageInfo( PackageInfo _packageInfo = PackageInfo(
appName: 'n.A.', appName: 'n.A.',
packageName: 'n.A.', packageName: 'n.A.',
@@ -37,6 +34,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(
key: _scaffoldMessengerKey,
child: Scaffold( child: Scaffold(
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
backgroundColor: CustomTheme.backgroundColor, backgroundColor: CustomTheme.backgroundColor,
@@ -45,7 +43,7 @@ class _SettingsViewState extends State<SettingsView> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(left: 16, bottom: 10), padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
child: Text( child: Text(
textAlign: TextAlign.start, textAlign: TextAlign.start,
loc.menu, loc.menu,
@@ -56,7 +54,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
), ),
Padding( Padding(
padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10), padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: Text( child: Text(
textAlign: TextAlign.start, textAlign: TextAlign.start,
loc.settings, loc.settings,
@@ -68,7 +66,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
SettingsListTile( SettingsListTile(
title: loc.export_data, title: loc.export_data,
icon: Icons.upload, icon: Icons.upload_rounded,
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 = await DataTransferService.getAppDataAsJson( final String json = await DataTransferService.getAppDataAsJson(
@@ -84,7 +82,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
SettingsListTile( SettingsListTile(
title: loc.import_data, title: loc.import_data,
icon: Icons.download, icon: Icons.download_rounded,
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(context); final result = await DataTransferService.importData(context);
@@ -94,7 +92,7 @@ class _SettingsViewState extends State<SettingsView> {
), ),
SettingsListTile( SettingsListTile(
title: loc.delete_all_data, title: loc.delete_all_data,
icon: Icons.delete, icon: Icons.delete_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16), suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () { onPressed: () {
showDialog<bool>( showDialog<bool>(
@@ -126,74 +124,11 @@ class _SettingsViewState extends State<SettingsView> {
}); });
}, },
), ),
const Padding(
padding: EdgeInsets.only(left: 16, top: 10, bottom: 10),
child: Text(
textAlign: TextAlign.start,
'App',
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
),
SettingsListTile(
title: loc.licenses,
icon: Icons.insert_drive_file,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const LicensesView()),
);
},
),
const Spacer(), const Spacer(),
Padding( Padding(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
child: Center( child: Center(
child: Column( child: Text(
spacing: 4,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
spacing: 40,
children: [
GestureDetector(
child: const Icon(Icons.language),
onTap: () => {
launchUrl(Uri.parse('https://liquid-dev.de')),
},
),
GestureDetector(
child: const FaIcon(FontAwesomeIcons.github),
onTap: () => {
launchUrl(
Uri.parse(
'https://github.com/liquiddevelopmentde',
),
),
},
),
GestureDetector(
child: Icon(
Platform.isIOS
? CupertinoIcons.mail_solid
: Icons.email,
),
onTap: () =>
launchUrl(Uri.parse('mailto:hi@liquid-dev.de')),
),
],
),
),
Text(
'© ${DateFormat('yyyy').format(DateTime.now())} Liquid Development',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
Text(
'Version ${_packageInfo.version} (${_packageInfo.buildNumber})', 'Version ${_packageInfo.version} (${_packageInfo.buildNumber})',
style: TextStyle( style: TextStyle(
color: Colors.grey.shade600, color: Colors.grey.shade600,
@@ -201,8 +136,6 @@ class _SettingsViewState extends State<SettingsView> {
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
],
),
), ),
), ),
], ],

View File

@@ -1,124 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/views/main_menu/settings_view/licenses/oss_licenses.dart';
class LicenseDetailView extends StatelessWidget {
final Package package;
const LicenseDetailView({super.key, required this.package});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Lizenzdetails')),
body: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
margin: const EdgeInsetsGeometry.only(right: 15),
width: 60,
height: 60,
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.description,
color: CustomTheme.primaryColor,
size: 30,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
package.name,
textAlign: TextAlign.left,
style: const TextStyle(
height: 0,
fontSize: 24,
fontWeight: FontWeight.bold,
),
),
if (package.version != null) ...[
Text(
'Version ${package.version}',
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade300,
),
),
],
],
),
],
),
if (package.authors.isNotEmpty) ...[
const SizedBox(height: 8),
SelectableText(
package.authors.join(', '),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
if (package.homepage != null &&
package.homepage!.isNotEmpty) ...[
const SizedBox(height: 8),
SelectableText(
package.homepage!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
],
],
),
),
const SizedBox(height: 20),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
decoration: CustomTheme.standardBoxDecoration,
child: (package.license != null && package.license!.isNotEmpty)
? SelectableText(
package.license!,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade300,
height: 1.5,
fontFamily: 'monospace',
),
)
: Center(
heightFactor: 25,
child: Text(
loc.no_license_text_available,
style: TextStyle(color: Colors.grey.shade500),
),
),
),
],
),
),
);
}
}

View File

@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:game_tracker/core/custom_theme.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/widgets/tiles/license_tile.dart';
class LicensesView extends StatelessWidget {
const LicensesView({super.key});
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(
title: Text(loc.licenses),
backgroundColor: CustomTheme.backgroundColor,
),
backgroundColor: CustomTheme.backgroundColor,
body: ListView.builder(
itemCount: allDependencies.length,
itemBuilder: (context, index) {
final package = allDependencies[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
child: LicenseTile(package: package),
);
},
),
);
}
}

View File

@@ -70,6 +70,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
super.initState(); super.initState();
db = Provider.of<AppDatabase>(context, listen: false); db = Provider.of<AppDatabase>(context, listen: false);
suggestedPlayers = skeletonData; suggestedPlayers = skeletonData;
selectedPlayers = widget.initialSelectedPlayers ?? [];
loadPlayerList(); loadPlayerList();
} }
@@ -99,7 +100,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
if (value.isEmpty) { if (value.isEmpty) {
// If the search is empty, it shows all unselected players. // If the search is empty, it shows all unselected players.
suggestedPlayers = allPlayers.where((player) { suggestedPlayers = allPlayers.where((player) {
return !selectedPlayers.contains(player); return !selectedPlayers.any((p) => p.id == player.id);
}).toList(); }).toList();
} else { } else {
// If there is input, it filters by name match (case-insensitive) and ensures // If there is input, it filters by name match (case-insensitive) and ensures
@@ -108,9 +109,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
final bool nameMatches = player.name.toLowerCase().contains( final bool nameMatches = player.name.toLowerCase().contains(
value.toLowerCase(), value.toLowerCase(),
); );
final bool isNotSelected = !selectedPlayers.contains( final bool isNotSelected = !selectedPlayers.any((p) => p.id == player.id);
player,
);
return nameMatches && isNotSelected; return nameMatches && isNotSelected;
}).toList(); }).toList();
} }
@@ -125,6 +124,8 @@ 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(
@@ -166,6 +167,7 @@ class _PlayerSelectionState extends State<PlayerSelection> {
), ),
), ),
), ),
),
const SizedBox(height: 10), const SizedBox(height: 10),
Text( Text(
loc.all_players, loc.all_players,
@@ -243,8 +245,22 @@ 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;
}); });
}); });

View File

@@ -6,8 +6,9 @@ import 'package:game_tracker/presentation/widgets/tiles/text_icon_tile.dart';
/// A tile widget that displays information about a group, including its name and members. /// A tile widget that displays information about a group, including its name and members.
/// - [group]: The group data to be displayed. /// - [group]: The group data to be displayed.
/// - [isHighlighted]: Whether the tile should be highlighted. /// - [isHighlighted]: Whether the tile should be highlighted.
/// - [onTap]: An optional callback function to handle tap events.
class GroupTile extends StatelessWidget { class GroupTile extends StatelessWidget {
const GroupTile({super.key, required this.group, this.isHighlighted = false}); const GroupTile({super.key, required this.group, this.isHighlighted = false, this.onTap});
/// The group data to be displayed. /// The group data to be displayed.
final Group group; final Group group;
@@ -15,9 +16,14 @@ class GroupTile extends StatelessWidget {
/// Whether the tile should be highlighted. /// Whether the tile should be highlighted.
final bool isHighlighted; final bool isHighlighted;
/// Callback function to handle tap events.
final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnimatedContainer( return GestureDetector(
onTap: 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: isHighlighted decoration: isHighlighted
@@ -71,6 +77,7 @@ class GroupTile extends StatelessWidget {
const SizedBox(height: 2.5), const SizedBox(height: 2.5),
], ],
), ),
),
); );
} }
} }

View File

@@ -1,106 +0,0 @@
import 'package:flutter/material.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/oss_licenses.dart';
class LicenseTile extends StatelessWidget {
final Package package;
const LicenseTile({super.key, required this.package});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => LicenseDetailView(package: package),
),
);
},
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(16),
decoration: CustomTheme.standardBoxDecoration.copyWith(
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.description,
color: CustomTheme.primaryColor,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
package.name,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
if (package.version != null &&
package.version!.isNotEmpty) ...[
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
decoration: BoxDecoration(
color: CustomTheme.onBoxColor,
borderRadius: BorderRadius.circular(6),
),
child: Text(
'v${package.version}',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade500,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
const SizedBox(height: 4),
Text(
package.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade400,
height: 1.3,
),
),
],
),
),
const SizedBox(width: 12),
// Arrow Icon
Icon(Icons.chevron_right, color: Colors.grey.shade600, size: 24),
],
),
),
);
}
}

View File

@@ -47,26 +47,13 @@ class SettingsListTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Container( Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 28,
color: CustomTheme.primaryColor.withGreen(40),
),
),
/* Container(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: CustomTheme.primaryColor, color: CustomTheme.primaryColor,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: Icon(icon, size: 24), child: Icon(icon, size: 24),
),*/ ),
const SizedBox(width: 16), const SizedBox(width: 16),
Text(title, style: const TextStyle(fontSize: 18)), Text(title, style: const TextStyle(fontSize: 18)),
], ],

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.5+127 version: 0.0.4+101
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -9,32 +9,27 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
flutter_localizations:
sdk: flutter
clock: ^1.1.2
cupertino_icons: ^1.0.6
drift: ^2.27.0 drift: ^2.27.0
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
file_picker: ^10.3.6
file_saver: ^0.3.1
font_awesome_flutter: ^10.12.0
intl: any
json_schema: ^5.2.2
package_info_plus: ^9.0.0
path_provider: ^2.1.5 path_provider: ^2.1.5
provider: ^6.1.5 provider: ^6.1.5
skeletonizer: ^2.1.0+1 skeletonizer: ^2.1.0+1
url_launcher: ^6.3.2
uuid: ^4.5.2 uuid: ^4.5.2
file_picker: ^10.3.6
json_schema: ^5.2.2
file_saver: ^0.3.1
clock: ^1.1.2
intl: any
flutter_localizations:
sdk: flutter
package_info_plus: ^9.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
build_runner: ^2.5.4
dart_pubspec_licenses: ^3.0.14
drift_dev: ^2.27.0
flutter_lints: ^5.0.0 flutter_lints: ^5.0.0
drift_dev: ^2.27.0
build_runner: ^2.5.4
flutter: flutter:
uses-material-design: true uses-material-design: true