11import 'package:borneo_app/core/services/local_service.dart' ;
2+ import 'package:borneo_app/core/utils/hex_color.dart' ;
3+ import 'package:borneo_app/devices/borneo/lyfi/view_models/constants.dart' ;
24import 'package:borneo_app/devices/borneo/lyfi/view_models/lyfi_view_model.dart' ;
35import 'package:borneo_app/devices/borneo/lyfi/view_models/summary_device_view_model.dart' ;
46import 'package:borneo_app/devices/borneo/lyfi/views/lyfi_view.dart' ;
@@ -10,6 +12,7 @@ import 'package:borneo_app/core/services/app_notification_service.dart';
1012import 'package:borneo_kernel/drivers/borneo/lyfi/models.dart' ;
1113import 'package:borneo_wot/borneo/lyfi/wot_thing.dart' ;
1214import 'package:event_bus/event_bus.dart' ;
15+ import 'package:fl_chart/fl_chart.dart' ;
1316import 'package:flutter/material.dart' ;
1417import 'package:flutter_gettext/flutter_gettext.dart' ;
1518import 'package:logger/logger.dart' ;
@@ -38,6 +41,7 @@ class LyfiDeviceModuleMetadata extends DeviceModuleMetadata {
3841 deviceIconBuilder: _buildDeviceIcon,
3942 primaryStateIconBuilder: _buildPrimaryStateIcon,
4043 secondaryStatesBuilder: _secondaryStatesBuilder,
44+ summaryContentBuilder: _buildCardCenter,
4145 createSummaryVM: (dev, dm, bus, gt) => LyfiSummaryDeviceViewModel (dev, dm, bus, gt: gt),
4246 createWotThing: _createWotThing,
4347 );
@@ -47,8 +51,8 @@ class LyfiDeviceModuleMetadata extends DeviceModuleMetadata {
4751 Icons .light_outlined,
4852 size: iconSize,
4953 color: isOnline
50- ? Theme .of (context).colorScheme.onPrimaryContainer
51- : Theme .of (context).colorScheme.onPrimaryContainer .withValues (alpha: 0.38 ),
54+ ? Theme .of (context).colorScheme.primary
55+ : Theme .of (context).colorScheme.primary .withValues (alpha: 0.38 ),
5256 );
5357 }
5458
@@ -70,6 +74,40 @@ class LyfiDeviceModuleMetadata extends DeviceModuleMetadata {
7074 return [modeWidget, stateWidget];
7175 }
7276
77+ /// Custom card center: bar chart of per-channel brightness.
78+ /// Falls back to large device icon when offline, powered off, or data unavailable.
79+ static Widget _buildCardCenter (BuildContext context, AbstractDeviceSummaryViewModel vm) {
80+ final lvm = vm as LyfiSummaryDeviceViewModel ;
81+ return ValueListenableBuilder <LyfiDeviceInfo ?>(
82+ valueListenable: lvm.lyfiDeviceInfo,
83+ builder: (context, deviceInfo, _) {
84+ return ValueListenableBuilder <List <int >?>(
85+ valueListenable: lvm.channelBrightness,
86+ builder: (context, brightness, _) {
87+ // Show large icon when offline, powered off, or data not yet available
88+ final showIcon =
89+ ! lvm.isOnline ||
90+ ! lvm.isPowerOn ||
91+ deviceInfo == null ||
92+ brightness == null ||
93+ deviceInfo.channels.isEmpty;
94+ if (showIcon) {
95+ return Center (
96+ child: LayoutBuilder (
97+ builder: (context, constraints) {
98+ final iconSize = constraints.maxHeight * 0.72 ;
99+ return _buildDeviceIcon (context, iconSize, lvm.isOnline);
100+ },
101+ ),
102+ );
103+ }
104+ return _LyfiBrightnessChart (deviceInfo: deviceInfo, brightness: brightness);
105+ },
106+ );
107+ },
108+ );
109+ }
110+
73111 static String _modeText (BuildContext context, LyfiMode ? mode) {
74112 switch (mode) {
75113 case LyfiMode .manual:
@@ -104,3 +142,166 @@ class LyfiDeviceModuleMetadata extends DeviceModuleMetadata {
104142 return lyfiThing;
105143 }
106144}
145+
146+ /// A compact bar chart that displays Lyfi per-channel brightness.
147+ /// For a single channel, renders a circular progress indicator instead.
148+ class _LyfiBrightnessChart extends StatelessWidget {
149+ final LyfiDeviceInfo deviceInfo;
150+ final List <int > brightness;
151+
152+ const _LyfiBrightnessChart ({required this .deviceInfo, required this .brightness});
153+
154+ @override
155+ Widget build (BuildContext context) {
156+ final channelCount = deviceInfo.channels.length.clamp (0 , brightness.length);
157+ if (channelCount == 1 ) {
158+ return _buildSingleChannelGauge (context, channelCount);
159+ }
160+ return _buildBarChart (context, channelCount);
161+ }
162+
163+ Widget _buildSingleChannelGauge (BuildContext context, int channelCount) {
164+ final ch = deviceInfo.channels[0 ];
165+ final value = brightness[0 ];
166+ final fraction = (value / lyfiBrightnessMax).clamp (0.0 , 1.0 ).toDouble ();
167+ final pct = (fraction * 100 ).round ();
168+ final primaryColor = HexColor .fromHex (ch.color);
169+ final trackColor = Theme .of (context).colorScheme.surfaceContainerLow;
170+ return Center (
171+ child: LayoutBuilder (
172+ builder: (context, constraints) {
173+ final size = constraints.biggest.shortestSide;
174+ return SizedBox (
175+ width: size,
176+ height: size,
177+ child: Stack (
178+ alignment: Alignment .center,
179+ children: [
180+ CircularProgressIndicator (
181+ value: fraction,
182+ strokeWidth: size * 0.09 ,
183+ backgroundColor: trackColor,
184+ valueColor: AlwaysStoppedAnimation <Color >(primaryColor),
185+ strokeCap: StrokeCap .round,
186+ ),
187+ Column (
188+ mainAxisSize: MainAxisSize .min,
189+ children: [
190+ Text (
191+ '$pct %' ,
192+ style: Theme .of (context).textTheme.labelLarge? .copyWith (
193+ fontSize: (size * 0.22 ).clamp (12.0 , 22.0 ),
194+ fontWeight: FontWeight .bold,
195+ color: primaryColor,
196+ ),
197+ ),
198+ Text (
199+ ch.name,
200+ style: Theme .of (context).textTheme.labelSmall? .copyWith (
201+ fontSize: (size * 0.13 ).clamp (8.0 , 13.0 ),
202+ color: Theme .of (context).colorScheme.onSurface.withValues (alpha: 0.6 ),
203+ ),
204+ ),
205+ ],
206+ ),
207+ ],
208+ ),
209+ );
210+ },
211+ ),
212+ );
213+ }
214+
215+ Widget _buildBarChart (BuildContext context, int channelCount) {
216+ final colorScheme = Theme .of (context).colorScheme;
217+ final barBackColor = colorScheme.surfaceContainerLow;
218+
219+ // Adaptive bar width: shrink as channel count grows
220+ final barWidth =
221+ (channelCount <= 4
222+ ? 18.0
223+ : channelCount <= 6
224+ ? 13.0
225+ : channelCount <= 8
226+ ? 10.0
227+ : 7.0 )
228+ .toDouble ();
229+
230+ // Label: abbreviate to fit — fewer chars for many channels
231+ final maxLabelLen = channelCount <= 4
232+ ? 4
233+ : channelCount <= 6
234+ ? 3
235+ : 2 ;
236+
237+ final groups = < BarChartGroupData > [];
238+ for (int i = 0 ; i < channelCount; i++ ) {
239+ final ch = deviceInfo.channels[i];
240+ final value = brightness[i].toDouble ();
241+ final primaryColor = HexColor .fromHex (ch.color);
242+ final lightStart = Color .lerp (primaryColor, Colors .white, 0.7 )! ;
243+ final fraction = (value / lyfiBrightnessMax).clamp (0.0 , 1.0 );
244+ final currentEndColor = Color .lerp (lightStart, primaryColor, fraction)! ;
245+
246+ groups.add (
247+ BarChartGroupData (
248+ x: i,
249+ barRods: [
250+ BarChartRodData (
251+ toY: value,
252+ borderRadius: BorderRadius .circular (4 ),
253+ gradient: LinearGradient (
254+ begin: Alignment .bottomCenter,
255+ end: Alignment .topCenter,
256+ colors: [lightStart, currentEndColor],
257+ ),
258+ width: barWidth,
259+ backDrawRodData: BackgroundBarChartRodData (
260+ show: true ,
261+ fromY: 0 ,
262+ toY: lyfiBrightnessMax.toDouble (),
263+ color: barBackColor,
264+ ),
265+ ),
266+ ],
267+ ),
268+ );
269+ }
270+
271+ return BarChart (
272+ BarChartData (
273+ barGroups: groups,
274+ maxY: lyfiBrightnessMax.toDouble (),
275+ groupsSpace: channelCount > 6 ? 4 : 8 ,
276+ titlesData: FlTitlesData (
277+ show: true ,
278+ bottomTitles: AxisTitles (
279+ sideTitles: SideTitles (
280+ showTitles: true ,
281+ reservedSize: 14 ,
282+ getTitlesWidget: (value, _) {
283+ final idx = value.toInt ();
284+ if (idx < 0 || idx >= deviceInfo.channels.length) return const SizedBox .shrink ();
285+ final ch = deviceInfo.channels[idx];
286+ final label = ch.name.length > maxLabelLen ? ch.name.substring (0 , maxLabelLen) : ch.name;
287+ return Text (
288+ label,
289+ style: Theme .of (context).textTheme.labelSmall? .copyWith (
290+ fontSize: channelCount > 6 ? 8.0 : 9.0 ,
291+ color: colorScheme.onSurface.withValues (alpha: 0.7 ),
292+ ),
293+ );
294+ },
295+ ),
296+ ),
297+ leftTitles: const AxisTitles (sideTitles: SideTitles (showTitles: false )),
298+ topTitles: const AxisTitles (sideTitles: SideTitles (showTitles: false )),
299+ rightTitles: const AxisTitles (sideTitles: SideTitles (showTitles: false )),
300+ ),
301+ borderData: FlBorderData (show: false ),
302+ gridData: const FlGridData (show: false ),
303+ barTouchData: BarTouchData (enabled: false ),
304+ ),
305+ );
306+ }
307+ }
0 commit comments