mirror of
https://github.com/rzuasti/oott.git
synced 2026-07-08 19:21:54 +02:00
Collapse chart time-range pills to a dropdown on phones
The device detail chart's time-range selector (Today / Last week / ...) rendered as a SegmentedButton that overflowed on narrow phone layouts. Reuse the responsive FilterSelector widget so the same control is used for both the list filters and the chart: segmented pills on wide layouts, a compact dropdown combo box on phones. To keep the two controls consistent, FilterSelector now renders a SegmentedButton (instead of ChoiceChips) on wide layouts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4bbaa6d3e1
commit
b3a4e4cb0b
@@ -28,15 +28,7 @@
|
|||||||
- [ ] Review the whole codebase for dead code, duplication and simplicity
|
- [ ] Review the whole codebase for dead code, duplication and simplicity
|
||||||
- [ ] In the devices list, add an order by type (in the wide version it should be over the icon on the left of the list)
|
- [ ] In the devices list, add an order by type (in the wide version it should be over the icon on the left of the list)
|
||||||
- [ ] How expensive it is to get the total number of pages and implement a go to last page
|
- [ ] How expensive it is to get the total number of pages and implement a go to last page
|
||||||
- [ ] Mobile - Device details - Chart buttons not fully visible, replace with combo only for narrow devices
|
- [x] Mobile - Device details - Chart buttons not fully visible, replace with combo only for narrow devices
|
||||||
- [x] Mobile - Implement pull to refresh on the notifications and devices list
|
|
||||||
- [x] Mobile - reduce lists length (# of items) so they fit on a phone in one screen (use iPhone latest gen and Google phone latest gen)
|
|
||||||
- [x] Mobile - In the devices list the filters and sort buttons overlap (dont fit in the screen)
|
|
||||||
- [x] When changing pages (either list) the items should change to placeholders while loading
|
|
||||||
- [x] The notifications list should not refresh coldly every time. It should add/remove notifications with an animation as if a stack
|
|
||||||
- [x] Make gruvbox the default theme
|
|
||||||
- [x] Can we add front-end tests?
|
|
||||||
- [x] Break down oott_api.dart in modules
|
|
||||||
|
|
||||||
## Improve engine
|
## Improve engine
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:intl/intl.dart';
|
|||||||
import '../model/device.dart';
|
import '../model/device.dart';
|
||||||
import '../model/device_event.dart';
|
import '../model/device_event.dart';
|
||||||
import '../utils/oott_api.dart';
|
import '../utils/oott_api.dart';
|
||||||
|
import '../widgets/filter_selector.dart';
|
||||||
|
|
||||||
enum _TimeRange {
|
enum _TimeRange {
|
||||||
today('Today'),
|
today('Today'),
|
||||||
@@ -128,19 +129,15 @@ class _DeviceEventHistoryState extends State<DeviceEventHistory> {
|
|||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
FilterSelector<_TimeRange>(
|
||||||
scrollDirection: Axis.horizontal,
|
values: _TimeRange.values,
|
||||||
child: SegmentedButton<_TimeRange>(
|
selected: _selectedRange,
|
||||||
segments: _TimeRange.values
|
labelOf: (r) => r.label,
|
||||||
.map((r) => ButtonSegment(value: r, label: Text(r.label)))
|
onSelected: (range) {
|
||||||
.toList(),
|
setState(() => _selectedRange = range);
|
||||||
selected: {_selectedRange},
|
|
||||||
onSelectionChanged: (selection) {
|
|
||||||
setState(() => _selectedRange = selection.first);
|
|
||||||
_loadEvents();
|
_loadEvents();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
if (events.isEmpty)
|
if (events.isEmpty)
|
||||||
const Padding(
|
const Padding(
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../theme/dimens.dart';
|
import '../theme/dimens.dart';
|
||||||
|
|
||||||
/// A single-select filter control that adapts to the available width.
|
/// A single-select control that adapts to the available width.
|
||||||
///
|
///
|
||||||
/// On wide (tablet/desktop) layouts the options are laid out as a row of
|
/// On wide (tablet/desktop) layouts the options are laid out as a
|
||||||
/// [ChoiceChip]s. On narrow (phone) layouts, where a full row of chips competes
|
/// [SegmentedButton] of pills. On narrow (phone) layouts, where a full row of
|
||||||
/// for horizontal space with neighbouring actions, it collapses to a compact
|
/// pills competes for horizontal space with neighbouring actions, it collapses
|
||||||
/// dropdown button showing the current selection.
|
/// to a compact dropdown button showing the current selection.
|
||||||
class FilterSelector<T> extends StatelessWidget {
|
class FilterSelector<T> extends StatelessWidget {
|
||||||
const FilterSelector({
|
const FilterSelector({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -32,27 +32,26 @@ class FilterSelector<T> extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final isWide = MediaQuery.sizeOf(context).width >= Breakpoints.medium;
|
final isWide = MediaQuery.sizeOf(context).width >= Breakpoints.medium;
|
||||||
return isWide ? _buildChips(context) : _buildDropdown(context);
|
return isWide ? _buildSegmented(context) : _buildDropdown(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChips(BuildContext context) {
|
Widget _buildSegmented(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: Insets.sm,
|
horizontal: Insets.sm,
|
||||||
vertical: Insets.xs,
|
vertical: Insets.xs,
|
||||||
),
|
),
|
||||||
child: Wrap(
|
child: SegmentedButton<T>(
|
||||||
spacing: Insets.sm,
|
showSelectedIcon: false,
|
||||||
children: values
|
segments: values
|
||||||
.map(
|
.map(
|
||||||
(value) => ChoiceChip(
|
(value) =>
|
||||||
label: Text(labelOf(value)),
|
ButtonSegment<T>(value: value, label: Text(labelOf(value))),
|
||||||
selected: selected == value,
|
|
||||||
onSelected: (_) => onSelected(value),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
|
selected: {selected},
|
||||||
|
onSelectionChanged: (selection) => onSelected(selection.first),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ Widget _harness({
|
|||||||
}
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('wide layout shows chips and reports selection', (tester) async {
|
testWidgets('wide layout shows segmented pills and reports selection', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
String? picked;
|
String? picked;
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
_harness(
|
_harness(
|
||||||
@@ -33,9 +35,12 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// All three options are visible as chips at once.
|
// All three options are visible as segments at once.
|
||||||
expect(find.byType(ChoiceChip), findsNWidgets(3));
|
expect(find.byType(SegmentedButton<String>), findsOneWidget);
|
||||||
expect(find.byType(PopupMenuButton<String>), findsNothing);
|
expect(find.byType(PopupMenuButton<String>), findsNothing);
|
||||||
|
expect(find.text('New'), findsOneWidget);
|
||||||
|
expect(find.text('Old'), findsOneWidget);
|
||||||
|
expect(find.text('All'), findsOneWidget);
|
||||||
|
|
||||||
await tester.tap(find.text('Old'));
|
await tester.tap(find.text('Old'));
|
||||||
expect(picked, 'Old');
|
expect(picked, 'Old');
|
||||||
@@ -51,8 +56,8 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Collapsed: no chips, a single button showing the current selection.
|
// Collapsed: no segmented pills, a single button showing the selection.
|
||||||
expect(find.byType(ChoiceChip), findsNothing);
|
expect(find.byType(SegmentedButton<String>), findsNothing);
|
||||||
expect(find.byType(PopupMenuButton<String>), findsOneWidget);
|
expect(find.byType(PopupMenuButton<String>), findsOneWidget);
|
||||||
expect(find.text('New'), findsOneWidget);
|
expect(find.text('New'), findsOneWidget);
|
||||||
expect(find.text('Old'), findsNothing);
|
expect(find.text('Old'), findsNothing);
|
||||||
|
|||||||
Reference in New Issue
Block a user