1 Commits

Author SHA1 Message Date
f0379e97c7 Merge pull request 'MVP' (#141) from development into main
Reviewed-on: #141
Reviewed-by: gelbeinhalb <spam@yannick-weigert.de>
2026-01-09 12:55:50 +00:00
18 changed files with 445 additions and 531 deletions

View File

@@ -1,19 +0,0 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
Route<T> adaptivePageRoute<T>({
required Widget Function(BuildContext) builder,
bool fullscreenDialog = false,
}) {
if (Platform.isIOS) {
return CupertinoPageRoute<T>(
builder: builder,
fullscreenDialog: fullscreenDialog,
);
}
return MaterialPageRoute<T>(
builder: builder,
fullscreenDialog: fullscreenDialog,
);
}

View File

@@ -9,7 +9,7 @@ class Match {
final String name; final String name;
final List<Player>? players; final List<Player>? players;
final Group? group; final Group? group;
Player? winner; final Player? winner;
Match({ Match({
String? id, String? id,

View File

@@ -17,7 +17,7 @@
"data_successfully_imported": "Daten erfolgreich importiert", "data_successfully_imported": "Daten erfolgreich importiert",
"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?",
"error_creating_group": "Fehler beim Erstellen der Gruppe, bitte erneut versuchen", "error_creating_group": "Fehler beim Erstellen 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",

View File

@@ -277,7 +277,7 @@
"data_successfully_imported": "Data successfully imported", "data_successfully_imported": "Data successfully imported",
"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?",
"error_creating_group": "Error while creating group, please try again", "error_creating_group": "Error while creating group, please try again",
"error_reading_file": "Error reading file", "error_reading_file": "Error reading file",
"export_canceled": "Export canceled", "export_canceled": "Export canceled",

View File

@@ -209,7 +209,7 @@ abstract class AppLocalizations {
/// Confirmation dialog for deleting all data /// Confirmation dialog for deleting all data
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete all data'** /// **'Delete all data?'**
String get delete_all_data; String get delete_all_data;
/// Error message when group creation fails /// Error message when group creation fails

View File

@@ -67,7 +67,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get delete => 'Löschen'; String get delete => 'Löschen';
@override @override
String get delete_all_data => 'Alle Daten löschen'; String get delete_all_data => 'Alle Daten löschen?';
@override @override
String get error_creating_group => String get error_creating_group =>

View File

@@ -67,7 +67,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get delete => 'Delete'; String get delete => 'Delete';
@override @override
String get delete_all_data => 'Delete all data'; String get delete_all_data => 'Delete all data?';
@override @override
String get error_creating_group => String get error_creating_group =>

View File

@@ -44,12 +44,6 @@ class GameTracker extends StatelessWidget {
seedColor: CustomTheme.primaryColor, seedColor: CustomTheme.primaryColor,
brightness: Brightness.dark, brightness: Brightness.dark,
).copyWith(surface: CustomTheme.backgroundColor), ).copyWith(surface: CustomTheme.backgroundColor),
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.iOS: CupertinoPageTransitionsBuilder(),
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
},
),
), ),
home: const CustomNavigationBar(), home: const CustomNavigationBar(),
); );

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; 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';
@@ -57,7 +56,7 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
onPressed: () async { onPressed: () async {
await Navigator.push( await Navigator.push(
context, context,
adaptivePageRoute(builder: (_) => const SettingsView()), MaterialPageRoute(builder: (_) => const SettingsView()),
); );
setState(() { setState(() {
tabKeyCount++; tabKeyCount++;

View File

@@ -44,68 +44,66 @@ class _CreateGroupViewState extends State<CreateGroupView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return Scaffold(
child: Scaffold( backgroundColor: CustomTheme.backgroundColor,
backgroundColor: CustomTheme.backgroundColor, appBar: AppBar(title: Text(loc.create_new_group)),
appBar: AppBar(title: Text(loc.create_new_group)), body: SafeArea(
body: SafeArea( child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
children: [ Container(
Container( margin: CustomTheme.standardMargin,
margin: CustomTheme.standardMargin, child: TextInputField(
child: TextInputField( controller: _groupNameController,
controller: _groupNameController, hintText: loc.group_name,
hintText: loc.group_name,
),
), ),
Expanded( ),
child: PlayerSelection( Expanded(
onChanged: (value) { child: PlayerSelection(
setState(() { onChanged: (value) {
selectedPlayers = [...value]; setState(() {
}); selectedPlayers = [...value];
}, });
), },
), ),
CustomWidthButton( ),
text: loc.create_group, CustomWidthButton(
sizeRelativeToWidth: 0.95, text: loc.create_group,
buttonType: ButtonType.primary, sizeRelativeToWidth: 0.95,
onPressed: buttonType: ButtonType.primary,
(_groupNameController.text.isEmpty || onPressed:
(selectedPlayers.length < 2)) (_groupNameController.text.isEmpty ||
? null (selectedPlayers.length < 2))
: () async { ? null
bool success = await db.groupDao.addGroup( : () async {
group: Group( bool success = await db.groupDao.addGroup(
name: _groupNameController.text.trim(), group: Group(
members: selectedPlayers, name: _groupNameController.text.trim(),
), members: selectedPlayers,
); ),
if (!context.mounted) return; );
if (success) { if (!context.mounted) return;
Navigator.pop(context); if (success) {
} else { Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar( } else {
SnackBar( ScaffoldMessenger.of(context).showSnackBar(
backgroundColor: CustomTheme.boxColor, SnackBar(
content: Center( backgroundColor: CustomTheme.boxColor,
child: Text( content: Center(
AppLocalizations.of( child: Text(
context, AppLocalizations.of(
).error_creating_group, context,
style: const TextStyle(color: Colors.white), ).error_creating_group,
), style: const TextStyle(color: Colors.white),
), ),
), ),
); ),
} );
}, }
), },
const SizedBox(height: 20), ),
], const SizedBox(height: 20),
), ],
), ),
), ),
); );

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
@@ -86,7 +85,7 @@ class _GroupsViewState extends State<GroupsView> {
onPressed: () async { onPressed: () async {
await Navigator.push( await Navigator.push(
context, context,
adaptivePageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return const CreateGroupView(); return const CreateGroupView();
}, },

View File

@@ -1,16 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/data/db/database.dart'; 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/match.dart'; import 'package:game_tracker/data/dto/match.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/match_view/match_result_view.dart';
import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart';
import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart'; import 'package:game_tracker/presentation/widgets/buttons/quick_create_button.dart';
import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/info_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/match_summary_tile.dart';
import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart'; import 'package:game_tracker/presentation/widgets/tiles/quick_info_tile.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -45,6 +43,7 @@ class _HomeViewState extends State<HomeView> {
Player(name: 'Skeleton Player 2'), Player(name: 'Skeleton Player 2'),
], ],
), ),
winner: Player(name: 'Skeleton Player 1'),
), ),
); );
@@ -91,92 +90,121 @@ class _HomeViewState extends State<HomeView> {
child: InfoTile( child: InfoTile(
width: constraints.maxWidth * 0.95, width: constraints.maxWidth * 0.95,
title: loc.recent_matches, title: loc.recent_matches,
icon: Icons.history_rounded, icon: Icons.timer,
content: Column( content: Padding(
children: [ padding: const EdgeInsets.symmetric(horizontal: 40.0),
if (recentMatches.isNotEmpty) child: Visibility(
for (Match match in recentMatches) visible: !isLoading && loadedRecentMatches.isNotEmpty,
Padding( replacement: Center(
padding: const EdgeInsets.symmetric( heightFactor: 12,
vertical: 6.0, child: Text(
), AppLocalizations.of(
child: MatchTile( context,
compact: true, ).no_recent_matches_available,
width: constraints.maxWidth * 0.9,
match: match,
onTap: () async {
await Navigator.of(context).push(
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) =>
MatchResultView(match: match),
),
);
await updatedWinnerinRecentMatches(match.id);
},
),
)
else
Center(
heightFactor: 5,
child: Text(loc.no_recent_matches_available),
), ),
], ),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MatchSummaryTile(
matchTitle: recentMatches[0].name,
game: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(
recentMatches[0],
context,
),
winner: recentMatches[0].winner == null
? AppLocalizations.of(
context,
).match_in_progress
: recentMatches[0].winner!.name,
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8.0),
child: Divider(),
),
if (loadedRecentMatches.length > 1) ...[
MatchSummaryTile(
matchTitle: recentMatches[1].name,
game: 'Winner',
ruleset: 'Ruleset',
players: _getPlayerText(
recentMatches[1],
context,
),
winner: recentMatches[1].winner == null
? AppLocalizations.of(
context,
).match_in_progress
: recentMatches[1].winner!.name,
),
const SizedBox(height: 8),
] else ...[
Center(
heightFactor: 5.35,
child: Text(
AppLocalizations.of(
context,
).no_second_match_available,
),
),
],
],
),
),
), ),
), ),
), ),
Padding( InfoTile(
padding: EdgeInsets.zero, width: constraints.maxWidth * 0.95,
child: InfoTile( title: loc.quick_create,
width: constraints.maxWidth * 0.95, icon: Icons.add_box_rounded,
title: loc.quick_create, content: Column(
icon: Icons.add_box_rounded, children: [
content: Column( Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceEvenly, QuickCreateButton(
children: [ text: 'Category 1',
QuickCreateButton( onPressed: () {},
text: 'Category 1', ),
onPressed: () {}, QuickCreateButton(
), text: 'Category 2',
QuickCreateButton( onPressed: () {},
text: 'Category 2', ),
onPressed: () {}, ],
), ),
], Row(
), mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceEvenly, QuickCreateButton(
children: [ text: 'Category 3',
QuickCreateButton( onPressed: () {},
text: 'Category 3', ),
onPressed: () {}, QuickCreateButton(
), text: 'Category 4',
QuickCreateButton( onPressed: () {},
text: 'Category 4', ),
onPressed: () {}, ],
), ),
], Row(
), mainAxisAlignment: MainAxisAlignment.spaceEvenly,
Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceEvenly, QuickCreateButton(
children: [ text: 'Category 5',
QuickCreateButton( onPressed: () {},
text: 'Category 5', ),
onPressed: () {}, QuickCreateButton(
), text: 'Category 6',
QuickCreateButton( onPressed: () {},
text: 'Category 6', ),
onPressed: () {}, ],
), ),
], ],
),
],
),
), ),
), ),
SizedBox(height: MediaQuery.paddingOf(context).bottom),
], ],
), ),
), ),
@@ -187,7 +215,7 @@ class _HomeViewState extends State<HomeView> {
/// Loads the data for the HomeView from the database. /// Loads the data for the HomeView from the database.
/// This includes the match count, group count, and recent matches. /// This includes the match count, group count, and recent matches.
Future<void> loadHomeViewData() async { void loadHomeViewData() {
final db = Provider.of<AppDatabase>(context, listen: false); final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([ Future.wait([
db.matchDao.getMatchCount(), db.matchDao.getMatchCount(),
@@ -203,6 +231,11 @@ class _HomeViewState extends State<HomeView> {
..sort((a, b) => b.createdAt.compareTo(a.createdAt))) ..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2) .take(2)
.toList(); .toList();
if (loadedRecentMatches.length < 2) {
recentMatches.add(
Match(name: 'Dummy Match', winner: null, group: null, players: null),
);
}
if (mounted) { if (mounted) {
setState(() { setState(() {
isLoading = false; isLoading = false;
@@ -211,15 +244,18 @@ class _HomeViewState extends State<HomeView> {
}); });
} }
/// Updates the winner information for a specific match in the recent matches list. /// Generates a text representation of the players in the match.
Future<void> updatedWinnerinRecentMatches(String matchId) async { /// If the match has a group, it returns the group name and the number of additional players.
final db = Provider.of<AppDatabase>(context, listen: false); /// If there is no group, it returns the count of players.
final winner = await db.matchDao.getWinner(matchId: matchId); String _getPlayerText(Match game, context) {
final matchIndex = recentMatches.indexWhere((match) => match.id == matchId); final loc = AppLocalizations.of(context);
if (matchIndex != -1) { if (game.group == null) {
setState(() { final playerCount = game.players?.length ?? 0;
recentMatches[matchIndex].winner = winner; return loc.players_count(playerCount);
});
} }
if (game.players == null || game.players!.isEmpty) {
return game.group!.name;
}
return '${game.group!.name} + ${game.players!.length}';
} }
} }

View File

@@ -140,11 +140,7 @@ class _ChooseGroupViewState extends State<ChooseGroupView> {
filteredGroups.clear(); filteredGroups.clear();
filteredGroups.addAll( filteredGroups.addAll(
widget.groups.where( widget.groups.where(
(group) => (group) => group.name.toLowerCase().contains(query.toLowerCase()),
group.name.toLowerCase().contains(query.toLowerCase()) ||
group.members.any(
(player) => player.name.toLowerCase().contains(query.toLowerCase()),
),
), ),
); );
} }

View File

@@ -1,5 +1,5 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/core/enums.dart'; import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
@@ -119,142 +119,140 @@ class _CreateMatchViewState extends State<CreateMatchView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return 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)), body: SafeArea(
body: SafeArea( child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, children: [
children: [ Container(
Container( margin: CustomTheme.tileMargin,
margin: CustomTheme.tileMargin, child: TextInputField(
child: TextInputField( controller: _matchNameController,
controller: _matchNameController, hintText: hintText ?? '',
hintText: hintText ?? '',
),
), ),
ChooseTile( ),
title: loc.game, ChooseTile(
trailingText: selectedGameIndex == -1 title: loc.game,
? loc.none trailingText: selectedGameIndex == -1
: games[selectedGameIndex].$1, ? loc.none
onPressed: () async { : games[selectedGameIndex].$1,
selectedGameIndex = await Navigator.of(context).push( onPressed: () async {
adaptivePageRoute( selectedGameIndex = await Navigator.of(context).push(
builder: (context) => ChooseGameView( MaterialPageRoute(
games: games, builder: (context) => ChooseGameView(
initialGameIndex: selectedGameIndex, games: games,
), initialGameIndex: selectedGameIndex,
), ),
); ),
);
setState(() {
if (selectedGameIndex != -1) {
hintText = games[selectedGameIndex].$1;
selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
} else {
hintText = loc.match_name;
selectedRuleset = null;
}
});
},
),
ChooseTile(
title: loc.ruleset,
trailingText: selectedRuleset == null
? loc.none
: translateRulesetToString(selectedRuleset!, context),
onPressed: () async {
selectedRuleset = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseRulesetView(
rulesets: _rulesets,
initialRulesetIndex: selectedRulesetIndex,
),
),
);
if (!mounted) return;
selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
selectedGameIndex = -1;
setState(() {});
},
),
ChooseTile(
title: loc.group,
trailingText: selectedGroup == null
? loc.none_group
: selectedGroup!.name,
onPressed: () async {
selectedGroup = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroupId,
),
),
);
selectedGroupId = selectedGroup?.id ?? '';
if (selectedGroup != null) {
filteredPlayerList = playerList
.where(
(p) => !selectedGroup!.members.any((m) => m.id == p.id),
)
.toList();
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {});
},
),
Expanded(
child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers ?? [],
availablePlayers: filteredPlayerList,
onChanged: (value) {
setState(() { setState(() {
if (selectedGameIndex != -1) { selectedPlayers = value;
hintText = games[selectedGameIndex].$1;
selectedRuleset = games[selectedGameIndex].$3;
selectedRulesetIndex = _rulesets.indexWhere(
(r) => r.$1 == selectedRuleset,
);
} else {
hintText = loc.match_name;
selectedRuleset = null;
}
}); });
}, },
), ),
ChooseTile( ),
title: loc.ruleset, CustomWidthButton(
trailingText: selectedRuleset == null text: loc.create_match,
? loc.none sizeRelativeToWidth: 0.95,
: translateRulesetToString(selectedRuleset!, context), buttonType: ButtonType.primary,
onPressed: () async { onPressed: _enableCreateGameButton()
selectedRuleset = await Navigator.of(context).push( ? () async {
adaptivePageRoute( Match match = Match(
builder: (context) => ChooseRulesetView( name: _matchNameController.text.isEmpty
rulesets: _rulesets, ? (hintText ?? '')
initialRulesetIndex: selectedRulesetIndex, : _matchNameController.text.trim(),
), createdAt: DateTime.now(),
), group: selectedGroup,
); players: selectedPlayers,
if (!mounted) return; );
selectedRulesetIndex = _rulesets.indexWhere( await db.matchDao.addMatch(match: match);
(r) => r.$1 == selectedRuleset, if (context.mounted) {
); Navigator.pushReplacement(
selectedGameIndex = -1; context,
setState(() {}); CupertinoPageRoute(
}, fullscreenDialog: true,
), builder: (context) => MatchResultView(
ChooseTile( match: match,
title: loc.group, onWinnerChanged: widget.onWinnerChanged,
trailingText: selectedGroup == null
? loc.none_group
: selectedGroup!.name,
onPressed: () async {
selectedGroup = await Navigator.of(context).push(
adaptivePageRoute(
builder: (context) => ChooseGroupView(
groups: groupsList,
initialGroupId: selectedGroupId,
),
),
);
selectedGroupId = selectedGroup?.id ?? '';
if (selectedGroup != null) {
filteredPlayerList = playerList
.where(
(p) => !selectedGroup!.members.any((m) => m.id == p.id),
)
.toList();
} else {
filteredPlayerList = List.from(playerList);
}
setState(() {});
},
),
Expanded(
child: PlayerSelection(
key: ValueKey(selectedGroup?.id ?? 'no_group'),
initialSelectedPlayers: selectedPlayers ?? [],
availablePlayers: filteredPlayerList,
onChanged: (value) {
setState(() {
selectedPlayers = value;
});
},
),
),
CustomWidthButton(
text: loc.create_match,
sizeRelativeToWidth: 0.95,
buttonType: ButtonType.primary,
onPressed: _enableCreateGameButton()
? () async {
Match match = Match(
name: _matchNameController.text.isEmpty
? (hintText ?? '')
: _matchNameController.text.trim(),
createdAt: DateTime.now(),
group: selectedGroup,
players: selectedPlayers,
);
await db.matchDao.addMatch(match: match);
if (context.mounted) {
Navigator.pushReplacement(
context,
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) => MatchResultView(
match: match,
onWinnerChanged: widget.onWinnerChanged,
),
), ),
); ),
} );
} }
: null, }
), : null,
], ),
), ],
), ),
), ),
); );

