import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../app/build_info.dart'; import '../app/error_message.dart'; import '../app/load_state.dart'; import '../features/settings/models/app_theme_preset.dart'; import '../features/settings/models/geofence_system_state.dart'; import '../features/settings/providers/settings_providers.dart'; import '../models/home_config.dart'; import '../providers/providers.dart'; import 'home_edit_screen.dart'; import 'homes_screen.dart'; enum SettingsEntryPoint { homes, remote } class SettingsScreen extends ConsumerStatefulWidget { final SettingsEntryPoint entryPoint; const SettingsScreen({super.key, required this.entryPoint}); @override ConsumerState createState() => _SettingsScreenState(); } class _SettingsScreenState extends ConsumerState with WidgetsBindingObserver { bool _savingGeofence = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { ref.invalidate(geofenceSystemStatusProvider); } } @override Widget build(BuildContext context) { final currentHome = ref.watch(currentHomeProvider); final authState = ref.watch(authInfoProvider); final themePreset = ref.watch(appThemeProvider); final geofenceStatus = ref.watch(geofenceSystemStatusProvider); final geofenceState = geofenceStatus.asData?.value; return Scaffold( appBar: AppBar(title: const Text('НАСТРОЙКИ')), body: ListView( padding: const EdgeInsets.all(16), children: [ _SectionTitle( title: 'Дом и подключение', subtitle: 'К какому backend сейчас привязано приложение', ), _SectionCard( children: [ if (currentHome == null) const _EmptySectionState( icon: Icons.home_outlined, title: 'Активный дом не выбран', message: 'Выберите или добавьте дом, чтобы управлять светом.', ) else ...[ _InfoRow(label: 'Дом', value: currentHome.name), _InfoRow(label: 'Backend', value: currentHome.url), _InfoRow( label: 'Доступ', value: _authSummary(authState), muted: authState.status == LoadStatus.idle, ), const SizedBox(height: 12), Wrap( spacing: 8, runSpacing: 8, children: [ FilledButton.tonalIcon( onPressed: () => _openHomeEditor(context, currentHome), icon: const Icon(Icons.edit_location_alt_outlined), label: const Text('Редактировать дом'), ), OutlinedButton.icon( onPressed: () => _openHomes(context), icon: const Icon(Icons.home_work_outlined), label: const Text('Дома'), ), ], ), ], ], ), const SizedBox(height: 20), _SectionTitle( title: 'Гео-автоматизация', subtitle: 'Автовыключение света при уходе из дома', ), _SectionCard( children: [ if (currentHome == null) const _EmptySectionState( icon: Icons.location_off_outlined, title: 'Сначала нужен активный дом', message: 'Без выбранного дома geofence настраивать нечего.', ) else ...[ SwitchListTile( contentPadding: EdgeInsets.zero, secondary: Icon( Icons.directions_walk, color: currentHome.hasCoordinates ? Theme.of(context).colorScheme.primary : Theme.of(context).disabledColor, ), title: const Text('Выключать свет при уходе'), subtitle: Text( currentHome.hasCoordinates ? 'Работает только для текущего активного дома' : 'Сначала задайте координаты дома', ), value: currentHome.geofenceEnabled, onChanged: currentHome.hasCoordinates && !_savingGeofence ? (enabled) => _setGeofenceEnabled(currentHome, enabled) : null, ), const Divider(height: 24), _StatusTile( icon: _statusIcon(geofenceState, currentHome), color: _statusColor(context, geofenceState, currentHome), title: _statusTitle(geofenceState, currentHome), subtitle: _statusSubtitle(geofenceState, currentHome), ), if (_savingGeofence) const Padding( padding: EdgeInsets.only(top: 12), child: LinearProgressIndicator(minHeight: 3), ), const SizedBox(height: 12), ListTile( contentPadding: EdgeInsets.zero, leading: const Icon(Icons.radar_outlined), title: const Text('Радиус срабатывания'), subtitle: Text('${currentHome.geofenceRadiusMeters} м'), trailing: const Icon(Icons.chevron_right), enabled: currentHome.hasCoordinates && !_savingGeofence, onTap: currentHome.hasCoordinates && !_savingGeofence ? () => _editGeofenceRadius(context, currentHome) : null, ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: _buildGeofenceActions( context: context, home: currentHome, systemState: geofenceState, ), ), ], ], ), const SizedBox(height: 20), _SectionTitle( title: 'Внешний вид', subtitle: 'Фиксированные темы без лишней кастомизации', ), _SectionCard( children: [ RadioGroup( groupValue: themePreset, onChanged: (value) { if (value != null) { ref.read(appThemeProvider.notifier).setTheme(value); } }, child: Column( children: [ for (final preset in AppThemePreset.values) RadioListTile( value: preset, activeColor: preset.accentColor, secondary: CircleAvatar( radius: 12, backgroundColor: preset.accentColor, ), title: Text(preset.title), subtitle: Text(preset.subtitle), ), ], ), ), ], ), const SizedBox(height: 20), _SectionTitle( title: 'О приложении', subtitle: 'Короткая техническая сводка без маркетинговой воды', ), _SectionCard( children: [ const _InfoRow(label: 'Приложение', value: 'Ignis App'), _InfoRow(label: 'Сборка', value: BuildInfo.label), _InfoRow(label: 'Тема', value: themePreset.title), _InfoRow( label: 'Backend', value: currentHome?.url ?? 'не выбран', muted: currentHome == null, ), const SizedBox(height: 12), FilledButton.tonalIcon( onPressed: () => _copyDiagnosticsSummary( currentHome, authState, themePreset, ), icon: const Icon(Icons.content_copy_outlined), label: const Text('Скопировать сводку'), ), ], ), ], ), ); } List _buildGeofenceActions({ required BuildContext context, required HomeConfig home, required GeofenceSystemState? systemState, }) { final locationNotifier = ref.read(userLocationProvider.notifier); final issue = systemState?.issue; if (!home.hasCoordinates) { return [ FilledButton.tonalIcon( onPressed: () => _openHomeEditor(context, home), icon: const Icon(Icons.place_outlined), label: const Text('Задать координаты'), ), ]; } return switch (issue) { GeofenceSystemIssue.locationServicesDisabled => [ FilledButton.tonalIcon( onPressed: () async { await locationNotifier.openLocationSettings(); ref.invalidate(geofenceSystemStatusProvider); }, icon: const Icon(Icons.location_searching_outlined), label: const Text('Включить геолокацию'), ), ], GeofenceSystemIssue.permissionDenied => [ FilledButton.tonalIcon( onPressed: () async { await locationNotifier.requestPermission(); ref.invalidate(geofenceSystemStatusProvider); }, icon: const Icon(Icons.verified_user_outlined), label: const Text('Запросить доступ'), ), ], GeofenceSystemIssue.permissionDeniedForever || GeofenceSystemIssue.backgroundPermissionRequired => [ FilledButton.tonalIcon( onPressed: () async { await locationNotifier.openAppSettings(); ref.invalidate(geofenceSystemStatusProvider); }, icon: const Icon(Icons.settings_outlined), label: const Text('Открыть настройки Android'), ), ], _ => [ OutlinedButton.icon( onPressed: () => _openHomeEditor(context, home), icon: const Icon(Icons.edit_outlined), label: const Text('Координаты дома'), ), ], }; } Future _setGeofenceEnabled(HomeConfig home, bool enabled) async { await _saveCurrentHome( home.copyWith(geofenceEnabled: enabled), successMessage: enabled ? 'Автовыключение включено' : 'Автовыключение выключено', ); } Future _saveCurrentHome( HomeConfig updatedHome, { String? successMessage, }) async { setState(() => _savingGeofence = true); try { await ref.read(homesProvider.notifier).update(updatedHome); await ref.read(currentHomeProvider.notifier).switchTo(updatedHome); ref.invalidate(geofenceSystemStatusProvider); if (mounted && successMessage != null) { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(successMessage))); } } catch (error) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Не удалось сохранить настройки: ${describeLoadError(error)}', ), ), ); } } finally { if (mounted) { setState(() => _savingGeofence = false); } } } Future _editGeofenceRadius( BuildContext context, HomeConfig home, ) async { var draft = home.geofenceRadiusMeters.toDouble(); final saved = await showModalBottomSheet( context: context, showDragHandle: true, builder: (sheetContext) { return StatefulBuilder( builder: (context, setModalState) { final roundedDraft = (draft / 50).round() * 50; return SafeArea( top: false, child: Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Радиус geofence', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, ), ), const SizedBox(height: 8), Text( '$roundedDraft м', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 8), Slider( value: draft.clamp(100, 5000), min: 100, max: 5000, divisions: 98, label: '$roundedDraft м', onChanged: (value) { setModalState(() => draft = value); }, ), const SizedBox(height: 4), const Text( 'Чем больше радиус, тем меньше шанс ложного срабатывания далеко от дома.', ), const SizedBox(height: 16), Row( children: [ TextButton( onPressed: () => Navigator.of(sheetContext).pop(), child: const Text('Отмена'), ), const Spacer(), FilledButton( onPressed: () => Navigator.of(sheetContext).pop(roundedDraft), child: const Text('Сохранить'), ), ], ), ], ), ), ); }, ); }, ); if (saved == null || saved == home.geofenceRadiusMeters) { return; } await _saveCurrentHome( home.copyWith(geofenceRadiusMeters: saved), successMessage: 'Радиус geofence обновлён', ); } Future _copyDiagnosticsSummary( HomeConfig? currentHome, LoadState authState, AppThemePreset themePreset, ) async { final lines = [ 'app=Ignis App', 'build=${BuildInfo.label}', 'theme=${themePreset.storageValue}', 'home=${currentHome?.name ?? 'none'}', 'backend=${currentHome?.url ?? 'none'}', 'auth=${_authSummary(authState)}', 'geofence_enabled=${currentHome?.geofenceEnabled ?? false}', 'geofence_radius=${currentHome?.geofenceRadiusMeters ?? 0}', 'has_coordinates=${currentHome?.hasCoordinates == true}', ]; await Clipboard.setData(ClipboardData(text: lines.join('\n'))); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Сводка скопирована в буфер обмена')), ); } String _authSummary(LoadState authState) { final auth = authState.data; if (auth == null) { if (authState.hasError) { return authState.errorMessage ?? 'ошибка проверки доступа'; } return 'не загружено'; } final role = auth.isAdmin ? 'admin' : 'guest'; if (auth.name == null || auth.name!.trim().isEmpty) { return role; } return '${auth.name} · $role'; } IconData _statusIcon(GeofenceSystemState? state, HomeConfig home) { if (!home.geofenceEnabled) { return Icons.pause_circle_outline; } return switch (state?.issue) { GeofenceSystemIssue.ready => Icons.verified_outlined, GeofenceSystemIssue.locationServicesDisabled => Icons.location_disabled, GeofenceSystemIssue.permissionDenied || GeofenceSystemIssue.permissionDeniedForever || GeofenceSystemIssue.backgroundPermissionRequired => Icons.gpp_bad_outlined, GeofenceSystemIssue.missingCoordinates => Icons.place_outlined, _ => Icons.info_outline, }; } Color _statusColor( BuildContext context, GeofenceSystemState? state, HomeConfig home, ) { if (!home.geofenceEnabled) { return Theme.of(context).colorScheme.secondary; } return switch (state?.issue) { GeofenceSystemIssue.ready => Colors.green, GeofenceSystemIssue.locationServicesDisabled || GeofenceSystemIssue.permissionDenied || GeofenceSystemIssue.permissionDeniedForever || GeofenceSystemIssue.backgroundPermissionRequired || GeofenceSystemIssue.missingCoordinates => Theme.of( context, ).colorScheme.error, _ => Theme.of(context).colorScheme.primary, }; } String _statusTitle(GeofenceSystemState? state, HomeConfig home) { if (!home.geofenceEnabled) { return 'Автовыключение выключено'; } return switch (state?.issue) { GeofenceSystemIssue.ready => 'Geofence готов', GeofenceSystemIssue.missingCoordinates => 'Нужны координаты дома', GeofenceSystemIssue.locationServicesDisabled => 'Геолокация выключена', GeofenceSystemIssue.permissionDenied => 'Нужен доступ к геолокации', GeofenceSystemIssue.permissionDeniedForever => 'Доступ запрещён навсегда', GeofenceSystemIssue.backgroundPermissionRequired => 'Нужен доступ «Всегда»', GeofenceSystemIssue.noActiveHome => 'Нет активного дома', null => 'Проверяем системные условия', }; } String _statusSubtitle(GeofenceSystemState? state, HomeConfig home) { if (!home.geofenceEnabled) { return home.hasCoordinates ? 'Можно заранее настроить радиус и включить позже.' : 'Сначала задайте координаты дома, потом включайте geofence.'; } return switch (state?.issue) { GeofenceSystemIssue.ready => 'Android сможет отслеживать выход из зоны и запускать автовыключение.', GeofenceSystemIssue.missingCoordinates => 'Без координат дома geofence физически некуда поставить.', GeofenceSystemIssue.locationServicesDisabled => 'Пока системная геолокация выключена, фоновое срабатывание не поднимется.', GeofenceSystemIssue.permissionDenied => 'Нужно хотя бы базовое разрешение на геолокацию.', GeofenceSystemIssue.permissionDeniedForever => 'Разрешение нужно вернуть вручную в настройках Android.', GeofenceSystemIssue.backgroundPermissionRequired => 'Для фонового geofence нужен доступ к геолокации в режиме «Всегда».', GeofenceSystemIssue.noActiveHome => 'Сначала выберите активный дом, для которого будет работать автоматика.', null => 'Собираем информацию о системных ограничениях.', }; } Future _openHomeEditor(BuildContext context, HomeConfig home) async { await Navigator.of( context, ).push(MaterialPageRoute(builder: (_) => HomeEditScreen(home: home))); await ref.read(homesProvider.notifier).load(); await ref.read(currentHomeProvider.notifier).load(); ref.invalidate(geofenceSystemStatusProvider); } Future _openHomes(BuildContext context) async { if (widget.entryPoint == SettingsEntryPoint.homes) { Navigator.of(context).pop(); return; } Navigator.of( context, ).pushReplacement(MaterialPageRoute(builder: (_) => const HomesScreen())); } } class _SectionTitle extends StatelessWidget { final String title; final String subtitle; const _SectionTitle({required this.title, required this.subtitle}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of( context, ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w700), ), const SizedBox(height: 2), Text( subtitle, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.white54), ), ], ), ); } } class _SectionCard extends StatelessWidget { final List children; const _SectionCard({required this.children}); @override Widget build(BuildContext context) { return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: children, ), ), ); } } class _InfoRow extends StatelessWidget { final String label; final String value; final bool muted; const _InfoRow({ required this.label, required this.value, this.muted = false, }); @override Widget build(BuildContext context) { final valueStyle = Theme.of(context).textTheme.bodyMedium?.copyWith( color: muted ? Colors.white54 : null, fontWeight: FontWeight.w600, ); return Padding( padding: const EdgeInsets.only(bottom: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: Colors.white54), ), const SizedBox(height: 2), Text(value, style: valueStyle), ], ), ); } } class _StatusTile extends StatelessWidget { final IconData icon; final Color color; final String title; final String subtitle; const _StatusTile({ required this.icon, required this.color, required this.title, required this.subtitle, }); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: color), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text(subtitle, style: const TextStyle(color: Colors.white54)), ], ), ), ], ); } } class _EmptySectionState extends StatelessWidget { final IconData icon; final String title; final String message; const _EmptySectionState({ required this.icon, required this.title, required this.message, }); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon(icon, color: Colors.white38), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontWeight: FontWeight.w700)), const SizedBox(height: 2), Text(message, style: const TextStyle(color: Colors.white54)), ], ), ), ], ); } }