307 lines
10 KiB
Dart
307 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import '../app/error_message.dart';
|
||
import '../models/ignis_group.dart';
|
||
import '../providers/providers.dart';
|
||
import '../widgets/build_info_text.dart';
|
||
import '../widgets/group_card.dart';
|
||
import 'homes_screen.dart';
|
||
import 'group_edit_screen.dart';
|
||
import 'schedules_screen.dart';
|
||
import 'stats_screen.dart';
|
||
import 'event_log_screen.dart';
|
||
import 'api_keys_screen.dart';
|
||
|
||
/// Основной экран пульта управления.
|
||
/// Показывает группы текущего дома с управлением.
|
||
class RemoteScreen extends ConsumerStatefulWidget {
|
||
const RemoteScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<RemoteScreen> createState() => _RemoteScreenState();
|
||
}
|
||
|
||
class _RemoteScreenState extends ConsumerState<RemoteScreen> {
|
||
late final GroupsNotifier _groupsNotifier;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_groupsNotifier = ref.read(groupsProvider.notifier);
|
||
if (ref.read(remotePollingEnabledProvider)) {
|
||
Future.microtask(_groupsNotifier.startPolling);
|
||
}
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_groupsNotifier.stopPolling(resetStatus: false);
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final groups = ref.watch(groupsProvider);
|
||
final groupsLoadState = ref.watch(groupsLoadStateProvider);
|
||
final currentHome = ref.watch(currentHomeProvider);
|
||
final authInfoState = ref.watch(authInfoProvider);
|
||
final isAdmin = authInfoState.data?.isAdmin == true;
|
||
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text(currentHome?.name ?? 'IGNIS'),
|
||
leading: IconButton(
|
||
icon: const Icon(Icons.home),
|
||
tooltip: 'Дома',
|
||
onPressed: () => Navigator.of(context).pushReplacement(
|
||
MaterialPageRoute(builder: (_) => const HomesScreen()),
|
||
),
|
||
),
|
||
actions: [
|
||
// Кнопка добавления группы
|
||
IconButton(
|
||
icon: const Icon(Icons.add_circle_outline),
|
||
tooltip: 'Создать группу',
|
||
onPressed: () => Navigator.of(
|
||
context,
|
||
).push(MaterialPageRoute(builder: (_) => const GroupEditScreen())),
|
||
),
|
||
// Меню
|
||
PopupMenuButton<String>(
|
||
icon: const Icon(Icons.more_vert),
|
||
onSelected: (value) {
|
||
switch (value) {
|
||
case 'schedules':
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(builder: (_) => const SchedulesScreen()),
|
||
);
|
||
break;
|
||
case 'stats':
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(builder: (_) => const StatsScreen()),
|
||
);
|
||
break;
|
||
case 'log':
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(builder: (_) => const EventLogScreen()),
|
||
);
|
||
break;
|
||
case 'api_keys':
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(builder: (_) => const ApiKeysScreen()),
|
||
);
|
||
break;
|
||
}
|
||
},
|
||
itemBuilder: (context) => [
|
||
const PopupMenuItem(
|
||
value: 'schedules',
|
||
child: ListTile(
|
||
leading: Icon(Icons.schedule),
|
||
title: Text('Расписания'),
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
const PopupMenuItem(
|
||
value: 'stats',
|
||
child: ListTile(
|
||
leading: Icon(Icons.bar_chart),
|
||
title: Text('Статистика'),
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
const PopupMenuItem(
|
||
value: 'log',
|
||
child: ListTile(
|
||
leading: Icon(Icons.list_alt),
|
||
title: Text('Лог событий'),
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
if (isAdmin)
|
||
const PopupMenuItem(
|
||
value: 'api_keys',
|
||
child: ListTile(
|
||
leading: Icon(Icons.vpn_key),
|
||
title: Text('API-ключи'),
|
||
contentPadding: EdgeInsets.zero,
|
||
),
|
||
),
|
||
const PopupMenuItem(
|
||
enabled: false,
|
||
child: Padding(
|
||
padding: EdgeInsets.symmetric(vertical: 4),
|
||
child: BuildInfoText(compact: false, alignStart: true),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
body: groupsLoadState.isLoading && groups.isEmpty
|
||
? const Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
CircularProgressIndicator(color: Colors.deepOrange),
|
||
SizedBox(height: 20),
|
||
Text(
|
||
"Опрос ламп (это долго)...",
|
||
style: TextStyle(color: Colors.white54),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: groupsLoadState.hasError && groups.isEmpty
|
||
? _GroupsErrorView(message: groupsLoadState.errorMessage)
|
||
: groups.isEmpty
|
||
? Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.lightbulb_outline,
|
||
size: 64,
|
||
color: Colors.white24,
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
'Нет групп',
|
||
style: TextStyle(color: Colors.white54, fontSize: 16),
|
||
),
|
||
const SizedBox(height: 8),
|
||
TextButton.icon(
|
||
icon: const Icon(Icons.add),
|
||
label: const Text('Создать группу'),
|
||
onPressed: () => Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => const GroupEditScreen(),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
)
|
||
: RefreshIndicator(
|
||
color: Colors.deepOrange,
|
||
onRefresh: () => ref.read(groupsProvider.notifier).refresh(),
|
||
child: ListView.builder(
|
||
padding: const EdgeInsets.only(top: 8, bottom: 80),
|
||
itemCount: groups.length,
|
||
itemBuilder: (context, index) {
|
||
final g = groups[index];
|
||
return Dismissible(
|
||
key: Key(g.id),
|
||
direction: DismissDirection.endToStart,
|
||
background: Container(
|
||
alignment: Alignment.centerRight,
|
||
padding: const EdgeInsets.only(right: 20),
|
||
margin: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 6,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: Colors.redAccent.withValues(alpha: 0.3),
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
child: const Icon(Icons.delete, color: Colors.redAccent),
|
||
),
|
||
confirmDismiss: (_) => _confirmAndDeleteGroup(context, g),
|
||
child: GroupCard(group: g),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<bool> _confirmAndDeleteGroup(
|
||
BuildContext context,
|
||
IgnisGroup g,
|
||
) async {
|
||
final messenger = ScaffoldMessenger.of(context);
|
||
final confirmed =
|
||
await showDialog<bool>(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
title: const Text('Удалить группу?'),
|
||
content: Text('Удалить "${g.name}"?'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(false),
|
||
child: const Text('Отмена'),
|
||
),
|
||
TextButton(
|
||
onPressed: () => Navigator.of(ctx).pop(true),
|
||
child: const Text(
|
||
'Удалить',
|
||
style: TextStyle(color: Colors.redAccent),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
) ??
|
||
false;
|
||
|
||
if (!confirmed) return false;
|
||
|
||
try {
|
||
await ref.read(apiProvider).deleteGroup(g.id);
|
||
await ref.read(groupsProvider.notifier).refresh();
|
||
return true;
|
||
} catch (e) {
|
||
if (mounted) {
|
||
messenger.showSnackBar(
|
||
SnackBar(content: Text('Ошибка удаления: ${describeLoadError(e)}')),
|
||
);
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
class _GroupsErrorView extends ConsumerWidget {
|
||
final String? message;
|
||
|
||
const _GroupsErrorView({this.message});
|
||
|
||
@override
|
||
Widget build(BuildContext context, WidgetRef ref) {
|
||
return Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(24),
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
const Icon(
|
||
Icons.wifi_off_rounded,
|
||
size: 64,
|
||
color: Colors.deepOrange,
|
||
),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
'Не удалось загрузить группы',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(color: Colors.white70, fontSize: 16),
|
||
),
|
||
if (message != null) ...[
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
message!,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(color: Colors.white38, fontSize: 12),
|
||
),
|
||
],
|
||
const SizedBox(height: 16),
|
||
FilledButton.icon(
|
||
onPressed: () => ref.read(groupsProvider.notifier).refresh(),
|
||
icon: const Icon(Icons.refresh),
|
||
label: const Text('Повторить'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|