View File

@@ -1,7 +1,7 @@
import 'dart:core' hide Match; import 'dart:core' hide Match;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:game_tracker/core/adaptive_page_route.dart';
import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/constants.dart';
import 'package:game_tracker/core/custom_theme.dart'; import 'package:game_tracker/core/custom_theme.dart';
import 'package:game_tracker/data/db/database.dart'; import 'package:game_tracker/data/db/database.dart';
@@ -78,26 +78,20 @@ class _MatchViewState extends State<MatchView> {
height: MediaQuery.paddingOf(context).bottom - 20, height: MediaQuery.paddingOf(context).bottom - 20,
); );
} }
return Center( return MatchTile(
child: Padding( onTap: () async {
padding: const EdgeInsets.only(bottom: 12.0), Navigator.push(
child: MatchTile( context,
width: MediaQuery.sizeOf(context).width * 0.95, CupertinoPageRoute(
onTap: () async { fullscreenDialog: true,
Navigator.push( builder: (context) => MatchResultView(
context, match: matches[index],
adaptivePageRoute( onWinnerChanged: loadGames,
fullscreenDialog: true, ),
builder: (context) => MatchResultView( ),
match: matches[index], );
onWinnerChanged: loadGames, },
), match: matches[index],
),
);
},
match: matches[index],
),
),
); );
}, },
), ),
@@ -111,7 +105,7 @@ class _MatchViewState extends State<MatchView> {
onPressed: () async { onPressed: () async {
Navigator.push( Navigator.push(
context, context,
adaptivePageRoute( MaterialPageRoute(
builder: (context) => builder: (context) =>
CreateMatchView(onWinnerChanged: loadGames), CreateMatchView(onWinnerChanged: loadGames),
), ),

View File

@@ -4,7 +4,6 @@ import 'package:game_tracker/core/enums.dart';
import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart';
import 'package:game_tracker/presentation/widgets/tiles/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:package_info_plus/package_info_plus.dart';
class SettingsView extends StatefulWidget { class SettingsView extends StatefulWidget {
const SettingsView({super.key}); const SettingsView({super.key});
@@ -14,127 +13,113 @@ class SettingsView extends StatefulWidget {
} }
class _SettingsViewState extends State<SettingsView> { class _SettingsViewState extends State<SettingsView> {
PackageInfo _packageInfo = PackageInfo(
appName: 'n.A.',
packageName: 'n.A.',
version: 'n.A.',
buildNumber: 'n.A.',
);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initPackageInfo();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final loc = AppLocalizations.of(context); final loc = AppLocalizations.of(context);
return ScaffoldMessenger( return Scaffold(
child: Scaffold( appBar: AppBar(backgroundColor: CustomTheme.backgroundColor),
appBar: AppBar(backgroundColor: CustomTheme.backgroundColor), backgroundColor: CustomTheme.backgroundColor,
backgroundColor: CustomTheme.backgroundColor, body: LayoutBuilder(
body: Column( builder: (BuildContext context, BoxConstraints constraints) =>
mainAxisAlignment: MainAxisAlignment.start, SingleChildScrollView(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ mainAxisAlignment: MainAxisAlignment.start,
Padding( crossAxisAlignment: CrossAxisAlignment.start,
padding: const EdgeInsets.fromLTRB(24, 0, 24, 10), children: [
child: Text( Padding(
textAlign: TextAlign.start, padding: const EdgeInsets.fromLTRB(24, 0, 24, 10),
loc.menu, child: Text(
style: const TextStyle( textAlign: TextAlign.start,
fontSize: 28, loc.menu,
fontWeight: FontWeight.bold, 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) { Padding(
if (confirmed == true && context.mounted) { padding: const EdgeInsets.symmetric(
DataTransferService.deleteAllData(context); horizontal: 24,
showSnackbar( vertical: 10,
context: context, ),
message: AppLocalizations.of( 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, context,
).data_successfully_deleted, );
); if (!context.mounted) return;
} showImportSnackBar(context: context, result: result);
}); },
},
),
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,
), ),
), 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,
);
}
});
},
),
],
), ),
), ),
],
),
), ),
); );
} }
@@ -209,11 +194,4 @@ class _SettingsViewState extends State<SettingsView> {
), ),
); );
} }
Future<void> _initPackageInfo() async {
final info = await PackageInfo.fromPlatform();
setState(() {
_packageInfo = info;
});
}
} }

