Waaaay big enchancements
This commit is contained in:
@@ -1,16 +1,21 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Простой цветовой пикер в виде кольца HSV.
|
||||
/// Простой цветовой пикер в виде HSV-слайдеров.
|
||||
/// Возвращает RGB через callback.
|
||||
///
|
||||
/// [onColorChanged] -- вызывается при каждом движении (для превью UI).
|
||||
/// [onColorChangeEnd] -- вызывается при отпускании слайдера (для отправки на сервер).
|
||||
class SimpleColorPicker extends StatefulWidget {
|
||||
final Color initialColor;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final ValueChanged<Color>? onColorChangeEnd;
|
||||
|
||||
const SimpleColorPicker({
|
||||
super.key,
|
||||
this.initialColor = Colors.red,
|
||||
required this.onColorChanged,
|
||||
this.onColorChangeEnd,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -34,6 +39,10 @@ class _SimpleColorPickerState extends State<SimpleColorPicker> {
|
||||
Color get _currentColor =>
|
||||
HSVColor.fromAHSV(1.0, _hue, _saturation, _value).toColor();
|
||||
|
||||
void _notifyEnd() {
|
||||
(widget.onColorChangeEnd ?? widget.onColorChanged)(_currentColor);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@@ -63,6 +72,7 @@ class _SimpleColorPickerState extends State<SimpleColorPicker> {
|
||||
setState(() => _hue = v);
|
||||
widget.onColorChanged(_currentColor);
|
||||
},
|
||||
onChangeEnd: (_) => _notifyEnd(),
|
||||
),
|
||||
|
||||
// Saturation -- насыщенность (0-1)
|
||||
@@ -77,6 +87,7 @@ class _SimpleColorPickerState extends State<SimpleColorPicker> {
|
||||
setState(() => _saturation = v);
|
||||
widget.onColorChanged(_currentColor);
|
||||
},
|
||||
onChangeEnd: (_) => _notifyEnd(),
|
||||
),
|
||||
|
||||
// Value -- яркость (0-1)
|
||||
@@ -91,6 +102,7 @@ class _SimpleColorPickerState extends State<SimpleColorPicker> {
|
||||
setState(() => _value = v);
|
||||
widget.onColorChanged(_currentColor);
|
||||
},
|
||||
onChangeEnd: (_) => _notifyEnd(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -104,6 +116,7 @@ class _SimpleColorPickerState extends State<SimpleColorPicker> {
|
||||
required int divisions,
|
||||
required Color activeColor,
|
||||
required ValueChanged<double> onChanged,
|
||||
ValueChanged<double>? onChangeEnd,
|
||||
}) {
|
||||
return Row(
|
||||
children: [
|
||||
@@ -119,6 +132,7 @@ class _SimpleColorPickerState extends State<SimpleColorPicker> {
|
||||
divisions: divisions,
|
||||
activeColor: activeColor,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -17,7 +17,11 @@ class GroupCard extends ConsumerStatefulWidget {
|
||||
class _GroupCardState extends ConsumerState<GroupCard> {
|
||||
/// Текущий режим управления: temp (температура) или color (RGB)
|
||||
String _mode = 'temp';
|
||||
bool _showColorPicker = false;
|
||||
|
||||
// Локальные значения слайдеров -- обновляются мгновенно,
|
||||
// а на сервер отправляются через debounce в провайдере.
|
||||
double? _localBrightness;
|
||||
double? _localTemp;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -31,10 +35,14 @@ class _GroupCardState extends ConsumerState<GroupCard> {
|
||||
final int gVal = g['last_state']?['g'] ?? 200;
|
||||
final int b = g['last_state']?['b'] ?? 100;
|
||||
|
||||
// Значения слайдеров: локальные (если тянем) или серверные
|
||||
final briValue = (_localBrightness ?? bri.toDouble()).clamp(10.0, 100.0);
|
||||
final tempValue = (_localTemp ?? temp.toDouble()).clamp(2700.0, 6500.0);
|
||||
|
||||
// Цвет подсветки карточки зависит от режима и состояния
|
||||
final cardAccent = isOn
|
||||
? (_mode == 'temp'
|
||||
? Color.lerp(Colors.orange, Colors.blueAccent, (temp - 2700) / 3800)!
|
||||
? Color.lerp(Colors.orange, Colors.blueAccent, (tempValue - 2700) / 3800)!
|
||||
: Color.fromARGB(255, r, gVal, b))
|
||||
: Colors.white12;
|
||||
|
||||
@@ -90,15 +98,19 @@ class _GroupCardState extends ConsumerState<GroupCard> {
|
||||
// Яркость
|
||||
_SliderRow(
|
||||
icon: Icons.sunny,
|
||||
value: bri.toDouble().clamp(10, 100),
|
||||
value: briValue,
|
||||
min: 10,
|
||||
max: 100,
|
||||
divisions: 9,
|
||||
label: "$bri%",
|
||||
label: "${briValue.toInt()}%",
|
||||
activeColor: Colors.amber,
|
||||
onChanged: (v) => ref
|
||||
.read(groupsProvider.notifier)
|
||||
.setBrightness(id, v.toInt()),
|
||||
onChanged: (v) {
|
||||
setState(() => _localBrightness = v);
|
||||
ref.read(groupsProvider.notifier).setBrightness(id, v.toInt());
|
||||
},
|
||||
onChangeEnd: (v) {
|
||||
setState(() => _localBrightness = null);
|
||||
},
|
||||
),
|
||||
|
||||
// Переключатель режима: температура / цвет / сцена
|
||||
@@ -108,30 +120,21 @@ class _GroupCardState extends ConsumerState<GroupCard> {
|
||||
label: 'Темп.',
|
||||
icon: Icons.wb_twilight,
|
||||
selected: _mode == 'temp',
|
||||
onTap: () => setState(() {
|
||||
_mode = 'temp';
|
||||
_showColorPicker = false;
|
||||
}),
|
||||
onTap: () => setState(() => _mode = 'temp'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ModeChip(
|
||||
label: 'Цвет',
|
||||
icon: Icons.palette,
|
||||
selected: _mode == 'color',
|
||||
onTap: () => setState(() {
|
||||
_mode = 'color';
|
||||
_showColorPicker = true;
|
||||
}),
|
||||
onTap: () => setState(() => _mode = 'color'),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
_ModeChip(
|
||||
label: 'Сцена',
|
||||
icon: Icons.auto_awesome,
|
||||
selected: _mode == 'scene',
|
||||
onTap: () => setState(() {
|
||||
_mode = 'scene';
|
||||
_showColorPicker = false;
|
||||
}),
|
||||
onTap: () => setState(() => _mode = 'scene'),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -141,25 +144,30 @@ class _GroupCardState extends ConsumerState<GroupCard> {
|
||||
if (_mode == 'temp')
|
||||
_SliderRow(
|
||||
icon: Icons.wb_twilight,
|
||||
value: temp.toDouble().clamp(2700, 6500),
|
||||
value: tempValue,
|
||||
min: 2700,
|
||||
max: 6500,
|
||||
divisions: 38, // шаг 100K
|
||||
label: "${temp}K",
|
||||
label: "${tempValue.toInt()}K",
|
||||
activeColor: Color.lerp(
|
||||
Colors.orange, Colors.blueAccent, (temp - 2700) / 3800),
|
||||
onChanged: (v) => ref
|
||||
.read(groupsProvider.notifier)
|
||||
.setTemperature(id, v.toInt()),
|
||||
Colors.orange, Colors.blueAccent, (tempValue - 2700) / 3800),
|
||||
onChanged: (v) {
|
||||
setState(() => _localTemp = v);
|
||||
ref.read(groupsProvider.notifier).setTemperature(id, v.toInt());
|
||||
},
|
||||
onChangeEnd: (v) {
|
||||
setState(() => _localTemp = null);
|
||||
},
|
||||
),
|
||||
|
||||
// ─── Режим: цвет ───
|
||||
if (_mode == 'color')
|
||||
SimpleColorPicker(
|
||||
initialColor: Color.fromARGB(255, r, gVal, b),
|
||||
onColorChanged: (c) => ref
|
||||
.read(groupsProvider.notifier)
|
||||
.setColor(id, c.red, c.green, c.blue),
|
||||
onColorChanged: (c) {
|
||||
// Обновление UI-превью -- через debounce отправляется на сервер
|
||||
ref.read(groupsProvider.notifier).setColor(id, c.red, c.green, c.blue);
|
||||
},
|
||||
),
|
||||
|
||||
// ─── Режим: сцена ───
|
||||
@@ -183,6 +191,7 @@ class _SliderRow extends StatelessWidget {
|
||||
final String label;
|
||||
final Color? activeColor;
|
||||
final ValueChanged<double> onChanged;
|
||||
final ValueChanged<double>? onChangeEnd;
|
||||
|
||||
const _SliderRow({
|
||||
required this.icon,
|
||||
@@ -193,6 +202,7 @@ class _SliderRow extends StatelessWidget {
|
||||
required this.label,
|
||||
this.activeColor,
|
||||
required this.onChanged,
|
||||
this.onChangeEnd,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -209,6 +219,7 @@ class _SliderRow extends StatelessWidget {
|
||||
label: label,
|
||||
activeColor: activeColor,
|
||||
onChanged: onChanged,
|
||||
onChangeEnd: onChangeEnd,
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
@@ -272,17 +283,25 @@ class _ModeChip extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Выбор сцены из списка, загруженного с сервера
|
||||
class _SceneSelector extends ConsumerWidget {
|
||||
class _SceneSelector extends ConsumerStatefulWidget {
|
||||
final String groupId;
|
||||
|
||||
const _SceneSelector({required this.groupId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<_SceneSelector> createState() => _SceneSelectorState();
|
||||
}
|
||||
|
||||
class _SceneSelectorState extends ConsumerState<_SceneSelector> {
|
||||
bool _loadStarted = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final scenes = ref.watch(scenesProvider);
|
||||
|
||||
if (scenes.isEmpty) {
|
||||
if (scenes.isEmpty && !_loadStarted) {
|
||||
// Загрузить сцены при первом показе
|
||||
_loadStarted = true;
|
||||
Future.microtask(() => ref.read(scenesProvider.notifier).load());
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
@@ -296,24 +315,40 @@ class _SceneSelector extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (scenes.isEmpty) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Сцены не найдены',
|
||||
style: TextStyle(color: Colors.white38, fontSize: 12),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: scenes.map((scene) {
|
||||
// Сцена может быть строкой или Map с полем 'name'/'id'
|
||||
final sceneName = scene is String
|
||||
? scene
|
||||
: (scene['name'] ?? scene['id'] ?? scene.toString());
|
||||
final sceneId = scene is String
|
||||
? scene
|
||||
: (scene['id'] ?? scene['name'] ?? scene.toString());
|
||||
String sceneName;
|
||||
String sceneId;
|
||||
|
||||
if (scene is String) {
|
||||
sceneName = scene;
|
||||
sceneId = scene;
|
||||
} else if (scene is Map) {
|
||||
sceneName = (scene['name'] ?? scene['id'] ?? scene.toString()).toString();
|
||||
sceneId = (scene['id'] ?? scene['name'] ?? scene.toString()).toString();
|
||||
} else {
|
||||
sceneName = scene.toString();
|
||||
sceneId = scene.toString();
|
||||
}
|
||||
|
||||
return ActionChip(
|
||||
label: Text(sceneName.toString(), style: const TextStyle(fontSize: 12)),
|
||||
label: Text(sceneName, style: const TextStyle(fontSize: 12)),
|
||||
backgroundColor: Colors.white10,
|
||||
onPressed: () => ref
|
||||
.read(groupsProvider.notifier)
|
||||
.setScene(groupId, sceneId.toString()),
|
||||
.setScene(widget.groupId, sceneId),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user