11 Commits

Author SHA1 Message Date
gelbeinhalb
e2fe0c7d4d remove group_match_dao
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m18s
Pull Request Pipeline / test (pull_request) Failing after 2m42s
2026-01-12 11:42:03 +01:00
gelbeinhalb
b72ab70e02 add score table
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m22s
Pull Request Pipeline / test (pull_request) Failing after 2m52s
2026-01-12 11:38:38 +01:00
gelbeinhalb
189daf76dd move createdAt below description 2026-01-12 11:35:57 +01:00
gelbeinhalb
0f987f4c7a move match below ids 2026-01-12 11:34:02 +01:00
gelbeinhalb
5dd8f31942 add description to group_table.dart
Some checks failed
Pull Request Pipeline / lint (pull_request) Failing after 2m29s
Pull Request Pipeline / test (pull_request) Failing after 2m54s
2026-01-12 11:33:00 +01:00
gelbeinhalb
0394f5edf9 delete group_match_table.dart 2026-01-12 11:32:23 +01:00
gelbeinhalb
d8abad6fd8 add groupid gameid and notes to match 2026-01-12 11:30:37 +01:00
gelbeinhalb
7e6c309de0 add game table 2026-01-12 11:26:39 +01:00
gelbeinhalb
3344575132 add team id and score
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 2m0s
Pull Request Pipeline / lint (pull_request) Successful in 2m8s
2026-01-12 11:20:40 +01:00
gelbeinhalb
9b66e58dc0 add team table 2026-01-12 11:17:13 +01:00
gelbeinhalb
56562b22bb add description to player 2026-01-12 11:16:02 +01:00
39 changed files with 434 additions and 8374 deletions

View File

@@ -0,0 +1,35 @@
---
name: Bug report
about: Erstelle eine Meldung für etwas, das nicht Funktioniert, wie es soll.
title: ''
labels: 'Task/Bug'
assignees: ''
---
# Bug Report
## Beschreibung
[Eine klare und prägnante Beschreibung des Bugs]
## Schritte zur Reproduktion
1. Schritt 1
2. Schritt 2
3. ...
## Erwartetes Verhalten
[Was hätte passieren sollen]
## Tatsächliches Verhalten
[Was tatsächlich passiert ist]
## Screenshots/Protokolle
[Falls zutreffend, füge Screenshots, Error Logs oder Stack Traces hinzu]
## Umgebung
- Plattform: Android, iOS, Web
- OS: [z. B. iOS 18.5, Android 14]
- Flutter Version: [z.B. 3.35.6]
## Verwandte Issues
[Verweisen Sie auf ähnliche Issues oder PRs]

View File

@@ -0,0 +1,22 @@
---
name: Enhancement
about: Enhancements for current features
title: ''
labels: 'Task\Enhancement'
assignees: ''
---
# Enhancement
## Aktuelles Verhalten
[Beschreibe die bestehende Funktionalität]
## Einschränkungen/Probleme
[Was sind die aktuellen Mängel?]
## Vorgeschlagene Verbesserung
[Wie kann das Problem bzw. die Einschränkung verbessert werden?]
## Zugehörige Issues
[Links zu verwandten oder blockierenden Issues]

View File

@@ -0,0 +1,19 @@
---
name: Feature
about: Neues Feature für die App
title: ''
labels: 'Task\Feature'
assignees: ''
---
# Feature
## Beschreibung
[Ausführliche Erläuterung der vorgeschlagenen Funktion]
## Vorgeschlagene Lösung
[Beschreibe, wie die Feature funktionieren soll]
## Zugehörige Issues
[Links zu verwandten oder blockierenden Issues]

View File

@@ -0,0 +1,16 @@
# [PR Titel]
**Zugehörige Issue(s):**
Closes `<issue-no>`
## Beschreibung
*Eine klare und prägnante Übersicht über die vorgenommenen Änderungen. Erläutere nicht nur das was gemacht wurde, sondern auch warum.*
## Änderungen
- [ ] Neue Funktion X hinzugefügt
- [ ] Bug in Komponente Y behoben
- [ ] Modul Z für bessere Leistung refactored
- [ ] Dependencies aktualisiert
## Zusätzliche Anmerkungen
*Gibt es zusätzlichen Kontext, Einschränkungen oder Informationen, die Reviewer wissen sollten?*

View File

