Files
finlog/finlog_app/app/lib/modules/app_shell.dart

270 lines
8.2 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 _railBreakpoint = 800; // tablet and up
bool _railExtended = true; // start "open" on tablet/desktop
final FocusNode _contentFocus = FocusNode(debugLabel: 'contentFocus');
@override
void dispose() {
_contentFocus.dispose();
super.dispose();
}
// --- NAV ITEMS -------------------------------------------------------------
List<({IconData icon, IconData? selectedIcon, String label, String route})> _getItems(BuildContext context) {
final t = Translations.of(context);
return [
(
icon: Icons.dashboard_outlined,
selectedIcon: Icons.dashboard,
label: t.app.navigationDashboard,
route: '/home',
),
(
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,
selectedIcon: Icons.settings_outlined,
label: t.app.navigationSettings,
route: '/settings',
),
];
}
int _indexForPath(String p, List<({IconData icon, IconData? selectedIcon, String label, String route})> items) {
for (var i = 0; i < items.length; i++) {
if (p.startsWith(items[i].route)) 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);
@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 currentPath = GoRouterState.of(context).matchedLocation;
// keep focus on right/content pane on wide layouts
if (isRail) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted && !_contentFocus.hasFocus) _contentFocus.requestFocus();
});
}
final appBar = AppBar(
title: _LogoHeader(),
leading: isRail
? 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 (!isRail) {
// ------------------- MOBILE: Drawer -------------------
final selectedIndex = _indexForPath(currentPath, items);
return Scaffold(
appBar: appBar,
drawer: _AppDrawer(items: items, selectedIndex: selectedIndex),
body: SafeArea(child: widget.child),
);
}
// ------------------- TABLET/DESKTOP: NavigationRail -------------------
final selected = _indexForPath(currentPath, items);
final scheme = Theme.of(context).colorScheme;
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,
),
),
],
),
),
),
const VerticalDivider(width: 1),
Expanded(
child: SafeArea(
child: Focus(
focusNode: _contentFocus,
autofocus: true,
child: widget.child,
),
),
),
],
),
);
}
}
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),
],
),
);
}
}