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