Apply M3 filter placement recommendations to device and notification lists

Move status filter chips into AppBar bottom slot (left-aligned, sticky) on
both list screens. On the device list, relocate owner and type filters into
a modal bottom sheet behind a badged filter icon, and dismiss the sheet on
clear so fields always reflect the current filter state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-05-27 20:00:59 -04:00
co-authored by Claude Sonnet 4.6
parent e7d075ec29
commit 171e7516bc
2 changed files with 152 additions and 120 deletions
+95 -66
View File
@@ -87,23 +87,107 @@ class _DeviceListState extends State<DeviceList> {
} }
} }
bool get _hasActiveDetailFilters =>
_ownerController.text.isNotEmpty || _typeFilter != null;
void _showFilterSheet(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
builder: (sheetContext) {
return Padding(
padding: EdgeInsets.only(
left: 16,
right: 16,
top: 24,
bottom: MediaQuery.of(sheetContext).viewInsets.bottom + 24,
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Filters', style: Theme.of(sheetContext).textTheme.titleMedium),
const SizedBox(height: 16),
TextField(
controller: _ownerController,
decoration: const InputDecoration(
labelText: 'Owner',
prefixIcon: Icon(Icons.person_outline),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
DropdownButtonFormField<DeviceType?>(
initialValue: _typeFilter,
decoration: const InputDecoration(
labelText: 'Type',
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem(value: null, child: Text('All types')),
...DeviceType.values.map(
(t) => DropdownMenuItem(
value: t,
child: Row(
children: [
Icon(t.icon, size: 16),
const SizedBox(width: 4),
Text(t.label),
],
),
),
),
],
onChanged: (value) {
setState(() => _typeFilter = value);
_loadDevices();
},
),
const SizedBox(height: 16),
if (_hasActiveDetailFilters)
OutlinedButton(
onPressed: () {
setState(() {
_ownerController.clear();
_typeFilter = null;
});
_loadDevices();
Navigator.of(sheetContext).pop();
},
child: const Text('Clear filters'),
),
],
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
final formatter = FriendlyDateFormatter(); final formatter = FriendlyDateFormatter();
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Devices')), appBar: AppBar(
body: Column( title: const Text('Devices'),
children: [ actions: [
Padding( Badge(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), isLabelVisible: _hasActiveDetailFilters,
child: Column( child: IconButton(
crossAxisAlignment: CrossAxisAlignment.start, icon: const Icon(Icons.filter_list),
mainAxisSize: MainAxisSize.min, tooltip: 'Filter',
children: [ onPressed: () => _showFilterSheet(context),
SingleChildScrollView( ),
),
],
bottom: PreferredSize(
preferredSize: const Size.fromHeight(48),
child: Align(
alignment: Alignment.centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: Wrap( child: Wrap(
spacing: 8.0, spacing: 8.0,
children: [ children: [
@@ -134,62 +218,10 @@ class _DeviceListState extends State<DeviceList> {
], ],
), ),
), ),
const SizedBox(height: 8),
Row(
children: [
Expanded(
flex: 2,
child: TextField(
controller: _ownerController,
decoration: const InputDecoration(
labelText: 'Owner',
prefixIcon: Icon(Icons.person_outline),
isDense: true,
border: OutlineInputBorder(),
), ),
), ),
), ),
const SizedBox(width: 8), body: _isLoading
Expanded(
child: DropdownButtonFormField<DeviceType?>(
initialValue: _typeFilter,
decoration: const InputDecoration(
labelText: 'Type',
isDense: true,
border: OutlineInputBorder(),
),
items: [
const DropdownMenuItem(
value: null,
child: Text('All'),
),
...DeviceType.values.map(
(t) => DropdownMenuItem(
value: t,
child: Row(
children: [
Icon(t.icon, size: 16),
const SizedBox(width: 4),
Text(t.label),
],
),
),
),
],
onChanged: (value) {
setState(() => _typeFilter = value);
_loadDevices();
},
),
),
],
),
const SizedBox(height: 4),
],
),
),
Expanded(
child: _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _error != null : _error != null
? Center(child: Text('Error: $_error')) ? Center(child: Text('Error: $_error'))
@@ -277,9 +309,6 @@ class _DeviceListState extends State<DeviceList> {
}, },
), ),
), ),
),
],
),
); );
} }
} }
@@ -122,19 +122,18 @@ class _NotificationListState extends State<NotificationList> {
tooltip: 'Mark all as read', tooltip: 'Mark all as read',
), ),
], ],
), bottom: PreferredSize(
body: CustomScrollView( preferredSize: const Size.fromHeight(48),
slivers: [ child: Align(
// Filters go here alignment: Alignment.centerLeft,
SliverToBoxAdapter( child: SingleChildScrollView(
child: Container( scrollDirection: Axis.horizontal,
height: 50, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
alignment: Alignment.centerRight,
child: Wrap( child: Wrap(
spacing: 8.0, spacing: 8.0,
children: [ children: [
ChoiceChip( ChoiceChip(
label: Text('New'), label: const Text('New'),
selected: _filterChoice == 1, selected: _filterChoice == 1,
onSelected: (bool selected) { onSelected: (bool selected) {
_filterChoice = 1; _filterChoice = 1;
@@ -142,7 +141,7 @@ class _NotificationListState extends State<NotificationList> {
}, },
), ),
ChoiceChip( ChoiceChip(
label: Text('Old'), label: const Text('Old'),
selected: _filterChoice == 2, selected: _filterChoice == 2,
onSelected: (bool selected) { onSelected: (bool selected) {
_filterChoice = 2; _filterChoice = 2;
@@ -150,7 +149,7 @@ class _NotificationListState extends State<NotificationList> {
}, },
), ),
ChoiceChip( ChoiceChip(
label: Text('All'), label: const Text('All'),
selected: _filterChoice == 3, selected: _filterChoice == 3,
onSelected: (bool selected) { onSelected: (bool selected) {
_filterChoice = 3; _filterChoice = 3;
@@ -161,6 +160,10 @@ class _NotificationListState extends State<NotificationList> {
), ),
), ),
), ),
),
),
body: CustomScrollView(
slivers: [
PagedSliverList<int, oott_model.Notification>( PagedSliverList<int, oott_model.Notification>(
state: state, state: state,
fetchNextPage: fetchNextPage, fetchNextPage: fetchNextPage,