Files
ignis_app/lib/screens/homes_screen.dart
2026-05-15 10:18:46 +07:00

414 lines
13 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 '../models/home_config.dart';
import '../providers/providers.dart';
import 'home_edit_screen.dart';
import 'remote_screen.dart';
import 'settings_screen.dart';
/// Экран "Дома" -- список серверов Ignis.
/// Пользователь может добавить, удалить, переключить активный дом.
class HomesScreen extends ConsumerStatefulWidget {
const HomesScreen({super.key});
@override
ConsumerState<HomesScreen> createState() => _HomesScreenState();
}
class _HomesScreenState extends ConsumerState<HomesScreen>
with WidgetsBindingObserver {
late final UserLocationNotifier _userLocationNotifier;
bool _isWatchingLocation = false;
String? _switchingHomeId;
String? _deletingHomeId;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_userLocationNotifier = ref.read(userLocationProvider.notifier);
Future.microtask(() async {
await _syncLocationWatching();
await _syncGeofenceAutomation();
});
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
if (_isWatchingLocation) {
_userLocationNotifier.stopWatching();
}
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
Future.microtask(_refreshEnvironmentState);
}
}
@override
Widget build(BuildContext context) {
final homes = ref.watch(homesProvider);
final currentHome = ref.watch(currentHomeProvider);
final location = ref.watch(userLocationProvider);
return Scaffold(
appBar: AppBar(
title: const Text('ДОМА'),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: const Icon(Icons.settings_outlined),
tooltip: 'Настройки',
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) =>
const SettingsScreen(entryPoint: SettingsEntryPoint.homes),
),
),
),
],
),
body: homes.isEmpty
? const _EmptyHomesView()
: RefreshIndicator(
color: Colors.deepOrange,
onRefresh: _refreshEnvironmentState,
child: ListView(
padding: const EdgeInsets.all(12),
children: [
...homes.map((home) {
final isActive = currentHome?.id == home.id;
final isSwitching = _switchingHomeId == home.id;
final isDeleting = _deletingHomeId == home.id;
final isBusy = isSwitching || isDeleting;
final distKm = location.distanceToKm(
home.latitude,
home.longitude,
);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
enabled: !isBusy,
leading: Icon(
Icons.home,
color: isActive ? Colors.deepOrange : Colors.white38,
size: 28,
),
title: Text(
home.name,
style: TextStyle(
fontWeight: isActive
? FontWeight.bold
: FontWeight.normal,
color: isActive ? Colors.deepOrange : Colors.white,
),
),
subtitle: _HomeSubtitle(
home: home,
location: location,
distKm: distKm,
isActive: isActive,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isBusy)
const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
else ...[
IconButton(
icon: const Icon(
Icons.edit,
size: 20,
color: Colors.white38,
),
onPressed: () => _editHome(context, home),
),
IconButton(
icon: const Icon(
Icons.delete_outline,
size: 20,
color: Colors.redAccent,
),
onPressed: () => _confirmDelete(context, home),
),
],
],
),
onTap: isBusy ? null : () => _selectHome(context, home),
),
);
}),
SizedBox(height: MediaQuery.of(context).padding.bottom + 80),
],
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: Colors.deepOrange,
onPressed: () => _addHome(context),
child: const Icon(Icons.add),
),
);
}
void _selectHome(BuildContext context, HomeConfig home) async {
if (_switchingHomeId != null || _deletingHomeId != null) return;
setState(() => _switchingHomeId = home.id);
final messenger = ScaffoldMessenger.of(context);
try {
await ref.read(currentHomeProvider.notifier).select(home);
if (context.mounted) {
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const RemoteScreen()),
);
}
} catch (e) {
if (context.mounted) {
messenger.showSnackBar(
SnackBar(
content: Text('Не удалось выбрать дом: ${describeLoadError(e)}'),
),
);
}
} finally {
if (mounted) {
setState(() => _switchingHomeId = null);
}
}
}
Future<void> _addHome(BuildContext context) async {
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => const HomeEditScreen()));
await _refreshEnvironmentState();
}
Future<void> _editHome(BuildContext context, HomeConfig home) async {
await Navigator.of(
context,
).push(MaterialPageRoute(builder: (_) => HomeEditScreen(home: home)));
await _refreshEnvironmentState();
}
void _confirmDelete(BuildContext context, HomeConfig home) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Удалить дом?'),
content: Text('Удалить "${home.name}" (${home.url})?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('Отмена'),
),
TextButton(
onPressed: () async {
Navigator.of(ctx).pop();
await _deleteHome(context, home);
},
child: const Text(
'Удалить',
style: TextStyle(color: Colors.redAccent),
),
),
],
),
);
}
Future<void> _deleteHome(BuildContext context, HomeConfig home) async {
if (_switchingHomeId != null || _deletingHomeId != null) return;
final deletedCurrentHome = ref.read(currentHomeProvider)?.id == home.id;
setState(() => _deletingHomeId = home.id);
final messenger = ScaffoldMessenger.of(context);
try {
await ref.read(homesProvider.notifier).remove(home.id);
await ref.read(currentHomeProvider.notifier).load();
if (deletedCurrentHome) {
ref.read(authInfoProvider.notifier).clear();
}
await _refreshEnvironmentState();
} catch (e) {
if (context.mounted) {
messenger.showSnackBar(
SnackBar(
content: Text('Не удалось удалить дом: ${describeLoadError(e)}'),
),
);
}
} finally {
if (mounted) {
setState(() => _deletingHomeId = null);
}
}
}
Future<void> _refreshEnvironmentState() async {
await _syncLocationWatching();
if (_isWatchingLocation) {
await _userLocationNotifier.refresh();
}
await _syncGeofenceAutomation();
}
Future<void> _syncGeofenceAutomation() async {
await ref
.read(geofenceAutomationServiceProvider)
.syncActiveHome(ref.read(currentHomeProvider));
}
Future<void> _syncLocationWatching() async {
final shouldWatch = ref
.read(homesProvider)
.any((home) => home.hasCoordinates);
if (shouldWatch == _isWatchingLocation) {
return;
}
if (shouldWatch) {
await _userLocationNotifier.startWatching();
_isWatchingLocation = true;
return;
}
_userLocationNotifier.stopWatching();
_isWatchingLocation = false;
}
}
class _EmptyHomesView extends StatelessWidget {
const _EmptyHomesView();
@override
Widget build(BuildContext context) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.home_outlined, size: 64, color: Colors.white24),
SizedBox(height: 16),
Text(
'Нет добавленных домов',
style: TextStyle(color: Colors.white54, fontSize: 16),
),
SizedBox(height: 8),
Text(
'Добавьте сервер Ignis',
style: TextStyle(color: Colors.white38, fontSize: 14),
),
],
),
);
}
}
class _HomeSubtitle extends StatelessWidget {
final HomeConfig home;
final UserLocation location;
final double? distKm;
final bool isActive;
const _HomeSubtitle({
required this.home,
required this.location,
required this.distKm,
required this.isActive,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
home.url,
style: const TextStyle(color: Colors.white38, fontSize: 12),
),
if (distKm != null)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
const Icon(Icons.near_me, size: 11, color: Colors.white30),
const SizedBox(width: 4),
Text(
'~${formatDistance(distKm!)}',
style: const TextStyle(color: Colors.white30, fontSize: 11),
),
],
),
)
else if (home.hasCoordinates && !location.hasPosition)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
const Icon(Icons.location_on, size: 12, color: Colors.white24),
const SizedBox(width: 4),
Text(
location.error ?? 'Координаты заданы',
style: const TextStyle(color: Colors.white24, fontSize: 11),
),
],
),
)
else if (home.geofenceReady && isActive)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
const Icon(
Icons.shield_moon_outlined,
size: 12,
color: Colors.deepOrangeAccent,
),
const SizedBox(width: 4),
Text(
'Автовыключение: ${home.geofenceRadiusMeters} м',
style: const TextStyle(
color: Colors.deepOrangeAccent,
fontSize: 11,
),
),
],
),
)
else if (home.geofenceReady)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Row(
children: [
const Icon(
Icons.shield_moon_outlined,
size: 12,
color: Colors.white24,
),
const SizedBox(width: 4),
Text(
'Автовыключение: ${home.geofenceRadiusMeters} м',
style: const TextStyle(color: Colors.white24, fontSize: 11),
),
],
),
),
],
);
}
}