Cleanup
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import 'package:app/modules/app_shell.dart';
|
||||
import 'package:app/core/app_shell.dart';
|
||||
import 'package:app/modules/budget/budget_view.dart';
|
||||
import 'package:app/modules/car/car_view.dart';
|
||||
import 'package:app/modules/dashboard/dashboard_view.dart';
|
||||
|
||||
324
finlog_app/app/lib/core/app_shell.dart
Normal file
324
finlog_app/app/lib/core/app_shell.dart
Normal file
@@ -0,0 +1,324 @@
|
||||
import 'package:app/core/app/router.dart';
|
||||
import 'package:app/core/app/features/feature_controller.dart';
|
||||
import 'package:app/core/i18n/translations.g.dart';
|
||||
import 'package:app/core/ui/glas_bottom_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:provider/provider.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;
|
||||
final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus');
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_contentFocus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// --- NAV ITEMS -------------------------------------------------------------
|
||||
|
||||
// Desktop/Tablet drawer items
|
||||
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: AppRoute.home.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.account_balance_wallet_outlined,
|
||||
selectedIcon: Icons.account_balance_wallet,
|
||||
label: t.app.navigationBudgets, // Haushaltsbereich inkl. Budget
|
||||
route: AppRoute.budget.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.inventory_2_outlined,
|
||||
selectedIcon: Icons.inventory_2,
|
||||
label: t.app.navigationInventory,
|
||||
route: AppRoute.inventory.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.bar_chart_outlined,
|
||||
selectedIcon: Icons.bar_chart,
|
||||
label: t.app.navigationReports,
|
||||
route: AppRoute.reports.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.settings_outlined,
|
||||
selectedIcon: Icons.settings,
|
||||
label: t.app.navigationSettings,
|
||||
route: AppRoute.settings.path,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
// Mobile bottom bar items (4 Tabs)
|
||||
List<({IconData icon, String label, String route})> _getMobileTabs(
|
||||
BuildContext context,
|
||||
) {
|
||||
final t = Translations.of(context);
|
||||
return [
|
||||
(
|
||||
icon: Icons.dashboard,
|
||||
label: t.app.navigationDashboard,
|
||||
route: AppRoute.home.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.home,
|
||||
label: (t.app.navigationHousehold),
|
||||
route: AppRoute.budget.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.inventory_2,
|
||||
label: t.app.navigationInventory,
|
||||
route: AppRoute.inventory.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.directions_car,
|
||||
label: (t.app.navigationCar),
|
||||
route: AppRoute.car.path,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Route→Feature-Guard
|
||||
bool _routeEnabled(String route, FeatureController fc) {
|
||||
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;
|
||||
|
||||
// Default: sichtbar
|
||||
return true;
|
||||
}
|
||||
|
||||
@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(),
|
||||
// Kein Burger-Button, da mobil ohne Drawer (BottomNav) und Desktop mit persistentem Drawer
|
||||
leading: null,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: t.app.tooltipNotifications,
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.notifications_none),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: t.app.tooltipUserSettings,
|
||||
onPressed: () => context.push(AppRoute.settings.path),
|
||||
icon: const Icon(Icons.account_circle_outlined),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
if (!isDesktop) {
|
||||
// ------------------- MOBILE: Bottom Navigation -------------------
|
||||
final fc = context.read<FeatureController>();
|
||||
|
||||
// Wenn aktuelle Route deaktiviert ist, sanft nach Home umleiten
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final p = GoRouterState.of(context).matchedLocation;
|
||||
if (!_routeEnabled(p, fc)) {
|
||||
if (mounted) context.go(AppRoute.home.path);
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: appBar,
|
||||
// Let content flow under the floating bar for the glass effect
|
||||
extendBody: true,
|
||||
body: SafeArea(child: widget.child),
|
||||
bottomNavigationBar: AnimatedBuilder(
|
||||
animation: fc,
|
||||
builder: (context, _) {
|
||||
final baseTabs = _getMobileTabs(context);
|
||||
|
||||
// Filter allowed tabs (your existing guard)
|
||||
var tabs = <({IconData icon, String label, String route})>[
|
||||
for (final it in baseTabs)
|
||||
if (_routeEnabled(it.route, fc)) it,
|
||||
];
|
||||
|
||||
// Fallback to ensure min. 2 items
|
||||
if (tabs.length < 2) {
|
||||
tabs = [
|
||||
(
|
||||
icon: Icons.dashboard,
|
||||
label: Translations.of(context).app.navigationDashboard,
|
||||
route: AppRoute.home.path,
|
||||
),
|
||||
(
|
||||
icon: Icons.settings,
|
||||
label: Translations.of(context).app.navigationSettings,
|
||||
route: AppRoute.settings.path,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
final currentPath = GoRouterState.of(context).matchedLocation;
|
||||
|
||||
return GlassBottomBar(
|
||||
currentPath: currentPath,
|
||||
onSelect: (route) => context.go(route),
|
||||
items: [
|
||||
for (final it in tabs)
|
||||
GlassBottomBarItem(
|
||||
icon: it.icon,
|
||||
label: it.label,
|
||||
route: it.route,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ------------------- 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;
|
||||
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;
|
||||
}
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: fc,
|
||||
builder: (context, _) {
|
||||
final visibleItems = [
|
||||
for (final it in items)
|
||||
if (routeEnabled(it.route)) it,
|
||||
];
|
||||
|
||||
final currentPath = GoRouterState.of(context).matchedLocation;
|
||||
int safeSelected = 0;
|
||||
for (var i = 0; i < visibleItems.length; i++) {
|
||||
if (currentPath.startsWith(visibleItems[i].route)) {
|
||||
safeSelected = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
// 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),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user