feat(#17): responsive settings shell + intent categories + path-hash
Replace the monolithic card+dialog settings screen with a responsive master-detail shell (rail+pane when wide, list+push when narrow; layout chosen by width via LayoutBuilder). Regroup the existing settings into intent categories and surface Path Hash Size (1/2/3-byte) in Radio and range, the first device-config-parity gap (previously repeater/CLI-only). No settings lost: every existing control stays reachable. Phase 0 of #7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>pull/20/head
parent
54d3ee4856
commit
7d0080f2cd
@ -0,0 +1,145 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// One entry in the responsive settings shell: an icon, a title, an optional
|
||||||
|
/// subtitle, and a builder for the category's detail pane.
|
||||||
|
class SettingsCategory {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String? subtitle;
|
||||||
|
final WidgetBuilder builder;
|
||||||
|
|
||||||
|
const SettingsCategory({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
this.subtitle,
|
||||||
|
required this.builder,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Responsive master-detail container for settings.
|
||||||
|
///
|
||||||
|
/// Wide layouts (>= [_wideBreakpoint] logical pixels) show a pinned category
|
||||||
|
/// rail beside the selected detail pane. Narrow layouts show the category list
|
||||||
|
/// and push the detail onto the navigator. Same categories, same code, every
|
||||||
|
/// platform — the layout is chosen from the available width, not the OS.
|
||||||
|
class SettingsShell extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final List<SettingsCategory> categories;
|
||||||
|
final PreferredSizeWidget? appBarBottom;
|
||||||
|
|
||||||
|
const SettingsShell({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.categories,
|
||||||
|
this.appBarBottom,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SettingsShell> createState() => _SettingsShellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SettingsShellState extends State<SettingsShell> {
|
||||||
|
static const double _wideBreakpoint = 720;
|
||||||
|
static const double _railWidth = 280;
|
||||||
|
|
||||||
|
int _selected = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return constraints.maxWidth >= _wideBreakpoint
|
||||||
|
? _buildWide(context)
|
||||||
|
: _buildNarrow(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferredSizeWidget _appBar(String title) {
|
||||||
|
return AppBar(
|
||||||
|
title: Text(title),
|
||||||
|
centerTitle: true,
|
||||||
|
bottom: widget.appBarBottom,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildWide(BuildContext context) {
|
||||||
|
final categories = widget.categories;
|
||||||
|
final selected = _selected.clamp(0, categories.length - 1);
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _appBar(widget.title),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: _railWidth,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: categories.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final category = categories[i];
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(category.icon),
|
||||||
|
title: Text(category.title),
|
||||||
|
selected: i == selected,
|
||||||
|
selectedTileColor:
|
||||||
|
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||||
|
onTap: () => setState(() => _selected = i),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const VerticalDivider(width: 1),
|
||||||
|
Expanded(
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: ValueKey<int>(selected),
|
||||||
|
child: categories[selected].builder(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNarrow(BuildContext context) {
|
||||||
|
final categories = widget.categories;
|
||||||
|
return Scaffold(
|
||||||
|
appBar: _appBar(widget.title),
|
||||||
|
body: SafeArea(
|
||||||
|
top: false,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: categories.length,
|
||||||
|
itemBuilder: (context, i) {
|
||||||
|
final category = categories[i];
|
||||||
|
return ListTile(
|
||||||
|
leading: Icon(category.icon),
|
||||||
|
title: Text(category.title),
|
||||||
|
subtitle:
|
||||||
|
category.subtitle != null ? Text(category.subtitle!) : null,
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => _openDetail(context, category),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openDetail(BuildContext context, SettingsCategory category) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(category.title),
|
||||||
|
centerTitle: true,
|
||||||
|
bottom: widget.appBarBottom,
|
||||||
|
),
|
||||||
|
body: SafeArea(top: false, child: category.builder(context)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue