Files
ignis_app/lib/widgets/group_card.dart
2026-04-22 21:08:02 +07:00

370 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String, dynamic> group;
const GroupCard({super.key, required this.group});
@override
ConsumerState<GroupCard> createState() => _GroupCardState();
}
class _GroupCardState extends ConsumerState<GroupCard> {
/// Текущий режим управления: 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<double> onChanged;
final ValueChanged<double>? 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(),
);
}
}