Feature: Refactor _DesktopDrawer to support expandable/collapsible states, introduce glass effect panel design, and refine navigation UI.
This commit is contained in:
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user