From 2bdb094819b442b619a19d3cdc7e3ed6eca98070 Mon Sep 17 00:00:00 2001 From: Thatsaphorn Atchariyaphap Date: Sat, 27 Sep 2025 15:39:32 +0200 Subject: [PATCH] Feature: Introduce `GlassBottomBar` for enhanced navigation with a glass effect and integrate it into `AppShell` for improved UI aesthetics. --- .../app/lib/core/ui/glas_bottom_bar.dart | 129 ++++++++++++++++++ finlog_app/app/lib/modules/app_shell.dart | 31 ++--- 2 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 finlog_app/app/lib/core/ui/glas_bottom_bar.dart diff --git a/finlog_app/app/lib/core/ui/glas_bottom_bar.dart b/finlog_app/app/lib/core/ui/glas_bottom_bar.dart new file mode 100644 index 0000000..61f15ce --- /dev/null +++ b/finlog_app/app/lib/core/ui/glas_bottom_bar.dart @@ -0,0 +1,129 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class GlassBottomBarItem { + final IconData icon; + final String label; + final String route; + + const GlassBottomBarItem({ + required this.icon, + required this.label, + required this.route, + }); +} + +class GlassBottomBar extends StatelessWidget { + const GlassBottomBar({ + super.key, + required this.items, + required this.currentPath, + required this.onSelect, + }); + + final List items; + final String currentPath; + final void Function(String route) onSelect; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + bool isSelected(String route) => currentPath.startsWith(route); + + return SafeArea( + minimum: const EdgeInsets.only(left: 12, right: 12, bottom: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: cs.surface.withOpacity(0.65), // glass tint + borderRadius: BorderRadius.circular(20), + border: Border.all(color: cs.outline.withOpacity(0.18)), + boxShadow: [ + BoxShadow( + color: cs.shadow.withOpacity(0.08), + blurRadius: 18, + offset: const Offset(0, 6), + ), + ], + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (final it in items) + _GlassItem( + icon: it.icon, + label: it.label, + selected: isSelected(it.route), + onTap: () => onSelect(it.route), + ), + ], + ), + ), + ), + ), + ); + } +} + +class _GlassItem extends StatelessWidget { + const _GlassItem({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + }); + + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOut, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: selected + ? cs.primaryContainer.withOpacity(0.35) + : Colors.transparent, + borderRadius: BorderRadius.circular(14), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 22, + color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant, + ), + const SizedBox(height: 4), + Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/finlog_app/app/lib/modules/app_shell.dart b/finlog_app/app/lib/modules/app_shell.dart index ccd5e66..6c3a7b9 100644 --- a/finlog_app/app/lib/modules/app_shell.dart +++ b/finlog_app/app/lib/modules/app_shell.dart @@ -1,6 +1,7 @@ import 'package:app/core/app/router.dart'; import 'package:app/core/features/feature_controller.dart'; import 'package:app/core/i18n/translations.g.dart'; +import 'package:app/core/ui/glas_bottom_bar.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -160,23 +161,23 @@ class _AppShellState extends State { return Scaffold( appBar: appBar, + // Let content flow under the floating bar for the glass effect + extendBody: true, body: SafeArea(child: widget.child), bottomNavigationBar: AnimatedBuilder( animation: fc, builder: (context, _) { final baseTabs = _getMobileTabs(context); - // Erlaubte Tabs filtern + // Filter allowed tabs (your existing guard) var tabs = <({IconData icon, String label, String route})>[ for (final it in baseTabs) if (_routeEnabled(it.route, fc)) it, ]; - // Fallback: min. 2 Ziele sicherstellen + // Fallback to ensure min. 2 items if (tabs.length < 2) { - // Home ist immer erlaubt; „Einstellungen“ als zweites Ziel ergänzen tabs = [ - // Stelle sicher, dass Home als erstes drin ist ( icon: Icons.dashboard, label: Translations.of(context).app.navigationDashboard, @@ -190,22 +191,18 @@ class _AppShellState extends State { ]; } - // aktuellen Index robust bestimmen final currentPath = GoRouterState.of(context).matchedLocation; - int selected = 0; - for (var i = 0; i < tabs.length; i++) { - if (currentPath.startsWith(tabs[i].route)) { - selected = i; - break; - } - } - return NavigationBar( - selectedIndex: selected, - onDestinationSelected: (i) => context.go(tabs[i].route), - destinations: [ + return GlassBottomBar( + currentPath: currentPath, + onSelect: (route) => context.go(route), + items: [ for (final it in tabs) - NavigationDestination(icon: Icon(it.icon), label: it.label), + GlassBottomBarItem( + icon: it.icon, + label: it.label, + route: it.route, + ), ], ); },