mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
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:
co-authored by
Claude Sonnet 4.6
parent
ebcde0ff4a
commit
e5c27f067c
@@ -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 {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user