Feature: Refactor _DesktopDrawer to support expandable/collapsible states, introduce glass effect panel design, and refine navigation UI.

This commit is contained in:
2025-09-28 13:14:47 +02:00
parent 6532add0d6
commit e8132cee01

View File

@@ -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<AppShell> {
}
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<AppShell> {
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<AppShell> {
}
}
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<FeatureController>();
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,
),
),
),
);
}
}