Feature: Add feature toggles and settings for modular features (e.g., Car, Inventory), enhance navigation for mobile/desktop, and improve i18n integration.
This commit is contained in:
@@ -12,8 +12,7 @@ class AppShell extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppShellState extends State<AppShell> {
|
||||
static const double _railBreakpoint = 800; // tablet and up
|
||||
bool _railExtended = true; // start "open" on tablet/desktop
|
||||
static const double _breakpoint = 800; // Tablet/Desktop threshold
|
||||
final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus');
|
||||
|
||||
@override
|
||||
@@ -24,14 +23,16 @@ class _AppShellState extends State<AppShell> {
|
||||
|
||||
// --- NAV ITEMS -------------------------------------------------------------
|
||||
|
||||
List<({IconData icon, IconData? selectedIcon, String label, String route})> _getItems(BuildContext context) {
|
||||
// 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: '/home',
|
||||
route: '/',
|
||||
),
|
||||
(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
@@ -52,41 +53,62 @@ class _AppShellState extends State<AppShell> {
|
||||
route: '/reports',
|
||||
),
|
||||
(
|
||||
icon: Icons.settings,
|
||||
selectedIcon: Icons.settings_outlined,
|
||||
icon: Icons.settings_outlined,
|
||||
selectedIcon: Icons.settings,
|
||||
label: t.app.navigationSettings,
|
||||
route: '/settings',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
int _indexForPath(String p, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) {
|
||||
// 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 (p.startsWith(items[i].route)) return i;
|
||||
if (path.startsWith(routeOf(items[i]))) return i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void _goForIndex(BuildContext ctx, int i, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) => ctx.go(items[i].route);
|
||||
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 items = _getItems(context);
|
||||
final width = MediaQuery.of(context).size.width;
|
||||
final isRail = width >= _railBreakpoint;
|
||||
final isDesktop = width >= _breakpoint;
|
||||
final currentPath = GoRouterState.of(context).matchedLocation;
|
||||
|
||||
// keep focus on right/content pane on wide layouts
|
||||
if (isRail) {
|
||||
if (isDesktop) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted && !_contentFocus.hasFocus) _contentFocus.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
final appBar = AppBar(
|
||||
title: _LogoHeader(),
|
||||
leading: isRail
|
||||
title: const _LogoHeader(),
|
||||
// On desktop we use a persistent drawer, so no burger button.
|
||||
leading: isDesktop
|
||||
? null
|
||||
: Builder(
|
||||
builder: (ctx) => IconButton(
|
||||
@@ -109,79 +131,42 @@ class _AppShellState extends State<AppShell> {
|
||||
],
|
||||
);
|
||||
|
||||
if (!isRail) {
|
||||
// ------------------- MOBILE: Drawer -------------------
|
||||
final selectedIndex = _indexForPath(currentPath, items);
|
||||
if (!isDesktop) {
|
||||
// ------------------- MOBILE: Bottom Navigation -------------------
|
||||
final tabs = _getMobileTabs(context);
|
||||
final selected = _indexForPath(currentPath, tabs, (it) => it.route);
|
||||
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
drawer: _AppDrawer(items: items, selectedIndex: selectedIndex),
|
||||
// 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: NavigationRail -------------------
|
||||
final selected = _indexForPath(currentPath, items);
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
// ------------------- TABLET/DESKTOP: Persistent Drawer -------------------
|
||||
final items = _getDesktopItems(context);
|
||||
final selected = _indexForPath(currentPath, items, (it) => it.route);
|
||||
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
body: Row(
|
||||
children: [
|
||||
NavigationRailTheme(
|
||||
data: NavigationRailThemeData(
|
||||
groupAlignment: -1.0,
|
||||
// align to top
|
||||
useIndicator: true,
|
||||
indicatorColor: scheme.secondaryContainer,
|
||||
// background for selected "button"
|
||||
indicatorShape: const StadiumBorder(),
|
||||
selectedIconTheme: IconThemeData(
|
||||
color: scheme.onSecondaryContainer,
|
||||
),
|
||||
selectedLabelTextStyle: TextStyle(
|
||||
color: scheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
child: NavigationRail(
|
||||
extended: _railExtended,
|
||||
selectedIndex: selected,
|
||||
onDestinationSelected: (i) => _goForIndex(context, i, items),
|
||||
// leading: const Padding(
|
||||
// padding: EdgeInsets.only(top: 8),
|
||||
// child: _LogoHeader(),
|
||||
// ),
|
||||
destinations: [
|
||||
for (final it in items)
|
||||
NavigationRailDestination(
|
||||
icon: Icon(it.icon),
|
||||
selectedIcon: Icon(it.selectedIcon ?? it.icon),
|
||||
label: Text(it.label),
|
||||
),
|
||||
],
|
||||
trailingAtBottom: true,
|
||||
trailing: Column(
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
const Divider(height: 1),
|
||||
// toggle lives at the very bottom so layout doesn't jump
|
||||
IconButton(
|
||||
tooltip: _railExtended
|
||||
? t.app.tooltipCollapseRail
|
||||
: t.app.tooltipExpandRail,
|
||||
onPressed: () =>
|
||||
setState(() => _railExtended = !_railExtended),
|
||||
icon: Icon(
|
||||
_railExtended
|
||||
? Icons.keyboard_double_arrow_left
|
||||
: Icons.keyboard_double_arrow_right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Persistent drawer area
|
||||
SizedBox(
|
||||
width: 300,
|
||||
child: _DesktopDrawer(items: items, selectedIndex: selected),
|
||||
),
|
||||
const VerticalDivider(width: 1),
|
||||
// Content
|
||||
Expanded(
|
||||
child: SafeArea(
|
||||
child: Focus(
|
||||
@@ -197,6 +182,49 @@ class _AppShellState extends State<AppShell> {
|
||||
}
|
||||
}
|
||||
|
||||
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})
|
||||
|
||||
Reference in New Issue
Block a user