Align navigation with Material 3 responsive recommendations

- Replace single NavigationRail with adaptive navigation: NavigationBar
  (bottom) for compact <600dp, icon-only NavigationRail for medium
  600–840dp, and extended NavigationRail for expanded ≥840dp
- Remove nested Scaffold/AppBar from About, Settings, DeviceList, and
  DeviceDetail — all screens now render as plain widgets inside the
  single shell Scaffold; page titles surface as inline headings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-28 19:51:31 -04:00
co-authored by Claude Sonnet 4.6
parent ebcde0ff4a
commit e5c27f067c
5 changed files with 260 additions and 209 deletions
+3 -4
View File
@@ -15,13 +15,13 @@ class About extends StatelessWidget {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return Scaffold( return SingleChildScrollView(
appBar: AppBar(title: const Text('About OOTT')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(24), padding: const EdgeInsets.all(24),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('About OOTT', style: textTheme.headlineSmall),
const SizedBox(height: 16),
Text( Text(
'Easy to setup and use network device discovery and alert system. ' 'Easy to setup and use network device discovery and alert system. '
'Notifies you when new or unknown devices join your local area network.', 'Notifies you when new or unknown devices join your local area network.',
@@ -64,7 +64,6 @@ class About extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }
+21 -7
View File
@@ -53,11 +53,18 @@ class _DeviceDetailState extends State<DeviceDetail> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final device = _device; final device = _device;
return Scaffold( return Column(
appBar: AppBar( crossAxisAlignment: CrossAxisAlignment.start,
title: const Text('Device Details'), children: [
leading: BackButton(onPressed: () => context.pop()), Row(
actions: [ children: [
BackButton(onPressed: () => context.pop()),
Expanded(
child: Text(
'Device Details',
style: Theme.of(context).textTheme.headlineSmall,
),
),
if (device != null) if (device != null)
PopupMenuButton<String>( PopupMenuButton<String>(
icon: const Icon(Icons.more_vert), icon: const Icon(Icons.more_vert),
@@ -65,7 +72,11 @@ class _DeviceDetailState extends State<DeviceDetail> {
if (value == 'forget') { if (value == 'forget') {
await confirmForgetDevice(context, device, _loadDevice); await confirmForgetDevice(context, device, _loadDevice);
} else if (value == 'register') { } else if (value == 'register') {
await showRegisterDeviceDialog(context, device, _loadDevice); await showRegisterDeviceDialog(
context,
device,
_loadDevice,
);
} }
}, },
itemBuilder: (context) => [ itemBuilder: (context) => [
@@ -80,7 +91,8 @@ class _DeviceDetailState extends State<DeviceDetail> {
), ),
], ],
), ),
body: _isLoading Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _error != null : _error != null
? Center( ? Center(
@@ -113,6 +125,8 @@ class _DeviceDetailState extends State<DeviceDetail> {
], ],
), ),
), ),
),
],
); );
} }
} }
+13 -13
View File
@@ -119,11 +119,14 @@ class _DeviceListState extends State<DeviceList> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final formatter = FriendlyDateFormatter(); final formatter = FriendlyDateFormatter();
final textTheme = Theme.of(context).textTheme;
return Scaffold( return Column(
appBar: AppBar( crossAxisAlignment: CrossAxisAlignment.start,
title: const Text('Devices'), children: [
actions: [ Row(
children: [
Expanded(child: Text('Devices', style: textTheme.headlineSmall)),
Badge( Badge(
isLabelVisible: _hasActiveDetailFilters, isLabelVisible: _hasActiveDetailFilters,
child: IconButton( child: IconButton(
@@ -133,11 +136,8 @@ class _DeviceListState extends State<DeviceList> {
), ),
), ),
], ],
bottom: PreferredSize( ),
preferredSize: const Size.fromHeight(48), SingleChildScrollView(
child: Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Wrap( child: Wrap(
@@ -156,10 +156,8 @@ class _DeviceListState extends State<DeviceList> {
.toList(), .toList(),
), ),
), ),
), Expanded(
), child: _isLoading
),
body: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _error != null : _error != null
? Center(child: Text('Error: $_error')) ? Center(child: Text('Error: $_error'))
@@ -177,6 +175,8 @@ class _DeviceListState extends State<DeviceList> {
), ),
), ),
), ),
),
],
); );
} }
} }
+82 -46
View File
@@ -9,9 +9,34 @@ import 'home/home_screen.dart';
import 'status/status_screen.dart'; import 'status/status_screen.dart';
import 'utils/pref_utils.dart'; import 'utils/pref_utils.dart';
// M3 window size class breakpoints
const _mediumBreakpoint = 600.0;
const _expandedBreakpoint = 840.0;
typedef _NavDest = ({IconData icon, IconData activeIcon, String label});
const List<_NavDest> _destinations = [
(icon: Icons.home_outlined, activeIcon: Icons.home, label: 'Home'),
(
icon: Icons.devices_other_outlined,
activeIcon: Icons.devices_other,
label: 'Devices',
),
(
icon: Icons.monitor_heart_outlined,
activeIcon: Icons.monitor_heart,
label: 'Status',
),
(
icon: Icons.settings_outlined,
activeIcon: Icons.settings,
label: 'Settings',
),
(icon: Icons.info_outline, activeIcon: Icons.info, label: 'About'),
];
// Routes definitions // Routes definitions
final GoRouter router = GoRouter( final GoRouter router = GoRouter(
// If there is no API base URL send the user to settings
initialLocation: '/notifications', initialLocation: '/notifications',
routes: [ routes: [
ShellRoute( ShellRoute(
@@ -72,27 +97,35 @@ class MainShell extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final selectedIndex = _calculateSelectedIndex(context);
final width = constraints.maxWidth;
if (width < _mediumBreakpoint) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: _buildAppBar(context),
title: Container( body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( child: child,
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(10),
), ),
child: Text( bottomNavigationBar: NavigationBar(
'OOTT', selectedIndex: selectedIndex,
style: GoogleFonts.barlowCondensed( onDestinationSelected: (index) =>
color: Theme.of(context).colorScheme.onPrimary, _onDestinationSelected(index, context),
fontWeight: FontWeight.bold, destinations: _destinations
fontSize: 26, .map(
(d) => NavigationDestination(
icon: Icon(d.icon),
selectedIcon: Icon(d.activeIcon),
label: d.label,
), ),
)
.toList(),
), ),
), );
backgroundColor: Theme.of( }
context,
).colorScheme.surfaceContainerLowest, return Scaffold(
), appBar: _buildAppBar(context),
body: Row( body: Row(
children: [ children: [
SafeArea( SafeArea(
@@ -100,35 +133,17 @@ class MainShell extends StatelessWidget {
backgroundColor: Theme.of( backgroundColor: Theme.of(
context, context,
).colorScheme.surfaceContainerLow, ).colorScheme.surfaceContainerLow,
extended: constraints.maxWidth >= 600, extended: width >= _expandedBreakpoint,
destinations: [ destinations: _destinations
NavigationRailDestination( .map(
icon: Icon(Icons.home_outlined), (d) => NavigationRailDestination(
selectedIcon: Icon(Icons.home), icon: Icon(d.icon),
label: Text('Home'), selectedIcon: Icon(d.activeIcon),
label: Text(d.label),
), ),
NavigationRailDestination( )
icon: Icon(Icons.devices_other_outlined), .toList(),
selectedIcon: Icon(Icons.devices_other), selectedIndex: selectedIndex,
label: Text('Devices'),
),
NavigationRailDestination(
icon: Icon(Icons.monitor_heart_outlined),
selectedIcon: Icon(Icons.monitor_heart),
label: Text('Status'),
),
NavigationRailDestination(
icon: Icon(Icons.settings_outlined),
selectedIcon: Icon(Icons.settings),
label: Text('Settings'),
),
NavigationRailDestination(
icon: Icon(Icons.info_outline),
selectedIcon: Icon(Icons.info),
label: Text('About'),
),
],
selectedIndex: _calculateSelectedIndex(context),
onDestinationSelected: (index) => onDestinationSelected: (index) =>
_onDestinationSelected(index, context), _onDestinationSelected(index, context),
), ),
@@ -146,6 +161,27 @@ class MainShell extends StatelessWidget {
}, },
); );
} }
AppBar _buildAppBar(BuildContext context) {
return AppBar(
title: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'OOTT',
style: GoogleFonts.barlowCondensed(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
fontSize: 26,
),
),
),
backgroundColor: Theme.of(context).colorScheme.surfaceContainerLowest,
);
}
} }
String? _redirectToSettings() { String? _redirectToSettings() {
+5 -3
View File
@@ -79,13 +79,15 @@ class _SettingsState extends State<Settings> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appColors = Theme.of(context).extension<AppColorExtension>()!; final appColors = Theme.of(context).extension<AppColorExtension>()!;
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold( return SingleChildScrollView(
appBar: AppBar(title: const Text('Settings')), child: Form(
body: Form(
key: _formKey, key: _formKey,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text('Settings', style: textTheme.headlineSmall),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
controller: _baseUrlController, controller: _baseUrlController,