@@ -1,53 +0,0 @@
name: Bug Report
about: Erstelle eine Bug Report
labels: 'Task/Bug'
title: ''
body:
- type: textarea
id: description
attributes:
label: Beschreibung
description: Beschreibe klar und pregnant das Fehlerverhalten
placeholder: |
- Was genau ist das unerwünschte Verhalten?
- Welche Auswirkungen hat der Fehler?
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Schritte zur Reproduktion
description: Beschreibe, wie der Fehler reproduziert werden kann
placeholder: |
- 1. Schritt 1
- 2. Schritt 2
- 3. ...
- type: dropdown
id: enviroment
attributes:
label: Umgebung
description: Gebe an, auf welchen Platformen dieser Fehler auftritt
list: false
multiple: true
options: ['Android', 'iOS']
- type: textarea
id: solution
attributes:
label: Lösungsidee
description: Beschreibe, wie das Problem bzw. gelöst werden kann
placeholder: |
- Button X ändern, sodass ...
- Funktion X so erweitern, dass ...
- Design anpassen, sodass ...
- type: textarea
attributes:
label: Verwandte Issues
description: Verweise auf ähnliche Issues oder PRs
placeholder: |
- Knüpft an Issue #35 an
- Ersetzt Issue #12
- Brauch Implementierung von #43

View File

@@ -1,36 +0,0 @@
name: Enhancement
about: Erstelle ein Enhancement-Ticket
labels: 'Task/Enhancement'
title: ''
body:
- type: textarea
id: description
attributes:
label: Aktuelles Verhalten
description: Beschreibe, wie die Funktionalität aktuell gestaltet ist
placeholder: |
- Aktuell macht Button X folgendes ...
- Das Problem ist, dass ...
validations:
required: true
- type: textarea
id: solution
attributes:
label: Vorgeschlagene Verbesserung
description: Beschreibe, wie das Problem bzw. die Einschränkung verbessert werden kann
placeholder: |
- Button X ändern, sodass ...
- Funktion X so erweitern, dass ...
- Design anpassen, sodass ...
validations:
required: true
- type: textarea
attributes:
label: Zugehörige Issues
description: Links zu verwandten oder blockierenden Issues
placeholder: |
- Knüpft an Issue #35 an
- Ersetzt Issue #12
- Brauch Implementierung von #43

View File

@@ -1,36 +0,0 @@
name: Feature
about: Erstelle ein Feature-Ticket
labels: 'Task/Feature'
title: ''
body:
- type: textarea
id: description
attributes:
label: Beschreibung
description: Ausführliche Erläuterung der vorgeschlagenen Funktion
placeholder: |
- Welchen Zweck erfüllt das Feature?
- Welches Problem löst das Feature?
- Wer profitiert davon?
- Warum ist es wichtig?
validations:
required: true
- type: textarea
id: solution
attributes:
label: Vorgeschlagene Lösung
description: Beschreibe, wie das Feature funktionieren soll
placeholder: |
- Neues Widget, das folgendermaßen aussieht ...
- Neue Ansicht, die folgende Inhalte hat
- Neue Funktionsweise von Komponente XY
- type: textarea
attributes:
label: Zugehörige Issues
description: Links zu verwandten oder blockierenden Issues
placeholder: |
- Knüpft an Issue #35 an
- Ersetzt Issue #12
- Brauch Implementierung von #43

View File

@@ -1,47 +0,0 @@
name: Pull Request
about: Vorlage für Pull Requests
title: "WIP: [Name des Issues]"
body:
- type: input
id: related_issue
attributes:
label: Zugehörige Issue(s)
description: Issues welche mit diesem Pull Request geschlossen werden sollen
placeholder: "Closes #123"
validations:
required: true
- type: textarea
id: description
attributes:
label: Beschreibung
description: |
Eine klare und prägnante Zusammenfassung aller vorgenommenen Änderungen.
placeholder: |
Was wurde geändert?
validations:
required: true
- type: textarea
id: changes
attributes:
label: Änderungen
description: Liste alle Änderungen detailiert auf, die in diesem Pull Request vorgenommen wurden.
placeholder: |
- Neue Funktion X hinzugefügt
- Bug in Komponente X behoben
- Modul X für bessere Leistung refactored
- Dependencies aktualisiert
- type: textarea
id: additional_notes
attributes:
label: Zusätzliche Anmerkungen
description: |
Gibt es zusätzlichen Kontext, Einschränkungen oder Informationen,
die Reviewer wissen sollten?
placeholder: |
- Bekannte Einschränkungen
- Offene TODOs
- Hinweise für Reviewer

27
.gitignore vendored
View File

