Skip to content

Commit 4c45fc9

Browse files
committed
feat: add DimmingAppBar component and refactor DimmingScreen layout
1 parent b6cb149 commit 4c45fc9

File tree

6 files changed

+100
-125
lines changed

6 files changed

+100
-125
lines changed

client/lib/devices/borneo/lyfi/views/dimming_screen.dart

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:borneo_common/io/net/rssi.dart';
12
import 'package:flutter/material.dart';
23
import 'package:provider/provider.dart';
34
import 'package:flutter_gettext/flutter_gettext/context_ext.dart';
@@ -14,6 +15,66 @@ import 'editor/schedule_editor_view.dart';
1415
import 'editor/sun_editor_view.dart';
1516
// screen_top_rounded_container is used inside slider lists
1617

18+
class DimmingAppBar extends StatelessWidget {
19+
final VoidCallback? onBack;
20+
const DimmingAppBar({super.key, this.onBack});
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return SliverAppBar(
25+
pinned: false,
26+
floating: false,
27+
snap: false,
28+
foregroundColor: Theme.of(context).colorScheme.onSurface,
29+
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
30+
centerTitle: true,
31+
leading: Selector<LyfiViewModel, bool>(
32+
selector: (context, vm) => vm.isBusy,
33+
builder: (context, isBusy, child) =>
34+
IconButton(icon: const Icon(Icons.arrow_back), onPressed: isBusy ? null : onBack),
35+
),
36+
title: Selector<LyfiViewModel, ({LyfiMode mode, bool canSwitch})>(
37+
selector: (context, vm) => (
38+
mode: vm.mode,
39+
canSwitch: vm.isOnline && !vm.isSuspectedOffline && vm.isOn && vm.state == LyfiState.dimming,
40+
),
41+
builder: (context, data, _) {
42+
return SegmentedButton<LyfiMode>(
43+
showSelectedIcon: false,
44+
selected: <LyfiMode>{data.mode},
45+
segments: [
46+
ButtonSegment<LyfiMode>(value: LyfiMode.manual, icon: const Icon(Icons.bar_chart_outlined, size: 20)),
47+
ButtonSegment<LyfiMode>(value: LyfiMode.scheduled, icon: const Icon(Icons.alarm_outlined, size: 20)),
48+
ButtonSegment<LyfiMode>(value: LyfiMode.sun, icon: const Icon(Icons.wb_sunny_outlined, size: 20)),
49+
],
50+
onSelectionChanged: data.canSwitch
51+
? (Set<LyfiMode> newSelection) {
52+
if (data.mode != newSelection.single) {
53+
context.read<LyfiViewModel>().switchMode(newSelection.single);
54+
}
55+
}
56+
: null,
57+
);
58+
},
59+
),
60+
actions: [
61+
Selector<LyfiViewModel, RssiLevel?>(
62+
selector: (_, vm) => vm.rssiLevel,
63+
builder: (context, rssi, _) => Center(
64+
child: switch (rssi) {
65+
null => Icon(Icons.wifi_off, size: 24, color: Theme.of(context).colorScheme.error),
66+
RssiLevel.strong => const Icon(Icons.wifi_rounded, size: 24),
67+
RssiLevel.medium => const Icon(Icons.wifi_2_bar_rounded, size: 24),
68+
RssiLevel.weak => const Icon(Icons.wifi_1_bar_rounded, size: 24),
69+
},
70+
),
71+
),
72+
const SizedBox(width: 16),
73+
],
74+
);
75+
}
76+
}
77+
1778
class DimmingScreen extends StatelessWidget {
1879
static const routeName = '/lyfi/dimming';
1980
const DimmingScreen({super.key});
@@ -35,13 +96,14 @@ class DimmingScreen extends StatelessWidget {
3596
child: Scaffold(
3697
body: Stack(
3798
children: [
38-
// keep the sliver headers but disable scrolling entirely; this mirrors
39-
// the pattern used by the details screen and ensures the UI is fixed
40-
// in place instead of being scrollable.
41-
NestedScrollView(
99+
// CustomScrollView with NeverScrollableScrollPhysics keeps the
100+
// header slivers pinned while the body itself cannot scroll.
101+
// The SingleChildScrollView inside each editor view handles local
102+
// scrolling for just the slider-list area.
103+
CustomScrollView(
42104
physics: const NeverScrollableScrollPhysics(),
43-
headerSliverBuilder: (context, innerBoxIsScrolled) => [
44-
LyfiAppBar(
105+
slivers: [
106+
DimmingAppBar(
45107
onBack: () async {
46108
final vm = context.read<LyfiViewModel>();
47109
if (!vm.isLocked && !vm.isSuspectedOffline) {
@@ -54,8 +116,8 @@ class DimmingScreen extends StatelessWidget {
54116
),
55117
const LyfiBusyIndicatorSliver(),
56118
const LyfiStatusBannersSliver(),
119+
const SliverFillRemaining(hasScrollBody: true, child: DimmingView()),
57120
],
58-
body: DimmingView(),
59121
),
60122
const _ConnectionGuardOverlay(),
61123
],
@@ -65,72 +127,12 @@ class DimmingScreen extends StatelessWidget {
65127
}
66128
}
67129

68-
class DimmingHeroPanel extends StatelessWidget {
69-
const DimmingHeroPanel({super.key});
70-
71-
@override
72-
Widget build(BuildContext context) {
73-
return Material(
74-
color: Theme.of(context).scaffoldBackgroundColor,
75-
child: Row(
76-
mainAxisAlignment: MainAxisAlignment.center,
77-
crossAxisAlignment: CrossAxisAlignment.end,
78-
children: [
79-
Selector<LyfiViewModel, ({LyfiMode mode, bool canSwitch})>(
80-
selector: (context, vm) => (
81-
mode: vm.mode,
82-
canSwitch: vm.isOnline && !vm.isSuspectedOffline && vm.isOn && vm.state == LyfiState.dimming,
83-
),
84-
builder: (context, vm, _) {
85-
return SegmentedButton<LyfiMode>(
86-
showSelectedIcon: false,
87-
selected: <LyfiMode>{vm.mode},
88-
segments: [
89-
ButtonSegment<LyfiMode>(
90-
value: LyfiMode.manual,
91-
label: Text(context.translate('MANU')),
92-
icon: const Icon(Icons.bar_chart_outlined, size: 16),
93-
),
94-
ButtonSegment<LyfiMode>(
95-
value: LyfiMode.scheduled,
96-
label: Text(context.translate('SCHED')),
97-
icon: const Icon(Icons.alarm_outlined, size: 16),
98-
),
99-
ButtonSegment<LyfiMode>(
100-
value: LyfiMode.sun,
101-
label: Text(context.translate('SUN')),
102-
icon: const Icon(Icons.wb_sunny_outlined, size: 16),
103-
),
104-
],
105-
onSelectionChanged: vm.canSwitch
106-
? (Set<LyfiMode> newSelection) {
107-
if (vm.mode != newSelection.single) {
108-
context.read<LyfiViewModel>().switchMode(newSelection.single);
109-
}
110-
}
111-
: null,
112-
);
113-
},
114-
),
115-
],
116-
),
117-
);
118-
}
119-
}
120-
121130
class DimmingView extends StatelessWidget {
122131
const DimmingView({super.key});
123132

124133
@override
125134
Widget build(BuildContext context) {
126-
return Column(
127-
spacing: 8,
128-
mainAxisAlignment: MainAxisAlignment.start,
129-
children: [
130-
const DimmingHeroPanel(),
131-
Expanded(child: const EditorHost()),
132-
],
133-
);
135+
return const EditorHost();
134136
}
135137
}
136138

client/lib/devices/borneo/lyfi/views/moon_screen.dart

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,30 @@ class MoonScreen extends StatelessWidget {
6565
} else if (snapshot.hasError) {
6666
return Center(child: Text('Error: ${snapshot.error}'));
6767
} else {
68-
return Column(
69-
mainAxisSize: MainAxisSize.max,
70-
children: [
71-
AspectRatio(
72-
aspectRatio: 1.5,
73-
child: Consumer<MoonViewModel>(builder: (context, vm, _) => buildGraph(context, vm)),
74-
),
75-
const SizedBox(height: 24),
76-
Expanded(
77-
child: Consumer<MoonViewModel>(
78-
builder: (context, vm, _) => ScreenTopRoundedContainer(
79-
color: Theme.of(context).colorScheme.surfaceContainer,
80-
padding: EdgeInsets.fromLTRB(0, 24, 0, 24),
81-
child: BrightnessSliderList(vm.editor, disabled: !vm.enabled || !vm.canEdit),
68+
return LayoutBuilder(
69+
builder: (context, constraints) {
70+
return Column(
71+
spacing: 8,
72+
mainAxisSize: MainAxisSize.max,
73+
children: [
74+
SizedBox(
75+
height: 180,
76+
child: Consumer<MoonViewModel>(builder: (context, vm, _) => buildGraph(context, vm)),
8277
),
83-
),
84-
),
85-
],
78+
Expanded(
79+
child: Consumer<MoonViewModel>(
80+
builder: (context, vm, _) => ScreenTopRoundedContainer(
81+
color: Theme.of(context).colorScheme.surfaceContainer,
82+
padding: EdgeInsets.fromLTRB(0, 24, 0, 24),
83+
child: SingleChildScrollView(
84+
child: BrightnessSliderList(vm.editor, disabled: !vm.enabled || !vm.canEdit),
85+
),
86+
),
87+
),
88+
),
89+
],
90+
);
91+
},
8692
);
8793
}
8894
},

client/lib/devices/borneo/lyfi/views/settings_screen.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,6 @@ class SettingsScreen extends StatelessWidget {
353353
return;
354354
}
355355

356-
final deviceID = vm.deviceID;
357356
await vm.networkReset();
358357

359358
if (context.mounted) {

client/lib/features/devices/view_models/device_discovery_view_model.dart

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -321,24 +321,6 @@ class DeviceDiscoveryViewModel extends AbstractScreenViewModel {
321321
}
322322
}
323323

324-
/// Attempts to resolve a set of BLE names using [IBleProvisioner.fetchDeviceInfo].
325-
///
326-
/// Each result is stored in [_resolvedDeviceNames] and triggers a UI refresh.
327-
Future<void> _resolveDeviceNames(List<String> names) async {
328-
for (var name in names) {
329-
if (_scanCancelToken.isCancelled) return;
330-
try {
331-
final info = await _bleProvisioner.fetchDeviceInfo(deviceName: name, cancelToken: _scanCancelToken);
332-
if (info.name.isNotEmpty) {
333-
_resolvedDeviceNames[name] = info.name;
334-
_updateDiscoverableList();
335-
}
336-
} catch (e, st) {
337-
_logger.w('Failed to resolve name for $name', error: e, stackTrace: st);
338-
}
339-
}
340-
}
341-
342324
Future<void> addNewDevice(SupportedDeviceDescriptor deviceInfo) async {
343325
await _deviceManager.addNewDevice(deviceInfo, groupID: null);
344326
}

client/lib/shared/widgets/app_bar_apply_button.dart

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,25 @@ class AppBarApplyButton extends StatelessWidget {
1414
/// Callback when pressed.
1515
final VoidCallback? onPressed;
1616

17-
/// Whether the button is enabled.
18-
final bool enabled;
19-
2017
/// Custom text style (optional).
2118
final TextStyle? labelStyle;
2219

23-
const AppBarApplyButton({super.key, this.onPressed, this.enabled = true, this.labelStyle, this.label, this.icon})
20+
const AppBarApplyButton({super.key, this.onPressed, this.labelStyle, this.label, this.icon})
2421
: assert(label != null || icon != null, 'label or icon must be provided');
2522

2623
bool get _isIOS => Platform.isIOS || Platform.isMacOS;
2724

2825
@override
2926
Widget build(BuildContext context) {
3027
final theme = Theme.of(context);
31-
final color = enabled
28+
final color = onPressed != null
3229
? (_isIOS ? CupertinoColors.systemBlue : theme.colorScheme.primary)
3330
: (_isIOS ? CupertinoColors.inactiveGray : theme.disabledColor);
3431

3532
if (label == null && icon != null) {
3633
return IconButton(
3734
icon: Icon(icon, color: color),
38-
onPressed: enabled ? onPressed : null,
35+
onPressed: onPressed,
3936
);
4037
}
4138

@@ -59,13 +56,13 @@ class AppBarApplyButton extends StatelessWidget {
5956
if (_isIOS) {
6057
return CupertinoButton(
6158
padding: const EdgeInsets.symmetric(horizontal: 16),
62-
onPressed: enabled ? onPressed : null,
59+
onPressed: onPressed,
6360
child: Text(label!, style: textStyle),
6461
);
6562
}
6663

6764
return TextButton(
68-
onPressed: enabled ? onPressed : null,
65+
onPressed: onPressed,
6966
child: Text(label!.toUpperCase(), style: textStyle),
7067
);
7168
}
@@ -78,7 +75,7 @@ class AppBarApplyButton extends StatelessWidget {
7875
if (_isIOS) {
7976
return CupertinoButton(
8077
padding: const EdgeInsets.symmetric(horizontal: 12),
81-
onPressed: enabled ? onPressed : null,
78+
onPressed: onPressed,
8279
child: Row(
8380
mainAxisSize: MainAxisSize.min,
8481
children: [
@@ -91,7 +88,7 @@ class AppBarApplyButton extends StatelessWidget {
9188
}
9289

9390
return TextButton.icon(
94-
onPressed: enabled ? onPressed : null,
91+
onPressed: onPressed,
9592
icon: Icon(icon, color: color, size: 18),
9693
label: Text(label!.toUpperCase(), style: textStyle),
9794
);

client/lib/shared/widgets/generic_bottom_sheet_picker.dart

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import 'package:borneo_app/shared/widgets/screen_top_rounded_container.dart';
22
import 'package:flutter/material.dart';
3-
import 'package:provider/provider.dart';
4-
5-
import '../../core/models/platform_device_info.dart';
63

74
/// An entry used by [GenericBottomSheetPicker].
85
///
@@ -85,14 +82,6 @@ class GenericBottomSheetPicker<T> extends StatelessWidget {
8582
? selectedValue
8683
: (entries.isNotEmpty ? entries.first.value : null);
8784

88-
// Clip the contents so any scrolling glow or background does not
89-
// overflow the rounded corners of the sheet. The radius is pulled from
90-
// the shared [PlatformDeviceInfo] provider so we match the actual device
91-
// corners; if for whatever reason the provider isn't available we fall
92-
// back to the previous hardcoded value.
93-
final info = Provider.of<PlatformDeviceInfo?>(context, listen: false);
94-
final double corner = info?.screenCornerRadius.topLeft ?? 16.0;
95-
9685
return ScreenTopRoundedContainer(
9786
child: Column(
9887
mainAxisSize: MainAxisSize.min,

0 commit comments

Comments
 (0)