View File

@@ -10,16 +10,8 @@ import 'package:intl/intl.dart';
/// creation date, associated group, winner, and players. /// creation date, associated group, winner, and players.
/// - [match]: The match data to be displayed. /// - [match]: The match data to be displayed.
/// - [onTap]: The callback invoked when the tile is tapped. /// - [onTap]: The callback invoked when the tile is tapped.
/// - [width]: Optional width for the tile.
/// - [compact]: Whether to display the tile in a compact mode
class MatchTile extends StatefulWidget { class MatchTile extends StatefulWidget {
const MatchTile({ const MatchTile({super.key, required this.match, required this.onTap});
super.key,
required this.match,
required this.onTap,
this.width,
this.compact = false,
});
/// The match data to be displayed. /// The match data to be displayed.
final Match match; final Match match;
@@ -27,12 +19,6 @@ class MatchTile extends StatefulWidget {
/// The callback invoked when the tile is tapped. /// The callback invoked when the tile is tapped.
final VoidCallback onTap; final VoidCallback onTap;
/// Optional width for the tile.
final double? width;
/// Whether to display the tile in a compact mode
final bool compact;
@override @override
State<MatchTile> createState() => _MatchTileState(); State<MatchTile> createState() => _MatchTileState();
} }
@@ -55,10 +41,13 @@ class _MatchTileState extends State<MatchTile> {
return GestureDetector( return GestureDetector(
onTap: widget.onTap, onTap: widget.onTap,
child: Container( child: Container(
margin: EdgeInsets.zero, margin: CustomTheme.tileMargin,
width: widget.width,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: CustomTheme.standardBoxDecoration, decoration: BoxDecoration(
color: CustomTheme.boxColor,
border: Border.all(color: CustomTheme.boxBorder),
borderRadius: BorderRadius.circular(12),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -91,22 +80,7 @@ class _MatchTileState extends State<MatchTile> {
const SizedBox(width: 6), const SizedBox(width: 6),
Expanded( Expanded(
child: Text( child: Text(
'${group.name}${widget.match.players != null ? ' + ${widget.match.players?.length}' : ''}', group.name,
style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 12),
] else if (widget.compact) ...[
Row(
children: [
const Icon(Icons.person, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
'${widget.match.players!.length} ${loc.players}',
style: const TextStyle(fontSize: 14, color: Colors.grey), style: const TextStyle(fontSize: 14, color: Colors.grey),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -153,46 +127,9 @@ class _MatchTileState extends State<MatchTile> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
] else ...[
Container(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 12,
),
decoration: BoxDecoration(
color: Colors.amber.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Colors.amber.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
const Icon(
Icons.watch_later,
size: 20,
color: Colors.amber,
),
const SizedBox(width: 8),
Expanded(
child: Text(
loc.match_in_progress,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: CustomTheme.textColor,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
),
const SizedBox(height: 12),
], ],
if (_allPlayers.isNotEmpty && widget.compact == false) ...[ if (_allPlayers.isNotEmpty) ...[
Text( Text(
loc.players, loc.players,
style: const TextStyle( style: const TextStyle(

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.4+101 version: 0.0.1+21
environment: environment:
sdk: ^3.8.1 sdk: ^3.8.1
@@ -9,6 +9,11 @@ environment:
dependencies: dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
material_symbols_icons: ^4.2815.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
drift: ^2.27.0 drift: ^2.27.0
drift_flutter: ^0.2.4 drift_flutter: ^0.2.4
path_provider: ^2.1.5 path_provider: ^2.1.5
@@ -22,7 +27,6 @@ dependencies:
intl: any intl: any
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
package_info_plus: ^9.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: