Files
ignis_app/lib/screens/group_edit_screen.dart
2026-04-23 20:24:08 +07:00

325 lines
10 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 '../app/error_message.dart';
import '../app/load_state.dart';
import '../providers/providers.dart';
import '../widgets/load_error_view.dart';
/// Экран создания новой группы ламп.
/// Загружает список устройств, позволяет выбрать нужные.
class GroupEditScreen extends ConsumerStatefulWidget {
const GroupEditScreen({super.key});
@override
ConsumerState<GroupEditScreen> createState() => _GroupEditScreenState();
}
class _GroupEditScreenState extends ConsumerState<GroupEditScreen> {
final _idCtrl = TextEditingController();
final _nameCtrl = TextEditingController();
final Set<String> _selectedMacs = {};
bool _saving = false;
bool _rescanning = false;
@override
void initState() {
super.initState();
_loadDevices();
}
@override
void dispose() {
_idCtrl.dispose();
_nameCtrl.dispose();
super.dispose();
}
Future<void> _loadDevices() async {
await ref.read(devicesProvider.notifier).load();
}
/// Пересканировать сеть и перезагрузить устройства
Future<void> _rescan() async {
setState(() => _rescanning = true);
try {
await ref.read(apiProvider).rescanNetwork();
// Подождать немного -- сканирование асинхронное
await Future.delayed(const Duration(seconds: 3));
await ref.read(devicesProvider.notifier).load();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Ошибка сканирования: ${describeLoadError(e)}'),
),
);
}
} finally {
if (mounted) setState(() => _rescanning = false);
}
}
@override
Widget build(BuildContext context) {
final devicesState = ref.watch(devicesProvider);
final devices = devicesState.data;
return Scaffold(
appBar: AppBar(
title: const Text('НОВАЯ ГРУППА'),
actions: [
// Кнопка ресканирования сети
IconButton(
icon: _rescanning
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.refresh),
tooltip: 'Пересканировать сеть',
onPressed: _rescanning ? null : _rescan,
),
],
),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ID группы
TextField(
controller: _idCtrl,
decoration: const InputDecoration(
labelText: 'ID группы (например "bedroom")',
prefixIcon: Icon(Icons.tag),
),
),
const SizedBox(height: 12),
// Название группы
TextField(
controller: _nameCtrl,
decoration: const InputDecoration(
labelText: 'Название (например "Спальня")',
prefixIcon: Icon(Icons.label),
),
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 16),
// Заголовок списка устройств
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Устройства (${_selectedMacs.length} выбрано)',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.white70,
),
),
TextButton(
onPressed: devices.isEmpty
? null
: () {
setState(() {
if (_selectedMacs.length == devices.length) {
_selectedMacs.clear();
} else {
for (final d in devices) {
final mac = _extractMac(d);
if (mac != null) _selectedMacs.add(mac);
}
}
});
},
child: Text(
_selectedMacs.length == devices.length
? 'Снять все'
: 'Выбрать все',
style: const TextStyle(fontSize: 12),
),
),
],
),
// Список устройств
Expanded(child: _buildDevices(devicesState, devices)),
// Кнопка сохранения
Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).padding.bottom + 8,
),
child: SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.deepOrange,
foregroundColor: Colors.white,
),
onPressed: _saving ? null : _save,
child: _saving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text('СОЗДАТЬ ГРУППУ'),
),
),
),
],
),
),
);
}
Widget _buildDevices(
LoadState<List<dynamic>> devicesState,
List<dynamic> devices,
) {
if ((devicesState.isIdle || devicesState.isLoading) && devices.isEmpty) {
return const Center(
child: CircularProgressIndicator(color: Colors.deepOrange),
);
}
if (devicesState.hasError && devices.isEmpty) {
return LoadErrorView(
title: 'Не удалось загрузить устройства',
message: devicesState.errorMessage,
icon: Icons.lightbulb_outline,
onRetry: _loadDevices,
);
}
if (devices.isEmpty) {
return const Center(
child: Text(
'Устройства не найдены.\nПопробуйте пересканировать сеть.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.white38),
),
);
}
final hasStatusHeader = devicesState.isLoading || devicesState.hasError;
final statusHeaderCount = hasStatusHeader ? 1 : 0;
return ListView.builder(
itemCount: devices.length + statusHeaderCount,
itemBuilder: (context, index) {
if (hasStatusHeader && index == 0) {
if (devicesState.isLoading) {
return const Padding(
padding: EdgeInsets.only(bottom: 12),
child: LinearProgressIndicator(color: Colors.deepOrange),
);
}
return LoadErrorBanner(
title: 'Не удалось обновить устройства',
message: devicesState.errorMessage,
onRetry: _loadDevices,
);
}
final deviceIndex = index - statusHeaderCount;
final d = devices[deviceIndex];
final mac = _extractMac(d) ?? '';
final name = _extractName(d);
final ip = _extractIp(d);
final selected = _selectedMacs.contains(mac);
return CheckboxListTile(
value: selected,
activeColor: Colors.deepOrange,
title: Text(name),
subtitle: Text(
'$mac${ip != null ? ' - $ip' : ''}',
style: const TextStyle(fontSize: 11, color: Colors.white38),
),
onChanged: (v) {
setState(() {
if (v == true) {
_selectedMacs.add(mac);
} else {
_selectedMacs.remove(mac);
}
});
},
);
},
);
}
/// Извлечь MAC-адрес из объекта устройства
String? _extractMac(dynamic device) {
if (device is Map) {
return (device['mac'] ?? device['id'] ?? device['mac_address'])
?.toString();
}
return device?.toString();
}
/// Извлечь имя устройства
String _extractName(dynamic device) {
if (device is Map) {
return (device['name'] ?? device['model'] ?? device['mac'] ?? 'Лампа')
.toString();
}
return device?.toString() ?? 'Лампа';
}
/// Извлечь IP-адрес
String? _extractIp(dynamic device) {
if (device is Map) {
return (device['ip'] ?? device['address'])?.toString();
}
return null;
}
Future<void> _save() async {
final id = _idCtrl.text.trim();
final name = _nameCtrl.text.trim();
if (id.isEmpty || name.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('Укажите ID и название')));
return;
}
if (_selectedMacs.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Выберите хотя бы одно устройство')),
);
return;
}
setState(() => _saving = true);
try {
await ref.read(apiProvider).createGroup(id, name, _selectedMacs.toList());
// Обновить список групп
await ref.read(groupsProvider.notifier).refresh();
if (mounted) Navigator.of(context).pop();
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Ошибка создания: ${describeLoadError(e)}')),
);
}
}
if (mounted) setState(() => _saving = false);
}
}