@@ -150,9 +150,25 @@ app.*.symbols
.gclient_previous_custom_vars
.gclient_previous_sync_commits
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# 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
@@ -160,8 +176,17 @@ migrate_working_dir/
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
@@ -171,5 +196,3 @@ app.*.map.json
/android/app/profile
/android/app/release
/devtools_options.yaml
untranslated_messages.json

View File

@@ -1,15 +1,7 @@
# Game Tracker
![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==)
![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)
![Version](https://img.shields.io/badge/Version-0.3.0-orange)
![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)
A all-in-one app to track card- and board games, manage players and groups and get statistics about your played games.

View File

@@ -1,5 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="game_tracker"
android:name="${applicationName}"
@@ -43,14 +42,5 @@
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- Required for url_launcher to open URLs in external browser -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="http" />
</intent>
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>

View File

@@ -18,7 +18,7 @@ pluginManagement {
plugins {
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
}

View File

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

View File

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

View File

@@ -1,98 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/database.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/dto/group.dart';
part 'group_match_dao.g.dart';
@DriftAccessor(tables: [GroupMatchTable])
class GroupMatchDao extends DatabaseAccessor<AppDatabase>
with _$GroupMatchDaoMixin {
GroupMatchDao(super.db);
/// Associates a group with a match by inserting a record into the
/// [GroupMatchTable].
Future<void> addGroupToMatch({
required String matchId,
required String groupId,
}) async {
if (await matchHasGroup(matchId: matchId)) {
throw Exception('Match already has a group');
}
await into(groupMatchTable).insert(
GroupMatchTableCompanion.insert(groupId: groupId, matchId: matchId),
mode: InsertMode.insertOrIgnore,
);
}
/// Retrieves the [Group] associated with the given [matchId].
/// Returns `null` if no group is found.
Future<Group?> getGroupOfMatch({required String matchId}) async {
final result = await (select(
groupMatchTable,
)..where((g) => g.matchId.equals(matchId))).getSingleOrNull();
if (result == null) {
return null;
}
final group = await db.groupDao.getGroupById(groupId: result.groupId);
return group;
}
/// Checks if there is a group associated with the given [matchId].
/// Returns `true` if there is a group, otherwise `false`.
Future<bool> matchHasGroup({required String matchId}) async {
final count =
await (selectOnly(groupMatchTable)
..where(groupMatchTable.matchId.equals(matchId))
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Checks if a specific group is associated with a specific match.
/// Returns `true` if the group is in the match, otherwise `false`.
Future<bool> isGroupInMatch({
required String matchId,
required String groupId,
}) async {
final count =
await (selectOnly(groupMatchTable)
..where(
groupMatchTable.matchId.equals(matchId) &
groupMatchTable.groupId.equals(groupId),
)
..addColumns([groupMatchTable.groupId.count()]))
.map((row) => row.read(groupMatchTable.groupId.count()))
.getSingle();
return (count ?? 0) > 0;
}
/// Removes the association of a group from a match based on [groupId] and
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> removeGroupFromMatch({
required String matchId,
required String groupId,
}) async {
final query = delete(groupMatchTable)
..where((g) => g.matchId.equals(matchId) & g.groupId.equals(groupId));
final rowsAffected = await query.go();
return rowsAffected > 0;
}
/// Updates the group associated with a match to [newGroupId] based on
/// [matchId].
/// Returns `true` if more than 0 rows were affected, otherwise `false`.
Future<bool> updateGroupOfMatch({
required String matchId,
required String newGroupId,
}) async {
final updatedRows =
await (update(groupMatchTable)..where((g) => g.matchId.equals(matchId)))
.write(GroupMatchTableCompanion(groupId: Value(newGroupId)));
return updatedRows > 0;
}
}

View File

@@ -1,10 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'group_match_dao.dart';
// ignore_for_file: type=lint
mixin _$GroupMatchDaoMixin on DatabaseAccessor<AppDatabase> {
$GroupTableTable get groupTable => attachedDatabase.groupTable;
$MatchTableTable get matchTable => attachedDatabase.matchTable;
$GroupMatchTableTable get groupMatchTable => attachedDatabase.groupMatchTable;
}

View File

@@ -1,12 +1,10 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:game_tracker/data/dao/group_dao.dart';
import 'package:game_tracker/data/dao/group_match_dao.dart';
import 'package:game_tracker/data/dao/match_dao.dart';
import 'package:game_tracker/data/dao/player_dao.dart';
import 'package:game_tracker/data/dao/player_group_dao.dart';
import 'package:game_tracker/data/dao/player_match_dao.dart';
import 'package:game_tracker/data/db/tables/group_match_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_group_table.dart';
@@ -22,16 +20,14 @@ part 'database.g.dart';
GroupTable,
MatchTable,
PlayerGroupTable,
PlayerMatchTable,
GroupMatchTable,
PlayerMatchTable
],
daos: [
PlayerDao,
GroupDao,
MatchDao,
PlayerGroupDao,
PlayerMatchDao,
GroupMatchDao,
PlayerMatchDao
],
)
class AppDatabase extends _$AppDatabase {

View File

@@ -0,0 +1,14 @@
import 'package:drift/drift.dart';
class GameTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get ruleset => text()();
TextColumn get description => text().nullable()();
TextColumn get color => text().nullable()();
TextColumn get icon => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -1,13 +0,0 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
class GroupMatchTable extends Table {
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
@override
Set<Column<Object>> get primaryKey => {groupId, matchId};
}

View File

@@ -3,6 +3,7 @@ import 'package:drift/drift.dart';
class GroupTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,9 +1,15 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/game_table.dart';
import 'package:game_tracker/data/db/tables/group_table.dart';
class MatchTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
late final winnerId = text().nullable()();
TextColumn get gameId =>
text().references(GameTable, #id, onDelete: KeyAction.cascade)();
TextColumn get groupId =>
text().references(GroupTable, #id, onDelete: KeyAction.cascade).nullable()(); // Nullable if not part of a group
TextColumn get name => text().nullable()();
TextColumn get notes => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -1,12 +1,16 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
import 'package:game_tracker/data/db/tables/team_table.dart';
class PlayerMatchTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
TextColumn get teamId =>
text().references(TeamTable, #id).nullable()();
IntColumn get score => integer()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId};

View File

@@ -3,6 +3,7 @@ import 'package:drift/drift.dart';
class PlayerTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
TextColumn get description => text().nullable()();
DateTimeColumn get createdAt => dateTime()();
@override

View File

@@ -0,0 +1,16 @@
import 'package:drift/drift.dart';
import 'package:game_tracker/data/db/tables/match_table.dart';
import 'package:game_tracker/data/db/tables/player_table.dart';
class ScoreTable extends Table {
TextColumn get playerId =>
text().references(PlayerTable, #id, onDelete: KeyAction.cascade)();
TextColumn get matchId =>
text().references(MatchTable, #id, onDelete: KeyAction.cascade)();
IntColumn get roundNumber => integer()();
IntColumn get score => integer()();
IntColumn get change => integer()();
@override
Set<Column<Object>> get primaryKey => {playerId, matchId, roundNumber};
}

View File

@@ -0,0 +1,9 @@
import 'package:drift/drift.dart';
class TeamTable extends Table {
TextColumn get id => text()();
TextColumn get name => text()();
@override
Set<Column<Object>> get primaryKey => {id};
}

View File

@@ -3,7 +3,6 @@
"all_players": "Alle Spieler:innen",
"all_players_selected": "Alle Spieler:innen ausgewählt",
"amount_of_matches": "Anzahl der Spiele",
"app_name": "Game Tracker",
"cancel": "Abbrechen",
"choose_game": "Spielvorlage wählen",
"choose_group": "Gruppe wählen",
@@ -13,7 +12,6 @@
"create_match": "Spiel erstellen",
"create_new_group": "Neue Gruppe erstellen",
"create_new_match": "Neues Spiel erstellen",
"data": "Daten",
"data_successfully_deleted": "Daten erfolgreich gelöscht",
"data_successfully_exported": "Daten erfolgreich exportiert",
"data_successfully_imported": "Daten erfolgreich importiert",
@@ -36,17 +34,13 @@
"info": "Info",
"invalid_schema": "Ungültiges Schema",
"least_points": "Niedrigste Punkte",
"legal": "Rechtliches",
"legal_notice": "Impressum",
"licenses": "Lizenzen",
"match_in_progress": "Spiel läuft...",
"match_name": "Spieltitel",
"matches": "Spiele",
"menu": "Menü",
"most_points": "Höchste Punkte",
"no_data_available": "Keine Daten verfügbar",
"no_groups_created_yet": "Noch keine Gruppen erstellt",
"no_licenses_found": "Keine Lizenzen gefunden",
"no_license_text_available": "Kein Lizenztext verfügbar",
"no_matches_created_yet": "Noch keine Spiele erstellt",
"no_players_created_yet": "Noch keine Spieler:in erstellt",
"no_players_found_with_that_name": "Keine Spieler:in mit diesem Namen gefunden",
@@ -60,7 +54,6 @@
"player_name": "Spieler:innenname",
"players": "Spieler:innen",
"players_count": "{count} Spieler",
"privacy_policy": "Datenschutzerklärung",
"quick_create": "Schnellzugriff",
"recent_matches": "Letzte Spiele",
"ruleset": "Regelwerk",

View File

@@ -39,9 +39,6 @@
"@create_new_match": {
"description": "Button text to create a new match"
},
"@data": {
"description": "Data label"
},
"@data_successfully_deleted": {
"description": "Success message after deleting data"
},
@@ -113,15 +110,6 @@
"@least_points": {
"description": "Title for least points ruleset"
},
"@legal": {
"description": "Legal section header"
},
"@legal_notice": {
"description": "Legal notice menu item"
},
"@licenses": {
"description": "Licenses menu item"
},
"@match_in_progress": {
"description": "Message when match is in progress"
},
@@ -131,6 +119,9 @@
"@matches": {
"description": "Label for matches"
},
"@menu": {
"description": "Menu label"
},
"@most_points": {
"description": "Title for most points ruleset"
},
@@ -140,12 +131,6 @@
"@no_groups_created_yet": {
"description": "Message when no groups exist"
},
"@no_licenses_found": {
"description": "Message when no licenses are found"
},
"@no_license_text_available": {
"description": "Message when no license text is available"
},
"@no_matches_created_yet": {
"description": "Message when no matches exist"
},
@@ -190,9 +175,6 @@
}
}
},
"@privacy_policy": {
"description": "Privacy policy menu item"
},
"@quick_create": {
"description": "Title for quick create section"
},
@@ -227,7 +209,7 @@
"description": "Shows the number of selected players"
},
"@settings": {
"description": "Label for the App Settings"
"description": "Settings label"
},
"@single_loser": {
"description": "Title for single loser ruleset"
@@ -290,7 +272,6 @@
"create_match": "Create match",
"create_new_group": "Create new group",
"create_new_match": "Create new match",
"data": "Data",
"data_successfully_deleted": "Data successfully deleted",
"data_successfully_exported": "Data successfully exported",
"data_successfully_imported": "Data successfully imported",
@@ -313,17 +294,13 @@
"info": "Info",
"invalid_schema": "Invalid Schema",
"least_points": "Least Points",
"legal": "Legal",
"legal_notice": "Legal Notice",
"licenses": "Licenses",
"match_in_progress": "Match in progress...",
"match_name": "Match name",
"matches": "Matches",
"menu": "Menu",
"most_points": "Most Points",
"no_data_available": "No data available",
"no_groups_created_yet": "No groups created yet",
"no_licenses_found": "No licenses found",
"no_license_text_available": "No license text available",
"no_matches_created_yet": "No matches created yet",
"no_players_created_yet": "No players created yet",
"no_players_found_with_that_name": "No players found with that name",
@@ -337,7 +314,6 @@
"player_name": "Player name",
"players": "Players",
"players_count": "{count} Players",
"privacy_policy": "Privacy Policy",
"quick_create": "Quick Create",
"recent_matches": "Recent Matches",
"ruleset": "Ruleset",

View File

@@ -176,12 +176,6 @@ abstract class AppLocalizations {
/// **'Create new match'**
String get create_new_match;
/// Data label
///
/// In en, this message translates to:
/// **'Data'**
String get data;
/// Success message after deleting data
///
/// In en, this message translates to:
@@ -314,24 +308,6 @@ abstract class AppLocalizations {
/// **'Least Points'**
String get least_points;
/// Legal section header
///
/// In en, this message translates to:
/// **'Legal'**
String get legal;
/// Legal notice menu item
///
/// In en, this message translates to:
/// **'Legal Notice'**
String get legal_notice;
/// Licenses menu item
///
/// In en, this message translates to:
/// **'Licenses'**
String get licenses;
/// Message when match is in progress
///
/// In en, this message translates to:
@@ -350,6 +326,12 @@ abstract class AppLocalizations {
/// **'Matches'**
String get matches;
/// Menu label
///
/// In en, this message translates to:
/// **'Menu'**
String get menu;
/// Title for most points ruleset
///
/// In en, this message translates to:
@@ -368,18 +350,6 @@ abstract class AppLocalizations {
/// **'No groups created yet'**
String get no_groups_created_yet;
/// 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;
/// Message when no matches exist
///
/// In en, this message translates to:
@@ -458,12 +428,6 @@ abstract class AppLocalizations {
/// **'{count} Players'**
String players_count(int count);
/// Privacy policy menu item
///
/// In en, this message translates to:
/// **'Privacy Policy'**
String get privacy_policy;
/// Title for quick create section
///
/// In en, this message translates to:
@@ -530,7 +494,7 @@ abstract class AppLocalizations {
/// **'Selected players'**
String get selected_players;
/// Label for the App Settings
/// Settings label
///
/// In en, this message translates to:
/// **'Settings'**

View File

@@ -49,9 +49,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get create_new_match => 'Neues Spiel erstellen';
@override
String get data => 'Daten';
@override
String get data_successfully_deleted => 'Daten erfolgreich gelöscht';
@@ -121,15 +118,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get least_points => 'Niedrigste Punkte';
@override
String get legal => 'Rechtliches';
@override
String get legal_notice => 'Impressum';
@override
String get licenses => 'Lizenzen';
@override
String get match_in_progress => 'Spiel läuft...';
@@ -139,6 +127,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get matches => 'Spiele';
@override
String get menu => 'Menü';
@override
String get most_points => 'Höchste Punkte';
@@ -148,12 +139,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get no_groups_created_yet => 'Noch keine Gruppen erstellt';
@override
String get no_licenses_found => 'Keine Lizenzen gefunden';
@override
String get no_license_text_available => 'Kein Lizenztext verfügbar';
@override
String get no_matches_created_yet => 'Noch keine Spiele erstellt';
@@ -196,9 +181,6 @@ class AppLocalizationsDe extends AppLocalizations {
return '$count Spieler';
}
@override
String get privacy_policy => 'Datenschutzerklärung';
@override
String get quick_create => 'Schnellzugriff';

View File

@@ -49,9 +49,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get create_new_match => 'Create new match';
@override
String get data => 'Data';
@override
String get data_successfully_deleted => 'Data successfully deleted';
@@ -121,15 +118,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get least_points => 'Least Points';
@override
String get legal => 'Legal';
@override
String get legal_notice => 'Legal Notice';
@override
String get licenses => 'Licenses';
@override
String get match_in_progress => 'Match in progress...';
@@ -139,6 +127,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get matches => 'Matches';
@override
String get menu => 'Menu';
@override
String get most_points => 'Most Points';
@@ -148,12 +139,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get no_groups_created_yet => 'No groups created yet';
@override
String get no_licenses_found => 'No licenses found';
@override
String get no_license_text_available => 'No license text available';
@override
String get no_matches_created_yet => 'No matches created yet';
@@ -196,9 +181,6 @@ class AppLocalizationsEn extends AppLocalizations {
return '$count Players';
}
@override
String get privacy_policy => 'Privacy Policy';
@override
String get quick_create => 'Quick Create';

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/home_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/widgets/navbar_item.dart';

View File

@@ -0,0 +1,219 @@
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/settings_list_tile.dart';
import 'package:game_tracker/services/data_transfer_service.dart';
import 'package:package_info_plus/package_info_plus.dart';
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@override
State<SettingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
PackageInfo _packageInfo = PackageInfo(
appName: 'n.A.',
packageName: 'n.A.',
version: 'n.A.',
buildNumber: 'n.A.',
);
@override
void initState() {
super.initState();
_initPackageInfo();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
backgroundColor: CustomTheme.backgroundColor,
body: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
child: Text(
textAlign: TextAlign.start,
loc.menu,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: Text(
textAlign: TextAlign.start,
loc.settings,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
SettingsListTile(
title: loc.export_data,
icon: Icons.upload_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final String json = await DataTransferService.getAppDataAsJson(
context,
);
final result = await DataTransferService.exportData(
json,
'game_tracker-data',
);
if (!context.mounted) return;
showExportSnackBar(context: context, result: result);
},
),
SettingsListTile(
title: loc.import_data,
icon: Icons.download_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final result = await DataTransferService.importData(context);
if (!context.mounted) return;
showImportSnackBar(context: context, result: result);
},
),
SettingsListTile(
title: loc.delete_all_data,
icon: Icons.delete_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () {
showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('${loc.delete_all_data}?'),
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) {
if (confirmed == true && context.mounted) {
DataTransferService.deleteAllData(context);
showSnackbar(
context: context,
message: AppLocalizations.of(
context,
).data_successfully_deleted,
);
}
});
},
),
const Spacer(),
Padding(
padding: const EdgeInsets.all(20),
child: Center(
child: Text(
'Version ${_packageInfo.version} (${_packageInfo.buildNumber})',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
),
);
}
/// Displays a snackbar based on the import result.
///
/// [context] The BuildContext to show the snackbar in.
/// [result] The result of the import operation.
void showImportSnackBar({
required BuildContext context,
required ImportResult result,
}) {
final loc = AppLocalizations.of(context);
switch (result) {
case ImportResult.success:
showSnackbar(context: context, message: loc.data_successfully_imported);
case ImportResult.invalidSchema:
showSnackbar(context: context, message: loc.invalid_schema);
case ImportResult.fileReadError:
showSnackbar(context: context, message: loc.error_reading_file);
case ImportResult.canceled:
showSnackbar(context: context, message: loc.import_canceled);
case ImportResult.formatException:
showSnackbar(context: context, message: loc.format_exception);
case ImportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception);
}
}
/// Displays a snackbar based on the export result.
///
/// [context] The BuildContext to show the snackbar in.
/// [result] The result of the export operation.
void showExportSnackBar({
required BuildContext context,
required ExportResult result,
}) {
final loc = AppLocalizations.of(context);
switch (result) {
case ExportResult.success:
showSnackbar(context: context, message: loc.data_successfully_exported);
case ExportResult.canceled:
showSnackbar(context: context, message: loc.export_canceled);
case ExportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception);
}
}
/// Displays a snackbar with the given message and optional action.
///
/// [context] The BuildContext to show the snackbar in.
/// [message] The message to display in the snackbar.
/// [duration] The duration for which the snackbar is displayed.
/// [action] An optional callback function to execute when the action button is pressed.
void showSnackbar({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
VoidCallback? action,
}) {
final loc = AppLocalizations.of(context);
final messenger = ScaffoldMessenger.of(context);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.onBoxColor,
duration: duration,
action: action != null
? SnackBarAction(label: loc.undo, onPressed: action)
: null,
),
);
}
Future<void> _initPackageInfo() async {
final info = await PackageInfo.fromPlatform();
setState(() {
_packageInfo = info;
});
}
}

View File

@@ -1,137 +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:url_launcher/url_launcher.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),
GestureDetector(
onTap: () async {
final uri = Uri.parse(package.homepage!);
await launchUrl(uri, mode: LaunchMode.platformDefault);
},
child: SizedBox(
width: 300,
child: Text(
package.homepage!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: CustomTheme.secondaryColor,
decoration: TextDecoration.underline,
decorationColor: CustomTheme.secondaryColor,
),
),
),
),
],
],
),
),
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

