From 736a61d54bb2b92420f2914643557c890660f42f Mon Sep 17 00:00:00 2001 From: Artem Kokos Date: Thu, 23 Apr 2026 20:32:44 +0700 Subject: [PATCH] fix: report group control failures --- lib/providers/providers.dart | 52 ++++++++++++++++++++++++++--- lib/screens/remote_screen.dart | 25 ++++++++------ lib/widgets/group_card.dart | 11 ++++++ test/read_only_load_state_test.dart | 20 +++++++++++ 4 files changed, 93 insertions(+), 15 deletions(-) diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index 9762c5f..c9e0cd1 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -278,7 +278,7 @@ class GroupsLoadStateNotifier extends Notifier { } void setError(Object error) => - state = GroupsLoadState.error(error.toString()); + state = GroupsLoadState.error(describeLoadError(error)); } class GroupsNotifier extends Notifier> { @@ -432,7 +432,6 @@ class GroupsNotifier extends Notifier> { state = updatedList; ref.read(groupsLoadStateProvider.notifier).setData(updatedList); } catch (e) { - debugPrint("Ошибка глобального опроса: $e"); if (_isActiveGeneration(generation)) { ref.read(groupsLoadStateProvider.notifier).setError(e); } @@ -474,6 +473,7 @@ class GroupsNotifier extends Notifier> { void _debouncedControl( String id, String key, + String action, Map localPatch, Map apiParams, ) { @@ -489,7 +489,8 @@ class GroupsNotifier extends Notifier> { await _api.controlGroup(id, apiParams); } catch (e) { _lockUntil.remove(id); - refresh(); + await refresh(); + ref.read(groupControlErrorProvider.notifier).report(id, action, e); } }, ); @@ -513,6 +514,7 @@ class GroupsNotifier extends Notifier> { _debouncedControl( id, 'brightness', + 'яркость', {'brightness': value}, {'brightness': value}, ); @@ -520,7 +522,13 @@ class GroupsNotifier extends Notifier> { /// Установить цветовую температуру (2700-6500K) -- с debounce void setTemperature(String id, int value) { - _debouncedControl(id, 'temp', {'temp': value}, {'temp': value}); + _debouncedControl( + id, + 'temp', + 'температуру', + {'temp': value}, + {'temp': value}, + ); } /// Установить RGB-цвет -- с debounce @@ -528,6 +536,7 @@ class GroupsNotifier extends Notifier> { _debouncedControl( id, 'color', + 'цвет', {'r': r, 'g': g, 'b': b}, {'r': r, 'g': g, 'b': b}, ); @@ -558,6 +567,41 @@ class GroupsNotifier extends Notifier> { } } +class GroupControlError { + final String groupId; + final String action; + final String message; + final int sequence; + + const GroupControlError({ + required this.groupId, + required this.action, + required this.message, + required this.sequence, + }); +} + +final groupControlErrorProvider = + NotifierProvider( + () => GroupControlErrorNotifier(), + ); + +class GroupControlErrorNotifier extends Notifier { + int _sequence = 0; + + @override + GroupControlError? build() => null; + + void report(String groupId, String action, Object error) { + state = GroupControlError( + groupId: groupId, + action: action, + message: describeLoadError(error), + sequence: ++_sequence, + ); + } +} + // ─── Устройства (для создания групп) ───────────────────────── final devicesProvider = diff --git a/lib/screens/remote_screen.dart b/lib/screens/remote_screen.dart index 5743b01..47313a0 100644 --- a/lib/screens/remote_screen.dart +++ b/lib/screens/remote_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../app/error_message.dart'; import '../providers/providers.dart'; import '../widgets/group_card.dart'; import 'homes_screen.dart'; @@ -191,8 +192,7 @@ class _RemoteScreenState extends ConsumerState { ), child: const Icon(Icons.delete, color: Colors.redAccent), ), - confirmDismiss: (_) => _confirmDeleteGroup(context, g), - onDismissed: (_) => _deleteGroup(g['id'].toString()), + confirmDismiss: (_) => _confirmAndDeleteGroup(context, g), child: GroupCard(group: g), ); }, @@ -201,12 +201,13 @@ class _RemoteScreenState extends ConsumerState { ); } - /// Подтверждение удаления группы свайпом - Future _confirmDeleteGroup( + Future _confirmAndDeleteGroup( BuildContext context, Map g, ) async { - return await showDialog( + final messenger = ScaffoldMessenger.of(context); + final confirmed = + await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Удалить группу?'), @@ -227,18 +228,20 @@ class _RemoteScreenState extends ConsumerState { ), ) ?? false; - } - Future _deleteGroup(String id) async { + if (!confirmed) return false; + try { - await ref.read(apiProvider).deleteGroup(id); + await ref.read(apiProvider).deleteGroup(g['id'].toString()); await ref.read(groupsProvider.notifier).refresh(); + return true; } catch (e) { if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text('Ошибка удаления: $e'))); + messenger.showSnackBar( + SnackBar(content: Text('Ошибка удаления: ${describeLoadError(e)}')), + ); } + return false; } } } diff --git a/lib/widgets/group_card.dart b/lib/widgets/group_card.dart index d4bbaa7..fad774b 100644 --- a/lib/widgets/group_card.dart +++ b/lib/widgets/group_card.dart @@ -38,6 +38,17 @@ class _GroupCardState extends ConsumerState { final int gVal = g['last_state']?['g'] ?? 200; final int b = g['last_state']?['b'] ?? 100; + ref.listen(groupControlErrorProvider, (previous, next) { + if (next == null || next.groupId != id) return; + if (previous?.sequence == next.sequence) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Не удалось применить ${next.action}: ${next.message}'), + ), + ); + }); + // Значения слайдеров: локальные (если тянем) или серверные final briValue = (_localBrightness ?? bri.toDouble()).clamp(10.0, 100.0); final tempValue = (_localTemp ?? temp.toDouble()).clamp(2700.0, 6500.0); diff --git a/test/read_only_load_state_test.dart b/test/read_only_load_state_test.dart index dc92b88..d3c9920 100644 --- a/test/read_only_load_state_test.dart +++ b/test/read_only_load_state_test.dart @@ -478,4 +478,24 @@ void main() { expect(api.controlledGroupId, 'kitchen'); expect(api.controlGroupParams, containsPair('scene', 'party')); }); + + test('debounced group control error is reported', () async { + final api = FakeIgnisApi(); + final container = containerWith(api); + api.controlGroupError = DioException( + requestOptions: RequestOptions(path: '/control/group/kitchen'), + type: DioExceptionType.connectionError, + message: 'No route to host', + ); + + container.read(groupsProvider.notifier).setBrightness('kitchen', 42); + await Future.delayed(const Duration(milliseconds: 400)); + + final error = container.read(groupControlErrorProvider); + expect(error, isNotNull); + expect(error!.groupId, 'kitchen'); + expect(error.action, 'яркость'); + expect(error.message, contains('Backend недоступен')); + expect(api.controlGroupParams, containsPair('brightness', 42)); + }); }