worldhopper/lib/screens/shell/main_shell_screen.dart
Felipe M. a24307d873
feat: tablet layout with sidebar navigation and responsive grids
Use width-based adaptive layout (768px breakpoint) so tablets get a
NavigationRail sidebar instead of the bottom bar, and grid columns
scale from 2 to 5 based on available width.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 12:44:45 +01:00

124 lines
4.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worldhopper/config/router.dart';
import 'package:worldhopper/l10n/app_localizations.dart';
import 'package:worldhopper/helpers/responsive_helper.dart';
import 'package:worldhopper/providers/navigation_provider.dart';
/// Shell widget that provides the bottom navigation bar
/// and handles tab switching for the main app sections
class MainShellScreen extends ConsumerStatefulWidget {
final StatefulNavigationShell navigationShell;
const MainShellScreen({
super.key,
required this.navigationShell,
});
@override
ConsumerState<MainShellScreen> createState() => _MainShellScreenState();
}
class _MainShellScreenState extends ConsumerState<MainShellScreen> {
bool _initialized = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initialized) {
_initialized = true;
// Load saved tab index and navigate to it
final savedIndex = ref.read(navigationStateProvider);
if (savedIndex != widget.navigationShell.currentIndex) {
// Delay navigation to allow widget tree to build
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.navigationShell.goBranch(savedIndex, initialLocation: true);
});
}
}
}
void _onDestinationSelected(int index) {
// Persist the tab change
ref.read(navigationStateProvider.notifier).setTabIndex(index);
// If re-tapping the same tab, pop any Navigator-pushed routes
// (e.g. sub-settings screens) back to the branch root
if (index == widget.navigationShell.currentIndex) {
final navigatorKey = AppRouter.branchNavigatorKeys[index];
navigatorKey.currentState?.popUntil((route) => route.isFirst);
}
// Navigate to the selected tab
widget.navigationShell.goBranch(
index,
initialLocation: index == widget.navigationShell.currentIndex,
);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final wide = isWideScreen(MediaQuery.of(context).size.width);
if (wide) {
return Scaffold(
body: Row(
children: [
NavigationRail(
labelType: NavigationRailLabelType.all,
selectedIndex: widget.navigationShell.currentIndex,
onDestinationSelected: _onDestinationSelected,
destinations: [
NavigationRailDestination(
icon: const Icon(Icons.auto_stories_outlined),
selectedIcon: const Icon(Icons.auto_stories),
label: Text(l10n.navLibrary),
),
NavigationRailDestination(
icon: const Icon(Icons.dns_outlined),
selectedIcon: const Icon(Icons.dns),
label: Text(l10n.navServers),
),
NavigationRailDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: Text(l10n.navSettings),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(child: widget.navigationShell),
],
),
);
}
return Scaffold(
body: widget.navigationShell,
bottomNavigationBar: NavigationBar(
selectedIndex: widget.navigationShell.currentIndex,
onDestinationSelected: _onDestinationSelected,
destinations: [
NavigationDestination(
icon: const Icon(Icons.auto_stories_outlined),
selectedIcon: const Icon(Icons.auto_stories),
label: l10n.navLibrary,
),
NavigationDestination(
icon: const Icon(Icons.dns_outlined),
selectedIcon: const Icon(Icons.dns),
label: l10n.navServers,
),
NavigationDestination(
icon: const Icon(Icons.settings_outlined),
selectedIcon: const Icon(Icons.settings),
label: l10n.navSettings,
),
],
),
);
}
}