@@ -1,310 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.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/enums.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/services/data_transfer_service.dart';
import 'package:intl/intl.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class SettingsView extends StatefulWidget {
const SettingsView({super.key});
@override
State<SettingsView> createState() => _SettingsViewState();
}
class _SettingsViewState extends State<SettingsView> {
PackageInfo _packageInfo = PackageInfo(
appName: 'n.A.',
packageName: 'n.A.',
version: 'n.A.',
buildNumber: 'n.A.',
);
@override
void initState() {
super.initState();
_initPackageInfo();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
backgroundColor: CustomTheme.backgroundColor,
body: SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 16, bottom: 10),
child: Text(
textAlign: TextAlign.start,
loc.settings,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10),
child: Text(
textAlign: TextAlign.start,
loc.data,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
),
SettingsListTile(
title: loc.export_data,
icon: Icons.upload,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final String json =
await DataTransferService.getAppDataAsJson(context);
final result = await DataTransferService.exportData(
json,
'game_tracker-data',
);
if (!context.mounted) return;
showExportSnackBar(context: context, result: result);
},
),
SettingsListTile(
title: loc.import_data,
icon: Icons.download,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () async {
final result = await DataTransferService.importData(context);
if (!context.mounted) return;
showImportSnackBar(context: context, result: result);
},
),
SettingsListTile(
title: loc.delete_all_data,
icon: Icons.delete,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: () {
showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('${loc.delete_all_data}?'),
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) {
if (confirmed == true && context.mounted) {
DataTransferService.deleteAllData(context);
showSnackbar(
context: context,
message: AppLocalizations.of(
context,
).data_successfully_deleted,
);
}
});
},
),
Padding(
padding: const EdgeInsets.only(left: 16, top: 10, bottom: 10),
child: Text(
textAlign: TextAlign.start,
loc.legal,
style: const 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(),
),
);
},
),
SettingsListTile(
title: loc.legal_notice,
icon: Icons.account_balance_sharp,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: null,
),
SettingsListTile(
title: loc.privacy_policy,
icon: Icons.gpp_good_rounded,
suffixWidget: const Icon(Icons.arrow_forward_ios, size: 16),
onPressed: null,
),
Padding(
padding: const EdgeInsets.only(top: 30, bottom: 20),
child: Center(
child: Column(
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})',
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
],
),
),
),
);
}
/// Displays a snackbar based on the import result.
///
/// [context] The BuildContext to show the snackbar in.
/// [result] The result of the import operation.
void showImportSnackBar({
required BuildContext context,
required ImportResult result,
}) {
final loc = AppLocalizations.of(context);
switch (result) {
case ImportResult.success:
showSnackbar(context: context, message: loc.data_successfully_imported);
case ImportResult.invalidSchema:
showSnackbar(context: context, message: loc.invalid_schema);
case ImportResult.fileReadError:
showSnackbar(context: context, message: loc.error_reading_file);
case ImportResult.canceled:
showSnackbar(context: context, message: loc.import_canceled);
case ImportResult.formatException:
showSnackbar(context: context, message: loc.format_exception);
case ImportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception);
}
}
/// Displays a snackbar based on the export result.
///
/// [context] The BuildContext to show the snackbar in.
/// [result] The result of the export operation.
void showExportSnackBar({
required BuildContext context,
required ExportResult result,
}) {
final loc = AppLocalizations.of(context);
switch (result) {
case ExportResult.success:
showSnackbar(context: context, message: loc.data_successfully_exported);
case ExportResult.canceled:
showSnackbar(context: context, message: loc.export_canceled);
case ExportResult.unknownException:
showSnackbar(context: context, message: loc.unknown_exception);
}
}
/// Displays a snackbar with the given message and optional action.
///
/// [context] The BuildContext to show the snackbar in.
/// [message] The message to display in the snackbar.
/// [duration] The duration for which the snackbar is displayed.
/// [action] An optional callback function to execute when the action button is pressed.
void showSnackbar({
required BuildContext context,
required String message,
Duration duration = const Duration(seconds: 3),
VoidCallback? action,
}) {
final loc = AppLocalizations.of(context);
final messenger = ScaffoldMessenger.of(context);
messenger.hideCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message, style: const TextStyle(color: Colors.white)),
backgroundColor: CustomTheme.onBoxColor,
duration: duration,
action: action != null
? SnackBarAction(label: loc.undo, onPressed: action)
: null,
),
);
}
/// Initializes the package information.
Future<void> _initPackageInfo() async {
final info = await PackageInfo.fromPlatform();
setState(() {
_packageInfo = info;
});
}
}

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.symmetric(vertical: 10, horizontal: 12),
decoration: CustomTheme.standardBoxDecoration.copyWith(
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
Icons.description,
color: CustomTheme.primaryColor,
size: 32,
),
),
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

