import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/error_message.dart'; import '../models/home_config.dart'; import '../providers/providers.dart'; import '../services/api_client.dart'; /// Экран создания или редактирования "дома" (сервера Ignis). class HomeEditScreen extends ConsumerStatefulWidget { final HomeConfig? home; // null -- создание, иначе редактирование const HomeEditScreen({super.key, this.home}); @override ConsumerState createState() => _HomeEditScreenState(); } class _HomeEditScreenState extends ConsumerState { final _formKey = GlobalKey(); final _nameCtrl = TextEditingController(); final _urlCtrl = TextEditingController(); final _keyCtrl = TextEditingController(); final _latCtrl = TextEditingController(); final _lonCtrl = TextEditingController(); bool _geofenceEnabled = false; bool _saving = false; bool get _isEdit => widget.home != null; /// Координаты заполнены (оба поля непустые) bool get _hasCoordinates => _latCtrl.text.trim().isNotEmpty && _lonCtrl.text.trim().isNotEmpty; @override void initState() { super.initState(); if (_isEdit) { _nameCtrl.text = widget.home!.name; _urlCtrl.text = widget.home!.url; if (widget.home!.latitude != null) { _latCtrl.text = widget.home!.latitude.toString(); } if (widget.home!.longitude != null) { _lonCtrl.text = widget.home!.longitude.toString(); } _geofenceEnabled = widget.home!.geofenceEnabled; _loadApiKey(); } // Следим за полями координат чтобы обновлять доступность Switch _latCtrl.addListener(_onCoordsChanged); _lonCtrl.addListener(_onCoordsChanged); } Future _loadApiKey() async { final apiKey = await ref .read(settingsServiceProvider) .getHomeApiKey(widget.home!.id); if (mounted && apiKey != null) { _keyCtrl.text = apiKey; } } void _onCoordsChanged() { // Если координаты очистили -- выключаем геофенс if (!_hasCoordinates && _geofenceEnabled) { setState(() => _geofenceEnabled = false); } else { setState(() {}); // перерисовать Switch enabled/disabled } } @override void dispose() { _latCtrl.removeListener(_onCoordsChanged); _lonCtrl.removeListener(_onCoordsChanged); _nameCtrl.dispose(); _urlCtrl.dispose(); _keyCtrl.dispose(); _latCtrl.dispose(); _lonCtrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(_isEdit ? 'РЕДАКТИРОВАТЬ ДОМ' : 'НОВЫЙ ДОМ')), body: Form( key: _formKey, autovalidateMode: AutovalidateMode.onUserInteraction, child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ TextFormField( controller: _nameCtrl, decoration: const InputDecoration( labelText: 'Название', hintText: 'Например: Квартира', prefixIcon: Icon(Icons.home), ), textCapitalization: TextCapitalization.sentences, validator: (value) => (value?.trim().isEmpty ?? true) ? 'Укажите название дома' : null, ), const SizedBox(height: 12), TextFormField( controller: _urlCtrl, decoration: const InputDecoration( labelText: 'Адрес сервера', hintText: 'Например: ignis.akokos.ru', helperText: 'Можно без https, приложение подставит его само', prefixIcon: Icon(Icons.dns), ), keyboardType: TextInputType.url, validator: (value) { final rawUrl = value?.trim() ?? ''; if (rawUrl.isEmpty) { return 'Укажите адрес сервера'; } try { final normalized = IgnisApi.normalizeBaseUrl(rawUrl); final parsed = Uri.parse(normalized); if ((parsed.scheme != 'http' && parsed.scheme != 'https') || parsed.host.isEmpty) { return 'Некорректный адрес сервера'; } } catch (_) { return 'Некорректный адрес сервера'; } return null; }, ), const SizedBox(height: 12), TextFormField( controller: _keyCtrl, decoration: const InputDecoration( labelText: 'API Key', helperText: 'Ключ проверяется перед сохранением дома', prefixIcon: Icon(Icons.key), ), obscureText: true, validator: (value) => (value?.trim().isEmpty ?? true) ? 'Укажите API key' : null, ), const SizedBox(height: 24), const Text( 'Координаты дома (опционально)', style: TextStyle( color: Colors.white54, fontSize: 13, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 4), const Text( 'Нужны только для расстояния и автовыключения по геолокации', style: TextStyle(color: Colors.white30, fontSize: 12), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TextFormField( controller: _latCtrl, decoration: const InputDecoration( labelText: 'Широта', prefixIcon: Icon(Icons.location_on, size: 20), hintText: '51.128', ), keyboardType: const TextInputType.numberWithOptions( decimal: true, signed: true, ), validator: (value) { final latText = value?.trim() ?? ''; final lonText = _lonCtrl.text.trim(); if (latText.isEmpty && lonText.isEmpty) { return null; } if (latText.isEmpty || lonText.isEmpty) { return 'Введите обе координаты'; } final lat = double.tryParse(latText); if (lat == null || lat < -90 || lat > 90) { return 'От -90 до 90'; } return null; }, ), ), const SizedBox(width: 12), Expanded( child: TextFormField( controller: _lonCtrl, decoration: const InputDecoration( labelText: 'Долгота', prefixIcon: Icon(Icons.location_on, size: 20), hintText: '71.430', ), keyboardType: const TextInputType.numberWithOptions( decimal: true, signed: true, ), validator: (value) { final lonText = value?.trim() ?? ''; final latText = _latCtrl.text.trim(); if (latText.isEmpty && lonText.isEmpty) { return null; } if (latText.isEmpty || lonText.isEmpty) { return 'Введите обе координаты'; } final lon = double.tryParse(lonText); if (lon == null || lon < -180 || lon > 180) { return 'От -180 до 180'; } return null; }, ), ), ], ), const SizedBox(height: 16), SwitchListTile( title: const Text('Выключать свет при уходе'), subtitle: Text( _hasCoordinates ? 'Автовыключение при удалении на 500 м' : 'Задайте координаты для активации', style: TextStyle( fontSize: 12, color: _hasCoordinates ? Colors.white38 : Colors.white24, ), ), value: _geofenceEnabled, activeThumbColor: Colors.deepOrange, onChanged: _hasCoordinates ? (v) => setState(() => _geofenceEnabled = v) : null, contentPadding: EdgeInsets.zero, secondary: Icon( Icons.directions_walk, color: _geofenceEnabled && _hasCoordinates ? Colors.deepOrange : Colors.white24, ), ), if (_geofenceEnabled && _hasCoordinates) const Padding( padding: EdgeInsets.only(left: 40, bottom: 4), child: Text( 'Проверка раз в ~15 мин (ограничение Android).\n' 'Работает только для текущего активного дома.\n' 'Нужны фоновые разрешения на геолокацию и уведомления.', style: TextStyle(fontSize: 11, color: Colors.white24), ), ), const SizedBox(height: 24), 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, ), ) : Text(_isEdit ? 'СОХРАНИТЬ' : 'ДОБАВИТЬ'), ), ), SizedBox(height: MediaQuery.of(context).padding.bottom + 16), ], ), ), ), ); } Future _save() async { FocusScope.of(context).unfocus(); if (!_formKey.currentState!.validate()) { return; } final name = _nameCtrl.text.trim(); final rawUrl = _urlCtrl.text.trim(); final key = _keyCtrl.text.trim(); final latText = _latCtrl.text.trim(); final lonText = _lonCtrl.text.trim(); if (name.isEmpty || rawUrl.isEmpty || key.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Заполните все обязательные поля')), ); return; } late final String url; try { url = IgnisApi.normalizeBaseUrl(rawUrl); final parsed = Uri.parse(url); if ((parsed.scheme != 'http' && parsed.scheme != 'https') || parsed.host.isEmpty) { throw const FormatException(); } } catch (_) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Некорректный адрес сервера')), ); return; } double? lat; double? lon; if (latText.isEmpty != lonText.isEmpty) { ScaffoldMessenger.of( context, ).showSnackBar(const SnackBar(content: Text('Введите обе координаты'))); return; } if (latText.isNotEmpty && lonText.isNotEmpty) { lat = double.tryParse(latText); lon = double.tryParse(lonText); if (lat == null || lon == null) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Некорректные координаты')), ); return; } if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Координаты вне допустимого диапазона')), ); return; } } setState(() => _saving = true); final clearCoords = latText.isEmpty && lonText.isEmpty; final home = _isEdit ? widget.home!.copyWith( name: name, url: url, latitude: lat, longitude: lon, geofenceEnabled: clearCoords ? false : _geofenceEnabled, clearCoordinates: clearCoords, ) : HomeConfig( id: DateTime.now().millisecondsSinceEpoch.toString(), name: name, url: url, latitude: lat, longitude: lon, geofenceEnabled: _geofenceEnabled, ); try { await ref.read(apiProvider).validateCredentials(url, key); if (_isEdit) { await ref.read(homesProvider.notifier).update(home, apiKey: key); } else { await ref.read(homesProvider.notifier).add(home, apiKey: key); } final currentHome = ref.read(currentHomeProvider); if (currentHome?.id == home.id) { await ref.read(currentHomeProvider.notifier).select(home); } // Синхронизировать фоновый таск с новыми настройками final allHomes = ref.read(homesProvider); await syncGeofenceTask( allHomes, currentHome: ref.read(currentHomeProvider), ); if (mounted) Navigator.of(context).pop(); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Не удалось сохранить дом: ${describeLoadError(e)}'), ), ); } } finally { if (mounted) setState(() => _saving = false); } } }