diff --git a/lib/core/custom_theme.dart b/lib/core/custom_theme.dart index a6c6376..cfe9b5a 100644 --- a/lib/core/custom_theme.dart +++ b/lib/core/custom_theme.dart @@ -11,6 +11,8 @@ class CustomTheme { static Color onBoxColor = const Color(0xFF181818); static Color boxBorder = const Color(0xFF272727); static const Color textColor = Colors.white; + static Color navBarItemSelectedColor = primaryColor.withGreen(100); + static Color navBarItemUnselectedColor = Colors.grey.shade400; // ==================== Border Radius ==================== static const double standardBorderRadius = 12.0; diff --git a/lib/presentation/views/main_menu/custom_navigation_bar.dart b/lib/presentation/views/main_menu/custom_navigation_bar.dart index f6eb381..3d1fa96 100644 --- a/lib/presentation/views/main_menu/custom_navigation_bar.dart +++ b/lib/presentation/views/main_menu/custom_navigation_bar.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/custom_theme.dart'; @@ -71,53 +73,102 @@ class _CustomNavigationBarState extends State backgroundColor: CustomTheme.backgroundColor, body: tabs[currentIndex], extendBody: true, - bottomNavigationBar: SafeArea( - minimum: const EdgeInsets.only(bottom: 30), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 10), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(24), - color: CustomTheme.primaryColor, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(24), - child: SizedBox( - height: 60, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - NavbarItem( - index: 0, - isSelected: currentIndex == 0, - icon: Icons.home_rounded, - label: loc.home, - onTabTapped: onTabTapped, + bottomNavigationBar: SizedBox( + height: 70 + MediaQuery.of(context).padding.bottom, + child: Stack( + children: [ + // Dynamically generated blur layers for ultra-smooth transition + ...List.generate(34, (index) { + // Use cubic curve for an even more natural, smoother transition + final progress = index / 34.0; // 0.0 to 1.0 + final cubic = progress * progress * progress; // cubic curve + final blurStrength = + 0.5 + (cubic * 50.0); // Very smooth from 0.5 to 50.5 + + // Height goes completely from 100% to 0% (all the way down) + // With extra density at the bottom for softer transition + final heightFactor = index < 25 + // First 25 layers: 100% to 30% + ? 1.0 - (progress * 0.7) + // Last 10 layers: 30% to 0% (denser) + : 0.3 - ((index - 25) / 34.0); + + return Positioned( + left: 0, + right: 0, + bottom: 0, + height: + (70 + MediaQuery.of(context).padding.bottom) * + heightFactor.clamp(0.05, 1.0), + child: ClipRect( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: blurStrength, + sigmaY: blurStrength, + ), + child: Container(color: Colors.transparent), ), - NavbarItem( - index: 1, - isSelected: currentIndex == 1, - icon: Icons.gamepad_rounded, - label: loc.matches, - onTabTapped: onTabTapped, + ), + ); + }), + // Gradient overlay + Positioned.fill( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + CustomTheme.boxColor.withValues(alpha: 1), + CustomTheme.boxColor.withValues(alpha: 0.5), + CustomTheme.boxColor.withValues(alpha: 0.2), + CustomTheme.boxColor.withValues(alpha: 0.0), + ], + stops: const [0.0, 0.4, 0.8, 1], ), - NavbarItem( - index: 2, - isSelected: currentIndex == 2, - icon: Icons.group_rounded, - label: loc.groups, - onTabTapped: onTabTapped, - ), - NavbarItem( - index: 3, - isSelected: currentIndex == 3, - icon: Icons.bar_chart_rounded, - label: loc.statistics, - onTabTapped: onTabTapped, - ), - ], + ), ), ), - ), + // Navbar content + SafeArea( + child: SizedBox( + height: 70, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + 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, + icon: Icons.group_rounded, + label: loc.groups, + onTabTapped: onTabTapped, + ), + NavbarItem( + index: 3, + isSelected: currentIndex == 3, + icon: Icons.bar_chart_rounded, + label: loc.statistics, + onTabTapped: onTabTapped, + ), + ], + ), + ), + ), + ], ), ), ); diff --git a/lib/presentation/views/main_menu/group_view/groups_view.dart b/lib/presentation/views/main_menu/group_view/groups_view.dart index 239aa23..ec5fc44 100644 --- a/lib/presentation/views/main_menu/group_view/groups_view.dart +++ b/lib/presentation/views/main_menu/group_view/groups_view.dart @@ -8,7 +8,7 @@ import 'package:game_tracker/data/dto/player.dart'; import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/group_view/create_group_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/group_tile.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; @@ -79,10 +79,10 @@ class _GroupsViewState extends State { ), ), Positioned( - bottom: MediaQuery.paddingOf(context).bottom, - child: CustomWidthButton( + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: MainMenuButton( text: loc.create_group, - sizeRelativeToWidth: 0.90, + icon: Icons.group_add, onPressed: () async { await Navigator.push( context, diff --git a/lib/presentation/views/main_menu/match_view/match_view.dart b/lib/presentation/views/main_menu/match_view/match_view.dart index ecfa9ca..7705d18 100644 --- a/lib/presentation/views/main_menu/match_view/match_view.dart +++ b/lib/presentation/views/main_menu/match_view/match_view.dart @@ -1,6 +1,7 @@ import 'dart:core' hide Match; import 'package:flutter/material.dart'; +import 'package:fluttericon/rpg_awesome_icons.dart'; import 'package:game_tracker/core/adaptive_page_route.dart'; import 'package:game_tracker/core/constants.dart'; import 'package:game_tracker/core/custom_theme.dart'; @@ -12,7 +13,7 @@ import 'package:game_tracker/l10n/generated/app_localizations.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/create_match/create_match_view.dart'; import 'package:game_tracker/presentation/views/main_menu/match_view/match_result_view.dart'; import 'package:game_tracker/presentation/widgets/app_skeleton.dart'; -import 'package:game_tracker/presentation/widgets/buttons/custom_width_button.dart'; +import 'package:game_tracker/presentation/widgets/buttons/main_menu_button.dart'; import 'package:game_tracker/presentation/widgets/tiles/match_tile.dart'; import 'package:game_tracker/presentation/widgets/top_centered_message.dart'; import 'package:provider/provider.dart'; @@ -104,10 +105,10 @@ class _MatchViewState extends State { ), ), Positioned( - bottom: MediaQuery.paddingOf(context).bottom, - child: CustomWidthButton( - text: loc.create_match, - sizeRelativeToWidth: 0.90, + bottom: MediaQuery.paddingOf(context).bottom + 20, + child: MainMenuButton( + text: 'Spiel erstellen', + icon: RpgAwesome.clovers_card, onPressed: () async { Navigator.push( context, diff --git a/lib/presentation/widgets/buttons/main_menu_button.dart b/lib/presentation/widgets/buttons/main_menu_button.dart new file mode 100644 index 0000000..d29566c --- /dev/null +++ b/lib/presentation/widgets/buttons/main_menu_button.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; + +/// A button for the main menu with an optional icon and a press animation. +/// - [text]: The text of the button. +/// - [icon]: The icon of the button. +/// - [onPressed]: The callback to be invoked when the button is pressed. +class MainMenuButton extends StatefulWidget { + const MainMenuButton({ + super.key, + required this.text, + this.icon, + required this.onPressed, + }); + + /// The text of the button. + final String text; + + /// The icon of the button. + final IconData? icon; + + /// The callback to be invoked when the button is pressed. + final void Function() onPressed; + + @override + State createState() => _MainMenuButtonState(); +} + +class _MainMenuButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + + _animationController = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + Widget build(BuildContext context) { + return ScaleTransition( + scale: _scaleAnimation, + child: GestureDetector( + onTapDown: (_) { + _animationController.forward(); + }, + onTapUp: (_) async { + await _animationController.reverse(); + if (mounted) { + widget.onPressed(); + } + }, + onTapCancel: () { + _animationController.reverse(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(30), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.icon != null) ...[ + Icon(widget.icon, size: 26, color: Colors.black), + const SizedBox(width: 7), + ], + Text( + widget.text, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } +} diff --git a/lib/presentation/widgets/navbar_item.dart b/lib/presentation/widgets/navbar_item.dart index 13a8d4d..abf7acc 100644 --- a/lib/presentation/widgets/navbar_item.dart +++ b/lib/presentation/widgets/navbar_item.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:game_tracker/core/custom_theme.dart'; /// A navigation bar item widget that represents a single tab in a navigation bar. /// - [index]: The index of the tab. @@ -35,7 +36,45 @@ class NavbarItem extends StatefulWidget { State createState() => _NavbarItemState(); } -class _NavbarItemState extends State { +class _NavbarItemState extends State + with SingleTickerProviderStateMixin { + /// Animation controller for the scale animation + late AnimationController _animationController; + + /// Scale animation for the icon when selected + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + + _scaleAnimation = Tween(begin: 1.0, end: 1.2).animate( + CurvedAnimation( + parent: _animationController, + curve: Curves.easeInOutBack, + ), + ); + + if (widget.isSelected) { + _animationController.forward(); + } + } + + // Retrigger animation on selection change + @override + void didUpdateWidget(NavbarItem oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isSelected && !oldWidget.isSelected) { + _animationController.forward(); + } else if (!widget.isSelected && oldWidget.isSelected) { + _animationController.reverse(); + } + } + @override Widget build(BuildContext context) { return Expanded( @@ -48,19 +87,29 @@ class _NavbarItemState extends State { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - widget.icon, - color: widget.isSelected ? Colors.white : Colors.black, + ScaleTransition( + scale: widget.isSelected + ? _scaleAnimation + : const AlwaysStoppedAnimation(1.0), + child: Icon( + widget.icon, + color: widget.isSelected + ? CustomTheme.navBarItemSelectedColor + : CustomTheme.navBarItemUnselectedColor, + size: 32, + ), ), const SizedBox(height: 4), Text( widget.label, style: TextStyle( - color: widget.isSelected ? Colors.white : Colors.black, - fontSize: 12, + color: widget.isSelected + ? CustomTheme.navBarItemSelectedColor + : CustomTheme.navBarItemUnselectedColor, + fontSize: widget.isSelected ? 12 : 11, fontWeight: widget.isSelected ? FontWeight.bold - : FontWeight.normal, + : FontWeight.w500, ), ), ], @@ -69,4 +118,10 @@ class _NavbarItemState extends State { ), ); } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 9bc63cb..83d5079 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: game_tracker description: "Game Tracking App for Card Games" publish_to: 'none' -version: 0.0.5+144 +version: 0.0.6+209 environment: sdk: ^3.8.1 @@ -17,6 +17,7 @@ dependencies: drift_flutter: ^0.2.4 file_picker: ^10.3.6 file_saver: ^0.3.1 + fluttericon: ^2.0.0 font_awesome_flutter: ^10.12.0 intl: any json_schema: ^5.2.2 @@ -27,7 +28,6 @@ dependencies: url_launcher: ^6.3.2 uuid: ^4.5.2 - dev_dependencies: flutter_test: sdk: flutter