Merge branch 'development' into feature/119-implementierung-der-games-2
All checks were successful
Pull Request Pipeline / test (pull_request) Successful in 49s
Pull Request Pipeline / lint (pull_request) Successful in 50s

This commit is contained in:
2026-05-09 11:30:26 +02:00
7 changed files with 122 additions and 330 deletions

View File

@@ -3,7 +3,6 @@ import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/custom_theme.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/group_view/group_view.dart';
import 'package:tallee/presentation/views/main_menu/home_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_view.dart';
import 'package:tallee/presentation/views/main_menu/settings_view/settings_view.dart';
import 'package:tallee/presentation/views/main_menu/statistics_view.dart';
@@ -31,7 +30,6 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
final loc = AppLocalizations.of(context);
// Pretty ugly but works
final List<Widget> tabs = [
KeyedSubtree(key: ValueKey('home_$tabKeyCount'), child: const HomeView()),
KeyedSubtree(
key: ValueKey('matches_$tabKeyCount'),
child: const MatchView(),
@@ -101,27 +99,20 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
NavbarItem(
index: 0,
isSelected: currentIndex == 0,
icon: Icons.home_rounded,
label: loc.home,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 1,
isSelected: currentIndex == 1,
icon: Icons.gamepad_rounded,
label: loc.matches,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 2,
isSelected: currentIndex == 2,
index: 1,
isSelected: currentIndex == 1,
icon: Icons.group_rounded,
label: loc.groups,
onTabTapped: onTabTapped,
),
NavbarItem(
index: 3,
isSelected: currentIndex == 3,
index: 2,
isSelected: currentIndex == 2,
icon: Icons.bar_chart_rounded,
label: loc.statistics,
onTabTapped: onTabTapped,
@@ -145,12 +136,10 @@ class _CustomNavigationBarState extends State<CustomNavigationBar>
final loc = AppLocalizations.of(context);
switch (currentIndex) {
case 0:
return loc.home;
case 1:
return loc.matches;
case 2:
case 1:
return loc.groups;
case 3:
case 2:
return loc.statistics;
default:
return '';

View File

@@ -1,259 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tallee/core/adaptive_page_route.dart';
import 'package:tallee/core/constants.dart';
import 'package:tallee/core/enums.dart';
import 'package:tallee/data/db/database.dart';
import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_result_view.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/buttons/quick_create_button.dart';
import 'package:tallee/presentation/widgets/tiles/info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/match_tile.dart';
import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart';
class HomeView extends StatefulWidget {
/// The main home view of the application, displaying quick info,
/// recent matches, and quick create options.
const HomeView({super.key});
@override
State<HomeView> createState() => _HomeViewState();
}
class _HomeViewState extends State<HomeView> {
bool isLoading = true;
/// Amount of matches in the database
int matchCount = 0;
/// Amount of groups in the database
int groupCount = 0;
/// Loaded recent matches from the database
List<Match> loadedRecentMatches = [];
/// Recent matches to display, initially filled with skeleton matches
List<Match> recentMatches = List.filled(
2,
Match(
name: 'Skeleton Match',
game: Game(
name: 'Skeleton Game',
ruleset: Ruleset.singleWinner,
description: 'This is a skeleton game description.',
color: GameColor.blue,
icon: '',
),
group: Group(
name: 'Skeleton Group',
description: 'This is a skeleton group description.',
members: [
Player(
name:
'Skeleton Player 1'
'',
),
Player(
name:
'Skeleton Player 2'
'',
),
],
),
notes: 'These are skeleton notes.',
players: [
Player(
name:
'Skeleton Player 1'
'',
),
Player(
name:
'Skeleton Player 2'
'',
),
],
),
);
@override
void initState() {
super.initState();
loadHomeViewData();
}
@override
Widget build(BuildContext context) {
final loc = AppLocalizations.of(context);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return AppSkeleton(
fixLayoutBuilder: true,
enabled: isLoading,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QuickInfoTile(
width: constraints.maxWidth * 0.45,
height: constraints.maxHeight * 0.13,
title: loc.matches,
icon: Icons.groups_rounded,
value: matchCount,
),
SizedBox(width: constraints.maxWidth * 0.05),
QuickInfoTile(
width: constraints.maxWidth * 0.45,
height: constraints.maxHeight * 0.13,
title: loc.groups,
icon: Icons.groups_rounded,
value: groupCount,
),
],
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: InfoTile(
width: constraints.maxWidth * 0.95,
title: loc.recent_matches,
icon: Icons.history_rounded,
content: Column(
children: [
if (recentMatches.isNotEmpty)
for (Match match in recentMatches)
Padding(
padding: const EdgeInsets.symmetric(
vertical: 6.0,
),
child: MatchTile(
compact: true,
width: constraints.maxWidth * 0.9,
match: match,
onTap: () async {
await Navigator.of(context).push(
adaptivePageRoute(
fullscreenDialog: true,
builder: (context) =>
MatchResultView(match: match),
),
);
await loadRecentMatches();
setState(() {
print('loaded');
});
},
),
)
else
Center(
heightFactor: 5,
child: Text(loc.no_recent_matches_available),
),
],
),
),
),
Padding(
padding: EdgeInsets.zero,
child: InfoTile(
width: constraints.maxWidth * 0.95,
title: loc.quick_create,
icon: Icons.add_box_rounded,
content: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
QuickCreateButton(
text: 'Category 1',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 2',
onPressed: () {},
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
QuickCreateButton(
text: 'Category 3',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 4',
onPressed: () {},
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
QuickCreateButton(
text: 'Category 5',
onPressed: () {},
),
QuickCreateButton(
text: 'Category 6',
onPressed: () {},
),
],
),
],
),
),
),
SizedBox(height: MediaQuery.paddingOf(context).bottom),
],
),
),
);
},
);
}
/// Loads the data for the HomeView from the database.
/// This includes the match count, group count, and recent matches.
Future<void> loadHomeViewData() async {
final db = Provider.of<AppDatabase>(context, listen: false);
Future.wait([
db.matchDao.getMatchCount(),
db.groupDao.getGroupCount(),
db.matchDao.getAllMatches(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) {
matchCount = results[0] as int;
groupCount = results[1] as int;
loadedRecentMatches = results[2] as List<Match>;
recentMatches =
(loadedRecentMatches
..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
if (mounted) {
setState(() {
isLoading = false;
});
}
});
}
Future<void> loadRecentMatches() async {
final db = Provider.of<AppDatabase>(context, listen: false);
final matches = await db.matchDao.getAllMatches();
recentMatches =
(matches..sort((a, b) => b.createdAt.compareTo(a.createdAt)))
.take(2)
.toList();
}
}

View File

@@ -10,6 +10,7 @@ import 'package:tallee/data/models/game.dart';
import 'package:tallee/data/models/group.dart';
import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/data/models/score_entry.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/views/main_menu/match_view/create_match/create_match_view.dart';
import 'package:tallee/presentation/views/main_menu/match_view/match_detail_view.dart';
@@ -30,8 +31,7 @@ class _MatchViewState extends State<MatchView> {
late final AppDatabase db;
bool isLoading = true;
/// Loaded matches from the database,
/// initially filled with skeleton matches
/// Loaded matches from the database, initially filled with skeleton matches
List<Match> matches = List.filled(
4,
Match(
@@ -46,7 +46,15 @@ class _MatchViewState extends State<MatchView> {
name: 'Group name',
members: List.filled(5, Player(name: 'Player')),
),
players: [Player(name: 'Player')],
players: [
Player(name: 'Player'),
Player(name: 'Player'),
Player(name: 'Player'),
Player(name: 'Player'),
Player(id: 'mvp_id', name: 'Player'),
],
scores: {'mvp_id': ScoreEntry(score: 1)},
endedAt: DateTime.now(),
),
);

View File

@@ -37676,12 +37676,12 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.''',
);
/// tallee 0.0.25+259
/// tallee 0.0.27+261
const _tallee = Package(
name: 'tallee',
description: 'Tracking App for Card Games',
authors: [],
version: '0.0.25+259',
version: '0.0.27+261',
spdxIdentifiers: ['LGPL-3.0'],
isMarkdown: false,
isSdk: false,

View File

@@ -6,6 +6,7 @@ import 'package:tallee/data/models/match.dart';
import 'package:tallee/data/models/player.dart';
import 'package:tallee/l10n/generated/app_localizations.dart';
import 'package:tallee/presentation/widgets/app_skeleton.dart';
import 'package:tallee/presentation/widgets/tiles/quick_info_tile.dart';
import 'package:tallee/presentation/widgets/tiles/statistics_tile.dart';
import 'package:tallee/presentation/widgets/top_centered_message.dart';
@@ -18,6 +19,9 @@ class StatisticsView extends StatefulWidget {
}
class _StatisticsViewState extends State<StatisticsView> {
int matchCount = 0;
int groupCount = 0;
List<(Player, int)> winCounts = List.filled(6, (
Player(name: 'Skeleton Player'),
1,
@@ -53,7 +57,27 @@ class _StatisticsViewState extends State<StatisticsView> {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(height: constraints.maxHeight * 0.01),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
QuickInfoTile(
width: constraints.maxWidth * 0.45,
height: constraints.maxHeight * 0.13,
title: loc.matches,
icon: Icons.groups_rounded,
value: matchCount,
),
SizedBox(width: constraints.maxWidth * 0.05),
QuickInfoTile(
width: constraints.maxWidth * 0.45,
height: constraints.maxHeight * 0.13,
title: loc.groups,
icon: Icons.groups_rounded,
value: groupCount,
),
],
),
SizedBox(height: constraints.maxHeight * 0.02),
Visibility(
visible:
winCounts.isEmpty &&
@@ -115,11 +139,17 @@ class _StatisticsViewState extends State<StatisticsView> {
Future.wait([
db.matchDao.getAllMatches(),
db.playerDao.getAllPlayers(),
db.matchDao.getMatchCount(),
db.groupDao.getGroupCount(),
Future.delayed(Constants.MINIMUM_SKELETON_DURATION),
]).then((results) async {
if (!mounted) return;
final matches = results[0] as List<Match>;
final players = results[1] as List<Player>;
matchCount = results[2] as int;
groupCount = results[3] as int;
winCounts = _calculateWinsForAllPlayers(
matches: matches,
players: players,
@@ -134,6 +164,7 @@ class _StatisticsViewState extends State<StatisticsView> {
winCounts: winCounts,
matchCounts: matchCounts,
);
setState(() {
isLoading = false;
});

View File

@@ -44,24 +44,49 @@ class _NavbarItemState extends State<NavbarItem>
/// Scale animation for the icon when selected
late Animation<double> _scaleAnimation;
/// Color animation for the icon
late Animation<Color?> _iconColorAnimation;
/// Background color animation for the icon container
late Animation<Color?> _bgColorAnimation;
/// Font size animation for the label
late Animation<double> _fontSizeAnimation;
/// A simple double tween used to lerp between two font weights
late Animation<double> _fontWeightT;
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
// Set initial value directly so the visual state matches widget.isSelected
value: widget.isSelected ? 1.0 : 0.0,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(
CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOutBack,
),
final curved = CurvedAnimation(
parent: _animationController,
curve: Curves.easeOut,
);
if (widget.isSelected) {
_animationController.forward();
}
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(curved);
_iconColorAnimation = ColorTween(
begin: CustomTheme.navBarItemUnselectedColor,
end: CustomTheme.navBarItemSelectedColor,
).animate(curved);
_bgColorAnimation = ColorTween(
begin: Colors.transparent,
end: CustomTheme.primaryColor.withAlpha(50),
).animate(curved);
_fontSizeAnimation = Tween<double>(begin: 11.0, end: 12.0).animate(curved);
// drives font weight interpolation
_fontWeightT = Tween<double>(begin: 0.0, end: 1.0).animate(curved);
}
// Retrigger animation on selection change
@@ -83,46 +108,44 @@ class _NavbarItemState extends State<NavbarItem>
behavior: HitTestBehavior.opaque,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
width: 50,
height: 50,
decoration: BoxDecoration(
color: widget.isSelected
? CustomTheme.primaryColor.withAlpha(50)
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
duration: const Duration(milliseconds: 200),
child: ScaleTransition(
scale: widget.isSelected
? _scaleAnimation
: const AlwaysStoppedAnimation(1.0),
child: Icon(
widget.icon,
color: widget.isSelected
? CustomTheme.navBarItemSelectedColor
: CustomTheme.navBarItemUnselectedColor,
size: 32,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
final iconColor = _iconColorAnimation.value!;
final bgColor = _bgColorAnimation.value!;
final fontSize = _fontSizeAnimation.value;
final fontWeight = FontWeight.lerp(
FontWeight.w500,
FontWeight.bold,
_fontWeightT.value,
);
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: bgColor,
borderRadius: const BorderRadius.all(Radius.circular(15)),
),
child: ScaleTransition(
scale: _scaleAnimation,
child: Icon(widget.icon, color: iconColor, size: 32),
),
),
),
),
Text(
widget.label,
style: TextStyle(
color: widget.isSelected
? CustomTheme.navBarItemSelectedColor
: CustomTheme.navBarItemUnselectedColor,
fontSize: widget.isSelected ? 12 : 11,
fontWeight: widget.isSelected
? FontWeight.bold
: FontWeight.w500,
),
),
],
Text(
widget.label,
style: TextStyle(
color: iconColor,
fontSize: fontSize,
fontWeight: fontWeight,
),
),
],
);
},
),
),
),