diff --git a/finlog_app/app/lib/core/app_shell.dart b/finlog_app/app/lib/core/app_shell.dart index 8a4d5a9..048519f 100644 --- a/finlog_app/app/lib/core/app_shell.dart +++ b/finlog_app/app/lib/core/app_shell.dart @@ -1,3 +1,5 @@ +import 'dart:ui' show ImageFilter; + import 'package:app/core/app/router.dart'; import 'package:app/core/app/features/feature_controller.dart'; import 'package:app/core/i18n/translations.g.dart'; @@ -130,8 +132,6 @@ class _AppShellState extends State { } final appBar = AppBar( - // title: const _LogoHeader(), - // Kein Burger-Button, da mobil ohne Drawer (BottomNav) und Desktop mit persistentem Drawer leading: null, actions: [ IconButton( @@ -218,11 +218,7 @@ class _AppShellState extends State { appBar: appBar, body: Row( children: [ - // Persistent drawer area - SizedBox( - width: 300, - child: _DesktopDrawer(items: items, selectedIndex: selected), - ), + _DesktopDrawer(items: items, selectedIndex: selected), const VerticalDivider(width: 1), // Content Expanded( @@ -240,7 +236,7 @@ class _AppShellState extends State { } } -class _DesktopDrawer extends StatelessWidget { +class _DesktopDrawer extends StatefulWidget { final List< ({IconData icon, IconData? selectedIcon, String label, String route}) > @@ -249,20 +245,30 @@ class _DesktopDrawer extends StatelessWidget { const _DesktopDrawer({required this.items, required this.selectedIndex}); + @override + State<_DesktopDrawer> createState() => _DesktopDrawerState(); +} + +class _DesktopDrawerState extends State<_DesktopDrawer> { + bool _expanded = true; // start expanded + + // You can tweak this threshold if needed. + static const double _collapseWidth = 88.0; // collapsed width + static const double _expandWidth = 280.0; // expanded width + static const double _switchAtWidth = 160.0; // below this, use icon-only mode + @override Widget build(BuildContext context) { - final scheme = Theme.of(context).colorScheme; + final cs = Theme.of(context).colorScheme; final fc = context.read(); bool routeEnabled(String route) { if (route.startsWith(AppRoute.home.path)) return true; if (route.startsWith(AppRoute.settings.path)) return true; - if (route.startsWith(AppRoute.budget.path)) return fc.hasHousehold; if (route.startsWith(AppRoute.inventory.path)) return fc.hasInventory; if (route.startsWith(AppRoute.car.path)) return fc.hasCar; if (route.startsWith(AppRoute.reports.path)) return fc.hasReports; - return true; } @@ -270,7 +276,7 @@ class _DesktopDrawer extends StatelessWidget { animation: fc, builder: (context, _) { final visibleItems = [ - for (final it in items) + for (final it in widget.items) if (routeEnabled(it.route)) it, ]; @@ -283,37 +289,96 @@ class _DesktopDrawer extends StatelessWidget { } } - return Material( - elevation: 0, - child: SafeArea( - child: ListTileTheme( - selectedColor: scheme.onSecondaryContainer, - selectedTileColor: scheme.secondaryContainer, - child: ListView( - padding: const EdgeInsets.symmetric(vertical: 8), - children: [ - // const Divider(), - for (var i = 0; i < visibleItems.length; i++) - ListTile( - selected: i == safeSelected, - leading: Icon( - i == safeSelected - ? (visibleItems[i].selectedIcon ?? - visibleItems[i].icon) - : visibleItems[i].icon, - ), - title: Text(visibleItems[i].label), - onTap: () => context.go(visibleItems[i].route), + final double targetWidth = _expanded ? _expandWidth : _collapseWidth; + + // Use the same tint as mobile glass bar: cs.surface.withOpacity(0.65) + final Color panelColor = cs.surface.withOpacity(0.65); + + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(12), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 14, sigmaY: 14), + child: AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + width: targetWidth, + decoration: BoxDecoration( + color: panelColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: cs.outlineVariant.withOpacity(0.40), ), - // Optional: Settings als fixer Eintrag, - // falls du ihn unabhängig von items immer unten haben willst: - // const Divider(), - // ListTile( - // leading: const Icon(Icons.settings_outlined), - // title: Text(t.app.navigationSettings), - // onTap: () => context.go(AppRoute.settings.path), - // ), - ], + boxShadow: [ + BoxShadow( + color: cs.shadow.withOpacity(0.04), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: LayoutBuilder( + builder: (context, constraints) { + final bool narrow = constraints.maxWidth < _switchAtWidth; + + return Column( + children: [ + // --------- CONTENT ---------- + Expanded( + child: ListView( + padding: const EdgeInsets.symmetric(vertical: 8), + children: [ + for (var i = 0; i < visibleItems.length; i++) + narrow + ? _CollapsedIconItem( + icon: + visibleItems[i].selectedIcon ?? + visibleItems[i].icon, + tooltip: visibleItems[i].label, + selected: i == safeSelected, + onTap: () => + context.go(visibleItems[i].route), + ) + : _ExpandedDrawerItem( + icon: visibleItems[i].icon, + selectedIcon: + visibleItems[i].selectedIcon ?? + visibleItems[i].icon, + label: visibleItems[i].label, + selected: i == safeSelected, + onTap: () => + context.go(visibleItems[i].route), + ), + ], + ), + ), + + const Divider(height: 1), + + // Expand/Collapse button (same behavior as before) + IconButton( + tooltip: _expanded + ? Translations.of( + context, + ).app.tooltipCollapseRail + : Translations.of( + context, + ).app.tooltipExpandRail, + onPressed: () => + setState(() => _expanded = !_expanded), + icon: Icon( + _expanded + ? Icons.keyboard_double_arrow_left + : Icons.keyboard_double_arrow_right, + ), + ), + ], + ); + }, + ), + ), ), ), ), @@ -322,3 +387,103 @@ class _DesktopDrawer extends StatelessWidget { ); } } + +class _ExpandedDrawerItem extends StatelessWidget { + const _ExpandedDrawerItem({ + required this.icon, + required this.selectedIcon, + required this.label, + required this.selected, + required this.onTap, + }); + + final IconData icon; + final IconData selectedIcon; + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: selected + ? cs.primaryContainer.withOpacity(0.35) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon( + selected ? selectedIcon : icon, + size: 22, + color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + label, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} + +class _CollapsedIconItem extends StatelessWidget { + const _CollapsedIconItem({ + required this.icon, + required this.tooltip, + required this.selected, + required this.onTap, + }); + + final IconData icon; + final String tooltip; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + return Tooltip( + message: tooltip, + waitDuration: const Duration(milliseconds: 400), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric(vertical: 10), + decoration: BoxDecoration( + color: selected + ? cs.primaryContainer.withOpacity(0.35) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + ), + alignment: Alignment.center, + child: Icon( + icon, + size: 22, + color: selected ? cs.onPrimaryContainer : cs.onSurfaceVariant, + ), + ), + ), + ); + } +}