298 lines
8.6 KiB
Dart
298 lines
8.6 KiB
Dart
import 'package:app/core/i18n/translations.g.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
|
|
class AppShell extends StatefulWidget {
|
|
final Widget child;
|
|
|
|
const AppShell({super.key, required this.child});
|
|
|
|
@override
|
|
State<AppShell> createState() => _AppShellState();
|
|
}
|
|
|
|
class _AppShellState extends State<AppShell> {
|
|
static const double _breakpoint = 800; // Tablet/Desktop threshold
|
|
final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus');
|
|
|
|
@override
|
|
void dispose() {
|
|
_contentFocus.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
// --- NAV ITEMS -------------------------------------------------------------
|
|
|
|
// Desktop/Tablet drawer items (you can keep them rich/longer here)
|
|
List<({IconData icon, IconData? selectedIcon, String label, String route})>
|
|
_getDesktopItems(BuildContext context) {
|
|
final t = Translations.of(context);
|
|
return [
|
|
(
|
|
icon: Icons.dashboard_outlined,
|
|
selectedIcon: Icons.dashboard,
|
|
label: t.app.navigationDashboard,
|
|
route: '/',
|
|
),
|
|
(
|
|
icon: Icons.account_balance_wallet_outlined,
|
|
selectedIcon: Icons.account_balance_wallet,
|
|
label: t.app.navigationBudgets,
|
|
route: '/budget',
|
|
),
|
|
(
|
|
icon: Icons.inventory_2_outlined,
|
|
selectedIcon: Icons.inventory_2,
|
|
label: t.app.navigationInventory,
|
|
route: '/inventory',
|
|
),
|
|
(
|
|
icon: Icons.bar_chart_outlined,
|
|
selectedIcon: Icons.bar_chart,
|
|
label: t.app.navigationReports,
|
|
route: '/reports',
|
|
),
|
|
(
|
|
icon: Icons.settings_outlined,
|
|
selectedIcon: Icons.settings,
|
|
label: t.app.navigationSettings,
|
|
route: '/settings',
|
|
),
|
|
];
|
|
}
|
|
|
|
// Mobile bottom bar items (exactly the four you asked for)
|
|
List<({IconData icon, String label, String route})> _getMobileTabs(
|
|
BuildContext context,
|
|
) {
|
|
final t = Translations.of(context);
|
|
return [
|
|
(icon: Icons.dashboard, label: t.app.navigationDashboard, route: '/home'),
|
|
// “Haushalt (inkl. Budget)” → map to /budget for now
|
|
(icon: Icons.home, label: 'Haushalt', route: '/budget'),
|
|
(icon: Icons.inventory_2, label: 'Inventar', route: '/inventory'),
|
|
(icon: Icons.directions_car, label: 'Auto', route: '/car'),
|
|
];
|
|
}
|
|
|
|
int _indexForPath<T>(String path, List<T> items, String Function(T) routeOf) {
|
|
for (var i = 0; i < items.length; i++) {
|
|
if (path.startsWith(routeOf(items[i]))) return i;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
void _goForIndex<T>(
|
|
BuildContext ctx,
|
|
int i,
|
|
List<T> items,
|
|
String Function(T) routeOf,
|
|
) {
|
|
ctx.go(routeOf(items[i]));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final t = Translations.of(context);
|
|
final width = MediaQuery.of(context).size.width;
|
|
final isDesktop = width >= _breakpoint;
|
|
final currentPath = GoRouterState.of(context).matchedLocation;
|
|
|
|
// keep focus on right/content pane on wide layouts
|
|
if (isDesktop) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted && !_contentFocus.hasFocus) _contentFocus.requestFocus();
|
|
});
|
|
}
|
|
|
|
final appBar = AppBar(
|
|
title: const _LogoHeader(),
|
|
// On desktop we use a persistent drawer, so no burger button.
|
|
leading: isDesktop
|
|
? null
|
|
: Builder(
|
|
builder: (ctx) => IconButton(
|
|
icon: const Icon(Icons.menu),
|
|
onPressed: () => Scaffold.of(ctx).openDrawer(),
|
|
tooltip: t.app.tooltipMenu,
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
tooltip: t.app.tooltipNotifications,
|
|
onPressed: () {},
|
|
icon: const Icon(Icons.notifications_none),
|
|
),
|
|
IconButton(
|
|
tooltip: t.app.tooltipUserSettings,
|
|
onPressed: () => context.push('/settings'),
|
|
icon: const Icon(Icons.account_circle_outlined),
|
|
),
|
|
],
|
|
);
|
|
|
|
if (!isDesktop) {
|
|
// ------------------- MOBILE: Bottom Navigation -------------------
|
|
final tabs = _getMobileTabs(context);
|
|
final selected = _indexForPath(currentPath, tabs, (it) => it.route);
|
|
|
|
return Scaffold(
|
|
appBar: appBar,
|
|
// Keep drawer for mobile? You asked for bottom bar instead — remove drawer.
|
|
body: SafeArea(child: widget.child),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: selected,
|
|
onDestinationSelected: (i) =>
|
|
_goForIndex(context, i, tabs, (it) => it.route),
|
|
destinations: [
|
|
for (final it in tabs)
|
|
NavigationDestination(icon: Icon(it.icon), label: it.label),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
// ------------------- TABLET/DESKTOP: Persistent Drawer -------------------
|
|
final items = _getDesktopItems(context);
|
|
final selected = _indexForPath(currentPath, items, (it) => it.route);
|
|
|
|
return Scaffold(
|
|
appBar: appBar,
|
|
body: Row(
|
|
children: [
|
|
// Persistent drawer area
|
|
SizedBox(
|
|
width: 300,
|
|
child: _DesktopDrawer(items: items, selectedIndex: selected),
|
|
),
|
|
const VerticalDivider(width: 1),
|
|
// Content
|
|
Expanded(
|
|
child: SafeArea(
|
|
child: Focus(
|
|
focusNode: _contentFocus,
|
|
autofocus: true,
|
|
child: widget.child,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DesktopDrawer extends StatelessWidget {
|
|
final List<
|
|
({IconData icon, IconData? selectedIcon, String label, String route})
|
|
>
|
|
items;
|
|
final int selectedIndex;
|
|
|
|
const _DesktopDrawer({required this.items, required this.selectedIndex});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
|
|
return Material(
|
|
elevation: 0,
|
|
child: SafeArea(
|
|
child: ListTileTheme(
|
|
selectedColor: scheme.onSecondaryContainer,
|
|
selectedTileColor: scheme.secondaryContainer,
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
children: [
|
|
const _LogoHeader(),
|
|
const Divider(),
|
|
for (var i = 0; i < items.length; i++)
|
|
ListTile(
|
|
selected: i == selectedIndex,
|
|
leading: Icon(
|
|
i == selectedIndex
|
|
? (items[i].selectedIcon ?? items[i].icon)
|
|
: items[i].icon,
|
|
),
|
|
title: Text(items[i].label),
|
|
onTap: () => context.go(items[i].route),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AppDrawer extends StatelessWidget {
|
|
final List<
|
|
({IconData icon, IconData? selectedIcon, String label, String route})
|
|
>
|
|
items;
|
|
final int selectedIndex;
|
|
|
|
const _AppDrawer({required this.items, required this.selectedIndex});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final scheme = Theme.of(context).colorScheme;
|
|
|
|
return Drawer(
|
|
child: SafeArea(
|
|
child: ListTileTheme(
|
|
selectedColor: scheme.onSecondaryContainer,
|
|
selectedTileColor: scheme.secondaryContainer,
|
|
child: ListView(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
children: [
|
|
const _LogoHeader(),
|
|
const Divider(),
|
|
for (var i = 0; i < items.length; i++)
|
|
ListTile(
|
|
selected: i == selectedIndex,
|
|
leading: Icon(
|
|
i == selectedIndex
|
|
? (items[i].selectedIcon ?? items[i].icon)
|
|
: items[i].icon,
|
|
),
|
|
title: Text(items[i].label),
|
|
onTap: () {
|
|
Navigator.of(context).maybePop();
|
|
context.go(items[i].route);
|
|
},
|
|
),
|
|
const Divider(),
|
|
ListTile(
|
|
leading: const Icon(Icons.settings_outlined),
|
|
title: Text(Translations.of(context).app.drawerSettings),
|
|
onTap: () {
|
|
Navigator.of(context).maybePop();
|
|
context.go('/settings');
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LogoHeader extends StatelessWidget {
|
|
const _LogoHeader();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return const Padding(
|
|
padding: EdgeInsets.all(16),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.account_balance_wallet, size: 28),
|
|
SizedBox(width: 8),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|