import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/error_message.dart'; import '../features/homes/services/home_connection_change.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 _saving = false; bool _loadingApiKey = false; bool _hasStoredApiKey = false; String _originalApiKey = ''; 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(); } _loadingApiKey = true; _loadApiKey(); } // Следим за полями координат, чтобы обновлять подсказки экрана. _latCtrl.addListener(_onCoordsChanged); _lonCtrl.addListener(_onCoordsChanged); } Future _loadApiKey() async { try { final apiKey = await ref .read(settingsServiceProvider) .getHomeApiKey(widget.home!.id); _originalApiKey = apiKey?.trim() ?? ''; _hasStoredApiKey = _originalApiKey.isNotEmpty; if (!mounted) { return; } } finally { if (mounted) { setState(() => _loadingApiKey = false); } } } void _onCoordsChanged() { setState(() {}); } @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: InputDecoration( labelText: 'API Key', hintText: _hasStoredApiKey ? 'Оставьте пустым, чтобы сохранить текущий ключ' : null, helperText: _loadingApiKey ? 'Загружаем сохранённый ключ...' : _hasStoredApiKey ? 'Сохранённый ключ хранится отдельно и не показывается в поле' : 'Ключ хранится отдельно в защищённом хранилище', prefixIcon: const Icon(Icons.key), ), obscureText: true, validator: (value) { final enteredKey = value?.trim() ?? ''; if (enteredKey.isNotEmpty || _hasStoredApiKey) { return null; } return 'Укажите API key'; }, ), 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), if (_hasCoordinates) const Padding( padding: EdgeInsets.only(bottom: 24), child: Text( 'Geofence и радиус настраиваются отдельно на экране настроек.', style: TextStyle(fontSize: 12, color: Colors.white38), ), ) else const SizedBox(height: 24), SizedBox( width: double.infinity, height: 48, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.deepOrange, foregroundColor: Colors.white, ), onPressed: (_saving || _loadingApiKey) ? 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 (_loadingApiKey) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Подождите, API key ещё загружается')), ); return; } if (!_formKey.currentState!.validate()) { return; } final name = _nameCtrl.text.trim(); final rawUrl = _urlCtrl.text.trim(); final enteredKey = _keyCtrl.text.trim(); final key = enteredKey.isNotEmpty ? enteredKey : _originalApiKey; 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 credentialsChanged = hasHomeConnectionChanges( originalHome: widget.home, normalizedUrl: url, apiKey: key, originalApiKey: _originalApiKey, ); final home = _isEdit ? widget.home!.copyWith( name: name, url: url, latitude: lat, longitude: lon, clearCoordinates: clearCoords, ) : HomeConfig( id: DateTime.now().millisecondsSinceEpoch.toString(), name: name, url: url, latitude: lat, longitude: lon, ); try { if (credentialsChanged) { await ref.read(apiProvider).validateCredentials(url, key); } if (_isEdit) { await ref .read(homesProvider.notifier) .update(home, apiKey: enteredKey.isNotEmpty ? key : null); } else { await ref.read(homesProvider.notifier).add(home, apiKey: key); } final currentHome = ref.read(currentHomeProvider); if (currentHome?.id == home.id) { if (credentialsChanged) { await ref.read(currentHomeProvider.notifier).select(home); } else { await ref.read(currentHomeProvider.notifier).switchTo(home); } } 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); } } }