@@ -38,7 +38,7 @@ class SettingsListTile extends StatelessWidget {
onTap: onPressed ?? () {},
child: Container(
margin: EdgeInsets.zero,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
decoration: CustomTheme.standardBoxDecoration,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -47,17 +47,12 @@ class SettingsListTile extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 44,
height: 44,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: CustomTheme.primaryColor.withAlpha(40),
borderRadius: BorderRadius.circular(10),
),
child: Icon(
icon,
size: 28,
color: CustomTheme.primaryColor.withGreen(40),
color: CustomTheme.primaryColor,
shape: BoxShape.circle,
),
child: Icon(icon, size: 24),
),
const SizedBox(width: 16),
Text(title, style: const TextStyle(fontSize: 18)),

View File

@@ -1,7 +1,7 @@
name: game_tracker
description: "Game Tracking App for Card Games"
publish_to: 'none'
version: 0.0.5+143
version: 0.0.4+101
environment:
sdk: ^3.8.1
@@ -9,32 +9,27 @@ environment:
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
clock: ^1.1.2
cupertino_icons: ^1.0.6
drift: ^2.27.0
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
provider: ^6.1.5
skeletonizer: ^2.1.0+1
url_launcher: ^6.3.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:
flutter_test:
sdk: flutter
build_runner: ^2.5.4
dart_pubspec_licenses: ^3.0.14
drift_dev: ^2.27.0
flutter_lints: ^5.0.0
drift_dev: ^2.27.0
build_runner: ^2.5.4
flutter:
uses-material-design: true