import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/providers.dart'; import 'color_picker.dart'; /// Карточка одной группы ламп с управлением: /// вкл/выкл, яркость, температура, цвет, сцена. class GroupCard extends ConsumerStatefulWidget { final Map group; const GroupCard({super.key, required this.group}); @override ConsumerState createState() => _GroupCardState(); } class _GroupCardState extends ConsumerState { /// Текущий режим управления: temp (температура) или color (RGB) String _mode = 'temp'; // Локальные значения слайдеров -- обновляются мгновенно, // а на сервер отправляются через debounce в провайдере. double? _localBrightness; double? _localTemp; int _channelValue(double channel) => (channel * 255.0).round().clamp(0, 255); @override Widget build(BuildContext context) { final g = widget.group; final id = g['id'].toString(); final name = g['name'] ?? 'Без имени'; final bool isOn = g['last_state']?['state'] ?? false; final int bri = g['last_state']?['brightness'] ?? 100; final int temp = g['last_state']?['temp'] ?? 4000; final int r = g['last_state']?['r'] ?? 255; 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, (tempValue - 2700) / 3800)! : Color.fromARGB(255, r, gVal, b)) : Colors.white12; return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), child: AnimatedContainer( duration: const Duration(milliseconds: 300), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: isOn ? Border.all( color: cardAccent.withValues(alpha: 0.3), width: 1, ) : null, ), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // ─── Заголовок + переключатель ─── Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( name, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, color: isOn ? Colors.white : Colors.white54, ), ), ), // Кнопка "таймер на 4 часа" if (isOn) IconButton( icon: const Icon(Icons.timer, size: 20, color: Colors.white38), tooltip: 'Включить на 4 часа', onPressed: () => ref.read(groupsProvider.notifier).setTimer4h(id), ), Switch( value: isOn, activeThumbColor: Colors.deepOrange, onChanged: (v) => ref.read(groupsProvider.notifier).toggleGroup(id, v), ), ], ), // ─── Управление (когда включено) ─── if (isOn) ...[ const SizedBox(height: 8), // Яркость _SliderRow( icon: Icons.sunny, value: briValue, min: 10, max: 100, divisions: 9, label: "${briValue.toInt()}%", activeColor: Colors.amber, onChanged: (v) { setState(() => _localBrightness = v); ref.read(groupsProvider.notifier).setBrightness(id, v.toInt()); }, onChangeEnd: (v) { setState(() => _localBrightness = null); }, ), // Переключатель режима: температура / цвет / сцена Row( children: [ _ModeChip( label: 'Темп.', icon: Icons.wb_twilight, selected: _mode == 'temp', onTap: () => setState(() => _mode = 'temp'), ), const SizedBox(width: 8), _ModeChip( label: 'Цвет', icon: Icons.palette, selected: _mode == 'color', onTap: () => setState(() => _mode = 'color'), ), const SizedBox(width: 8), _ModeChip( label: 'Сцена', icon: Icons.auto_awesome, selected: _mode == 'scene', onTap: () => setState(() => _mode = 'scene'), ), ], ), const SizedBox(height: 8), // ─── Режим: температура ─── if (_mode == 'temp') _SliderRow( icon: Icons.wb_twilight, value: tempValue, min: 2700, max: 6500, divisions: 38, // шаг 100K label: "${tempValue.toInt()}K", activeColor: Color.lerp( 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) { // Обновление UI-превью -- через debounce отправляется на сервер ref.read(groupsProvider.notifier).setColor( id, _channelValue(c.r), _channelValue(c.g), _channelValue(c.b), ); }, ), // ─── Режим: сцена ─── if (_mode == 'scene') _SceneSelector(groupId: id), ], ], ), ), ), ); } } /// Слайдер с иконкой и подписью class _SliderRow extends StatelessWidget { final IconData icon; final double value; final double min; final double max; final int divisions; final String label; final Color? activeColor; final ValueChanged onChanged; final ValueChanged? onChangeEnd; const _SliderRow({ required this.icon, required this.value, required this.min, required this.max, required this.divisions, required this.label, this.activeColor, required this.onChanged, this.onChangeEnd, }); @override Widget build(BuildContext context) { return Row( children: [ Icon(icon, size: 18, color: Colors.white54), Expanded( child: Slider( value: value, min: min, max: max, divisions: divisions, label: label, activeColor: activeColor, onChanged: onChanged, onChangeEnd: onChangeEnd, ), ), SizedBox( width: 50, child: Text( label, style: const TextStyle(fontSize: 12, color: Colors.white54), textAlign: TextAlign.right, ), ), ], ); } } /// Чип переключения режима class _ModeChip extends StatelessWidget { final String label; final IconData icon; final bool selected; final VoidCallback onTap; const _ModeChip({ required this.label, required this.icon, required this.selected, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: selected ? Colors.deepOrange.withValues(alpha: 0.2) : Colors.white10, borderRadius: BorderRadius.circular(20), border: selected ? Border.all(color: Colors.deepOrange, width: 1) : Border.all(color: Colors.transparent), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 14, color: selected ? Colors.deepOrange : Colors.white54), const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 12, color: selected ? Colors.deepOrange : Colors.white54, ), ), ], ), ), ); } } /// Выбор сцены из списка, загруженного с сервера class _SceneSelector extends ConsumerStatefulWidget { final String groupId; const _SceneSelector({required this.groupId}); @override 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 && !_loadStarted) { // Загрузить сцены при первом показе _loadStarted = true; Future.microtask(() => ref.read(scenesProvider.notifier).load()); return const Padding( padding: EdgeInsets.all(8.0), child: Center( child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ), ), ); } 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) { 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, style: const TextStyle(fontSize: 12)), backgroundColor: Colors.white10, onPressed: () => ref .read(groupsProvider.notifier) .setScene(widget.groupId, sceneId), ); }).toList(), ); } }