From 63a4113d81d15ddfb3e41ebf0df7a82cf29cec7f Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Wed, 8 Apr 2026 16:06:44 +0200 Subject: [PATCH] refactor(legacy): single source of truth for devices + persisted selection Introduces legacyDevicesProvider as the canonical AsyncNotifier owning the devices list, with semantic mutation methods (removeDevice, renameDevice, refresh) instead of ref.invalidate from outside. selectedDeviceProvider becomes an AsyncNotifier that persists the selected device id in SharedPreferences and resolves it against the shared list at build time, surviving cold starts. ControlPanelViewModel and LocationViewModel are converted to AsyncNotifier and react to selection changes, refetching positions / geofences / frequent_places automatically. Screens use .when with skipLoadingOnReload to avoid flicker. Multi-device fixes: - DeviceBanner cards now look up positions per device by identificator - LocationMap recenters the camera on device swipe - Country-level fallback zoom when no position is available - Banner shows each device's own info while swiping linked_devices and device_setup feed mutations through the new notifier methods. 30+ legacy view models updated to read selectedDeviceProvider via .value (AsyncNotifier shape). --- .../presentation/account_settings_screen.dart | 2 +- .../state/linked_devices_view_model.dart | 8 +- .../presentation/control_panel_screen.dart | 114 +++++---- .../state/control_panel_view_model.dart | 171 ++++++------- .../state/control_panel_view_state.dart | 2 - .../control_panel_view_state.freezed.dart | 46 ++-- .../presentation/activity_meter_screen.dart | 2 +- .../state/activity_meter_view_model.dart | 4 +- .../state/apps_use_view_model.dart | 2 +- .../state/background_image_view_model.dart | 6 +- .../state/call_history_view_model.dart | 2 +- .../state/contacts_view_model.dart | 2 +- .../health/presentation/health_screen.dart | 2 +- .../presentation/state/health_view_model.dart | 6 +- .../state/locate_device_view_model.dart | 2 +- .../state/remote_connection_view_model.dart | 2 +- .../state/rewards_view_model.dart | 2 +- .../scheduled_activities_view_model.dart | 2 +- .../state/volume_control_view_model.dart | 2 +- .../state/device_setup_view_model.dart | 5 + .../presentation/location_screen.dart | 217 ++++++++-------- .../state/location_view_model.dart | 242 +++++++++++------- .../state/location_view_state.dart | 1 - .../state/location_view_state.freezed.dart | 43 ++-- .../widgets/create_frequent_place_sheet.dart | 4 +- .../widgets/create_geofence_sheet.dart | 4 +- .../presentation/widgets/device_banner.dart | 9 +- .../presentation/widgets/location_map.dart | 29 ++- .../presentation/state/alarm_view_model.dart | 4 +- .../presentation/state/alerts_view_model.dart | 4 +- .../state/battery_view_model.dart | 4 +- .../state/block_phone_view_model.dart | 6 +- .../state/disable_functions_view_model.dart | 4 +- .../state/language_view_model.dart | 2 +- .../state/remote_management_view_model.dart | 2 +- .../state/sos_contacts_view_model.dart | 6 +- .../presentation/state/sound_view_model.dart | 2 +- .../state/sync_clock_view_model.dart | 2 +- .../state/timezone_view_model.dart | 4 +- .../state/wifi_settings_view_model.dart | 6 +- .../legacy_shared/lib/legacy_shared.dart | 1 + .../providers/legacy_devices_provider.dart | 41 +++ .../providers/selected_device_provider.dart | 60 ++++- 43 files changed, 619 insertions(+), 462 deletions(-) create mode 100644 modules/legacy/packages/legacy_shared/lib/src/providers/legacy_devices_provider.dart diff --git a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart index 86414005..5c2c2ee6 100644 --- a/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart +++ b/modules/legacy/modules/account/lib/src/features/account_settings/presentation/account_settings_screen.dart @@ -21,7 +21,7 @@ class AccountSettingsScreen extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themePortProvider); final color = theme.getColorFor(ThemeCode.legacyPrimary); - final selectedDevice = ref.watch(selectedDeviceProvider); + final selectedDevice = ref.watch(selectedDeviceProvider).value; final isLoggingOut = ref.watch( accountSettingsViewModelProvider.select((s) => s.isLoggingOut), ); diff --git a/modules/legacy/modules/account/lib/src/features/linked_devices/presentation/state/linked_devices_view_model.dart b/modules/legacy/modules/account/lib/src/features/linked_devices/presentation/state/linked_devices_view_model.dart index a68c50ed..25500f2e 100644 --- a/modules/legacy/modules/account/lib/src/features/linked_devices/presentation/state/linked_devices_view_model.dart +++ b/modules/legacy/modules/account/lib/src/features/linked_devices/presentation/state/linked_devices_view_model.dart @@ -81,6 +81,8 @@ class LinkedDevicesViewModel extends Notifier { ref.invalidate(selectedDeviceProvider); } + ref.read(legacyDevicesProvider.notifier).removeDevice(device.id); + unawaited(_tracking.legacyAccountLinkedDeviceUnlinked()); state = state.copyWith( @@ -109,7 +111,11 @@ class LinkedDevicesViewModel extends Notifier { if (deviceName.isEmpty) return; try { - _devicesRepository.updateDevice(request: _toRequest(device)); + await _devicesRepository.updateDevice(request: _toRequest(device)); + ref.read(legacyDevicesProvider.notifier).renameDevice( + deviceId: device.id, + newCarrierName: deviceName.trim(), + ); unawaited(_tracking.legacyAccountLinkedDeviceRenamed()); } catch (e) { state = state.copyWith( diff --git a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart index 7078593d..19cf1254 100644 --- a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart +++ b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter_svg/svg.dart'; import 'package:control_panel/src/features/control_panel/presentation/state/control_panel_view_model.dart'; +import 'package:control_panel/src/features/control_panel/presentation/state/control_panel_view_state.dart'; import 'package:control_panel/src/shared/widgets/device_map.dart'; import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; @@ -18,52 +19,52 @@ class ControlPanelScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themePortProvider); - final state = ref.watch(controlPanelViewModelProvider); - - ref.listen(controlPanelViewModelProvider.select((s) => s.errorMessage), ( - previous, - next, - ) { - if (next.isNotEmpty) { - showTopSnackbar(context, message: next, type: MessageType.error); - } - }); - - if (state.isLoading) { - return Scaffold( - backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary), - body: const Center(child: CircularProgressIndicator()), - ); - } + final asyncState = ref.watch(controlPanelViewModelProvider); return Scaffold( backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary), - body: SafeArea( - child: Container( - padding: EdgeInsets.symmetric(horizontal: 14), - child: Column( - children: [ - SizedBox(height: SizeUtils.getByScreen(small: 8, big: 4)), - _Header(), - SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)), - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _MenuSection(navigationContract: navigationContract), - SizedBox( - height: SizeUtils.getByScreen(small: 16, big: 22), - ), - _MapSection(navigationContract: navigationContract), - SizedBox( - height: SizeUtils.getByScreen(small: 14, big: 13), - ), - ], + body: asyncState.when( + skipLoadingOnReload: true, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + formatErrorMessage(error), + textAlign: TextAlign.center, + ), + ), + ), + data: (state) => SafeArea( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14), + child: Column( + children: [ + SizedBox(height: SizeUtils.getByScreen(small: 8, big: 4)), + _Header(state: state), + SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)), + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _MenuSection(navigationContract: navigationContract), + SizedBox( + height: SizeUtils.getByScreen(small: 16, big: 22), + ), + _MapSection( + state: state, + navigationContract: navigationContract, + ), + SizedBox( + height: SizeUtils.getByScreen(small: 14, big: 13), + ), + ], + ), ), ), - ), - ], + ], + ), ), ), ), @@ -72,11 +73,12 @@ class ControlPanelScreen extends ConsumerWidget { } class _Header extends ConsumerWidget { - const _Header(); + final ControlPanelViewState state; + + const _Header({required this.state}); @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(controlPanelViewModelProvider); final vm = ref.read(controlPanelViewModelProvider.notifier); return Stack( @@ -94,14 +96,14 @@ class _Header extends ConsumerWidget { ), SizedBox(width: SizeUtils.getByScreen(small: 8, big: 4)), SizedBox( - width: SizeUtils.getByScreen(small: 130, big: 140), + width: SizeUtils.getByScreen(small: 100, big: 110), height: 32, child: CustomDropdown( items: state.devices.map((DeviceEntity device) { - final name = device.carrierName ?? ''; return Text( - name.length > 10 ? '${name.substring(0, 10)}...' : name, + device.carrierName ?? '', overflow: TextOverflow.ellipsis, + maxLines: 1, ); }).toList(), values: state.devices, @@ -216,14 +218,17 @@ class _SectionButton extends ConsumerWidget { } class _MapSection extends ConsumerWidget { + final ControlPanelViewState state; final NavigationContract navigationContract; - const _MapSection({required this.navigationContract}); + const _MapSection({ + required this.state, + required this.navigationContract, + }); @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.read(themePortProvider); - final state = ref.watch(controlPanelViewModelProvider); final vm = ref.read(controlPanelViewModelProvider.notifier); return GestureDetector( @@ -243,7 +248,18 @@ class _MapSection extends ConsumerWidget { ), ), IconButton( - onPressed: vm.refreshPositions, + onPressed: () async { + try { + await vm.refreshPositions(); + } catch (e) { + if (!context.mounted) return; + showTopSnackbar( + context, + message: formatErrorMessage(e), + type: MessageType.error, + ); + } + }, icon: Icon( Icons.refresh, color: theme.getColorFor(ThemeCode.legacyPrimary), diff --git a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_model.dart b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_model.dart index 74879910..51e412e1 100644 --- a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_model.dart +++ b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_model.dart @@ -9,131 +9,114 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sf_shared/sf_shared.dart'; import 'package:sf_tracking/sf_tracking.dart'; -final controlPanelViewModelProvider = - NotifierProvider.autoDispose( - ControlPanelViewModel.new, - ); +final controlPanelViewModelProvider = AsyncNotifierProvider.autoDispose< + ControlPanelViewModel, + ControlPanelViewState>(ControlPanelViewModel.new); -class ControlPanelViewModel extends Notifier { - late final ControlPanelRepository _controlPanelRepository; - late final SharedDevicesRepository _devicesRepository; - late final SelectedDeviceNotifier _selectedDeviceNotifier; - late final SfTrackingRepository _tracking; +class ControlPanelViewModel extends AsyncNotifier { + late ControlPanelRepository _controlPanelRepository; + late SfTrackingRepository _tracking; @override - ControlPanelViewState build() { + Future build() async { _controlPanelRepository = ref.read(controlPanelRepositoryProvider); - _devicesRepository = ref.read(sharedDevicesRepositoryProvider); - _selectedDeviceNotifier = ref.read(selectedDeviceProvider.notifier); _tracking = ref.read(sfTrackingProvider); - _init(); - return const ControlPanelViewState(); - } - Future _init() async { - try { - final devices = await _devicesRepository.getDevices(); - if (!ref.mounted) return; + final devices = await ref.watch(legacyDevicesProvider.future); + if (devices.isEmpty) return const ControlPanelViewState(); - if (devices.isEmpty) { - state = state.copyWith(isLoading: false); - return; - } + final selected = await ref.watch(selectedDeviceProvider.future); - final previouslySelected = ref.read(selectedDeviceProvider); - final selected = - previouslySelected != null && - devices.any( - (d) => d.identificator == previouslySelected.identificator, - ) - ? previouslySelected - : devices.first; - - state = state.copyWith(devices: devices, selectedDevice: selected); - _selectedDeviceNotifier.setSelectedDevice(selected); - - final positionLists = await Future.wait( - devices.map( - (d) => _controlPanelRepository.getLatestPositions( - deviceId: d.identificator, - ), + final positionLists = await Future.wait>( + devices.map( + (d) => _controlPanelRepository.getLatestPositions( + deviceId: d.identificator, ), - ); - if (!ref.mounted) return; - - _applyPositions(positionLists); - state = state.copyWith(isLoading: false); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith( - isLoading: false, - errorMessage: formatErrorMessage(e), - ); - } - } - - void _applyPositions(List> positionLists) { - final latestPositions = positionLists.where((list) => list.isNotEmpty).map(( - list, - ) { - final valid = - list.where((p) => p.latitude != 0 || p.longitude != 0).toList() - ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); - return valid.isNotEmpty ? valid.first : list.last; - }).toList(); - - final selectedPosition = state.selectedDevice != null + ), + ); + final latestPositions = _pickLatest(positionLists); + final selectedPosition = selected != null ? latestPositions - .where( - (p) => - p.deviceIdentificator == - state.selectedDevice!.identificator, - ) + .where((p) => p.deviceIdentificator == selected.identificator) .firstOrNull : null; - state = state.copyWith( + return ControlPanelViewState( + devices: devices, + selectedDevice: selected, positions: latestPositions, selectedPosition: selectedPosition, ); } Future refreshPositions() async { - if (state.devices.isEmpty) return; - state = state.copyWith(errorMessage: ''); - try { - final positionLists = await Future.wait( - state.devices.map( - (d) => _controlPanelRepository.getLatestPositions( - deviceId: d.identificator, - ), + final current = state.value; + if (current == null || current.devices.isEmpty) return; + + final positionLists = await Future.wait>( + current.devices.map( + (d) => _controlPanelRepository.getLatestPositions( + deviceId: d.identificator, ), - ); - if (!ref.mounted) return; - _applyPositions(positionLists); - unawaited(_tracking.legacyControlPanelPositionsRefreshed()); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith(errorMessage: formatErrorMessage(e)); - } + ), + ); + if (!ref.mounted) return; + + final latestPositions = _pickLatest(positionLists); + final selectedPosition = current.selectedDevice != null + ? latestPositions + .where( + (p) => + p.deviceIdentificator == + current.selectedDevice!.identificator, + ) + .firstOrNull + : null; + + state = AsyncData( + current.copyWith( + positions: latestPositions, + selectedPosition: selectedPosition, + ), + ); + + unawaited(_tracking.legacyControlPanelPositionsRefreshed()); } - void setSelectedDevice(DeviceEntity device) { - final selectedPosition = state.positions + Future setSelectedDevice(DeviceEntity device) async { + final current = state.value; + if (current == null) return; + + final selectedPosition = current.positions .where((p) => p.deviceIdentificator == device.identificator) .firstOrNull; - state = state.copyWith( - selectedDevice: device, - selectedPosition: selectedPosition, + state = AsyncData( + current.copyWith( + selectedDevice: device, + selectedPosition: selectedPosition, + ), ); - _selectedDeviceNotifier.setSelectedDevice(device); + await ref + .read(selectedDeviceProvider.notifier) + .setSelectedDevice(device); unawaited( _tracking.legacyControlPanelDeviceSelected( - totalDevices: state.devices.length, + totalDevices: current.devices.length, ), ); } + + static List _pickLatest( + List> positionLists, + ) { + return positionLists.where((list) => list.isNotEmpty).map((list) { + final valid = + list.where((p) => p.latitude != 0 || p.longitude != 0).toList() + ..sort((a, b) => b.createdAt.compareTo(a.createdAt)); + return valid.isNotEmpty ? valid.first : list.last; + }).toList(); + } } diff --git a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.dart b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.dart index c9d047ee..2fb2ab17 100644 --- a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.dart +++ b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.dart @@ -11,7 +11,5 @@ abstract class ControlPanelViewState with _$ControlPanelViewState { DeviceEntity? selectedDevice, @Default([]) List positions, PositionEntity? selectedPosition, - @Default(true) bool isLoading, - @Default('') String errorMessage, }) = _ControlPanelViewState; } diff --git a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.freezed.dart b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.freezed.dart index 2ce58f3f..bf84d819 100644 --- a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.freezed.dart +++ b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/state/control_panel_view_state.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$ControlPanelViewState { - List get devices; DeviceEntity? get selectedDevice; List get positions; PositionEntity? get selectedPosition; bool get isLoading; String get errorMessage; + List get devices; DeviceEntity? get selectedDevice; List get positions; PositionEntity? get selectedPosition; /// Create a copy of ControlPanelViewState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $ControlPanelViewStateCopyWith get copyWith => _$ControlP @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is ControlPanelViewState&&const DeepCollectionEquality().equals(other.devices, devices)&&(identical(other.selectedDevice, selectedDevice) || other.selectedDevice == selectedDevice)&&const DeepCollectionEquality().equals(other.positions, positions)&&(identical(other.selectedPosition, selectedPosition) || other.selectedPosition == selectedPosition)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ControlPanelViewState&&const DeepCollectionEquality().equals(other.devices, devices)&&(identical(other.selectedDevice, selectedDevice) || other.selectedDevice == selectedDevice)&&const DeepCollectionEquality().equals(other.positions, positions)&&(identical(other.selectedPosition, selectedPosition) || other.selectedPosition == selectedPosition)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(devices),selectedDevice,const DeepCollectionEquality().hash(positions),selectedPosition,isLoading,errorMessage); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(devices),selectedDevice,const DeepCollectionEquality().hash(positions),selectedPosition); @override String toString() { - return 'ControlPanelViewState(devices: $devices, selectedDevice: $selectedDevice, positions: $positions, selectedPosition: $selectedPosition, isLoading: $isLoading, errorMessage: $errorMessage)'; + return 'ControlPanelViewState(devices: $devices, selectedDevice: $selectedDevice, positions: $positions, selectedPosition: $selectedPosition)'; } @@ -45,7 +45,7 @@ abstract mixin class $ControlPanelViewStateCopyWith<$Res> { factory $ControlPanelViewStateCopyWith(ControlPanelViewState value, $Res Function(ControlPanelViewState) _then) = _$ControlPanelViewStateCopyWithImpl; @useResult $Res call({ - List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage + List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition }); @@ -62,15 +62,13 @@ class _$ControlPanelViewStateCopyWithImpl<$Res> /// Create a copy of ControlPanelViewState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? devices = null,Object? selectedDevice = freezed,Object? positions = null,Object? selectedPosition = freezed,Object? isLoading = null,Object? errorMessage = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? devices = null,Object? selectedDevice = freezed,Object? positions = null,Object? selectedPosition = freezed,}) { return _then(_self.copyWith( devices: null == devices ? _self.devices : devices // ignore: cast_nullable_to_non_nullable as List,selectedDevice: freezed == selectedDevice ? _self.selectedDevice : selectedDevice // ignore: cast_nullable_to_non_nullable as DeviceEntity?,positions: null == positions ? _self.positions : positions // ignore: cast_nullable_to_non_nullable as List,selectedPosition: freezed == selectedPosition ? _self.selectedPosition : selectedPosition // ignore: cast_nullable_to_non_nullable -as PositionEntity?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, +as PositionEntity?, )); } /// Create a copy of ControlPanelViewState @@ -179,10 +177,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _ControlPanelViewState() when $default != null: -return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selectedPosition,_that.isLoading,_that.errorMessage);case _: +return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selectedPosition);case _: return orElse(); } @@ -200,10 +198,10 @@ return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selecte /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition) $default,) {final _that = this; switch (_that) { case _ControlPanelViewState(): -return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selectedPosition,_that.isLoading,_that.errorMessage);case _: +return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selectedPosition);case _: throw StateError('Unexpected subclass'); } @@ -220,10 +218,10 @@ return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selecte /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition)? $default,) {final _that = this; switch (_that) { case _ControlPanelViewState() when $default != null: -return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selectedPosition,_that.isLoading,_that.errorMessage);case _: +return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selectedPosition);case _: return null; } @@ -235,7 +233,7 @@ return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selecte class _ControlPanelViewState implements ControlPanelViewState { - const _ControlPanelViewState({final List devices = const [], this.selectedDevice, final List positions = const [], this.selectedPosition, this.isLoading = true, this.errorMessage = ''}): _devices = devices,_positions = positions; + const _ControlPanelViewState({final List devices = const [], this.selectedDevice, final List positions = const [], this.selectedPosition}): _devices = devices,_positions = positions; final List _devices; @@ -254,8 +252,6 @@ class _ControlPanelViewState implements ControlPanelViewState { } @override final PositionEntity? selectedPosition; -@override@JsonKey() final bool isLoading; -@override@JsonKey() final String errorMessage; /// Create a copy of ControlPanelViewState /// with the given fields replaced by the non-null parameter values. @@ -267,16 +263,16 @@ _$ControlPanelViewStateCopyWith<_ControlPanelViewState> get copyWith => __$Contr @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ControlPanelViewState&&const DeepCollectionEquality().equals(other._devices, _devices)&&(identical(other.selectedDevice, selectedDevice) || other.selectedDevice == selectedDevice)&&const DeepCollectionEquality().equals(other._positions, _positions)&&(identical(other.selectedPosition, selectedPosition) || other.selectedPosition == selectedPosition)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ControlPanelViewState&&const DeepCollectionEquality().equals(other._devices, _devices)&&(identical(other.selectedDevice, selectedDevice) || other.selectedDevice == selectedDevice)&&const DeepCollectionEquality().equals(other._positions, _positions)&&(identical(other.selectedPosition, selectedPosition) || other.selectedPosition == selectedPosition)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_devices),selectedDevice,const DeepCollectionEquality().hash(_positions),selectedPosition,isLoading,errorMessage); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_devices),selectedDevice,const DeepCollectionEquality().hash(_positions),selectedPosition); @override String toString() { - return 'ControlPanelViewState(devices: $devices, selectedDevice: $selectedDevice, positions: $positions, selectedPosition: $selectedPosition, isLoading: $isLoading, errorMessage: $errorMessage)'; + return 'ControlPanelViewState(devices: $devices, selectedDevice: $selectedDevice, positions: $positions, selectedPosition: $selectedPosition)'; } @@ -287,7 +283,7 @@ abstract mixin class _$ControlPanelViewStateCopyWith<$Res> implements $ControlPa factory _$ControlPanelViewStateCopyWith(_ControlPanelViewState value, $Res Function(_ControlPanelViewState) _then) = __$ControlPanelViewStateCopyWithImpl; @override @useResult $Res call({ - List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage + List devices, DeviceEntity? selectedDevice, List positions, PositionEntity? selectedPosition }); @@ -304,15 +300,13 @@ class __$ControlPanelViewStateCopyWithImpl<$Res> /// Create a copy of ControlPanelViewState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? devices = null,Object? selectedDevice = freezed,Object? positions = null,Object? selectedPosition = freezed,Object? isLoading = null,Object? errorMessage = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? devices = null,Object? selectedDevice = freezed,Object? positions = null,Object? selectedPosition = freezed,}) { return _then(_ControlPanelViewState( devices: null == devices ? _self._devices : devices // ignore: cast_nullable_to_non_nullable as List,selectedDevice: freezed == selectedDevice ? _self.selectedDevice : selectedDevice // ignore: cast_nullable_to_non_nullable as DeviceEntity?,positions: null == positions ? _self._positions : positions // ignore: cast_nullable_to_non_nullable as List,selectedPosition: freezed == selectedPosition ? _self.selectedPosition : selectedPosition // ignore: cast_nullable_to_non_nullable -as PositionEntity?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, +as PositionEntity?, )); } diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart index 0fdeb3e2..32e63515 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/activity_meter_screen.dart @@ -21,7 +21,7 @@ class ActivityMeterScreen extends ConsumerWidget { final theme = ref.watch(themePortProvider); final state = ref.watch(activityMeterViewModelProvider); final vm = ref.read(activityMeterViewModelProvider.notifier); - final device = ref.watch(selectedDeviceProvider); + final device = ref.watch(selectedDeviceProvider).value; ref.listen(activityMeterViewModelProvider.select((s) => s.errorEvent), ( previous, diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart index 35001727..8fe5961e 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/state/activity_meter_view_model.dart @@ -31,7 +31,7 @@ class ActivityMeterViewModel extends Notifier { return const ActivityMeterViewState(); } - String? get _identificator => ref.read(selectedDeviceProvider)?.identificator; + String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator; Future selectTimeRange(TimeRange range) async { if (range == state.timeRange) return; @@ -272,7 +272,7 @@ class ActivityMeterViewModel extends Notifier { } Future togglePedometer({required bool enabled}) async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return false; state = state.copyWith(errorEvent: null); diff --git a/modules/legacy/modules/device_management/lib/src/features/apps_use/presentation/state/apps_use_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/apps_use/presentation/state/apps_use_view_model.dart index c7f62e28..91f2dc63 100644 --- a/modules/legacy/modules/device_management/lib/src/features/apps_use/presentation/state/apps_use_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/apps_use/presentation/state/apps_use_view_model.dart @@ -30,7 +30,7 @@ class AppsUseViewModel extends Notifier { return const AppsUseViewState(); } - String? get _identificator => ref.read(selectedDeviceProvider)?.identificator; + String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator; Future selectTimeRange(TimeRange range) async { if (range == state.timeRange) return; diff --git a/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/state/background_image_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/state/background_image_view_model.dart index 9c26e703..ed3c1657 100644 --- a/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/state/background_image_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/state/background_image_view_model.dart @@ -32,7 +32,7 @@ class BackgroundImageViewModel extends Notifier { Future _load() async { try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final photos = await _repository.getPhotos(); @@ -68,7 +68,7 @@ class BackgroundImageViewModel extends Notifier { ); if (image == null) return; - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( @@ -108,7 +108,7 @@ class BackgroundImageViewModel extends Notifier { } Future setAsBackground(String photoId) async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( diff --git a/modules/legacy/modules/device_management/lib/src/features/call_history/presentation/state/call_history_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/call_history/presentation/state/call_history_view_model.dart index 5b098bcd..5114f7b9 100644 --- a/modules/legacy/modules/device_management/lib/src/features/call_history/presentation/state/call_history_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/call_history/presentation/state/call_history_view_model.dart @@ -27,7 +27,7 @@ class CallHistoryViewModel extends Notifier { } Future _load() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) { state = state.copyWith(isLoading: false); return; diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart index 4cb4515b..b81086a7 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/state/contacts_view_model.dart @@ -188,7 +188,7 @@ class ContactsViewModel extends Notifier { String userId, List contacts, ) async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; try { diff --git a/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart b/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart index fe4d5d49..60c765f2 100644 --- a/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart @@ -57,7 +57,7 @@ class _HealthScreenState extends ConsumerState final theme = ref.watch(themePortProvider); final state = ref.watch(healthViewModelProvider); final vm = ref.read(healthViewModelProvider.notifier); - final device = ref.watch(selectedDeviceProvider); + final device = ref.watch(selectedDeviceProvider).value; ref.listen(healthViewModelProvider.select((s) => s.errorEvent), ( previous, diff --git a/modules/legacy/modules/device_management/lib/src/features/health/presentation/state/health_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/health/presentation/state/health_view_model.dart index 16034b66..69764823 100644 --- a/modules/legacy/modules/device_management/lib/src/features/health/presentation/state/health_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/health/presentation/state/health_view_model.dart @@ -65,7 +65,7 @@ class HealthViewModel extends Notifier { }); } - String? get _identificator => ref.read(selectedDeviceProvider)?.identificator; + String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator; Future selectTimeRange(TimeRange range) async { if (range == state.timeRange) return; @@ -310,7 +310,7 @@ class HealthViewModel extends Notifier { } Future updateHeartRateFrequency({required int frequency}) async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return false; try { @@ -339,7 +339,7 @@ class HealthViewModel extends Notifier { } Future measure() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; try { diff --git a/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/state/locate_device_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/state/locate_device_view_model.dart index a8b415b3..0d69677d 100644 --- a/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/state/locate_device_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/state/locate_device_view_model.dart @@ -20,7 +20,7 @@ class LocateDeviceViewModel extends Notifier { _commandsRepository = ref.read(commandsRepositoryProvider); _tracking = ref.read(sfTrackingProvider); - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; return LocateDeviceViewState(device: device); } diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart index c3618bb2..8b89243d 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/state/remote_connection_view_model.dart @@ -44,7 +44,7 @@ class RemoteConnectionViewModel extends Notifier { } Future load() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith(deviceId: device.identificator); diff --git a/modules/legacy/modules/device_management/lib/src/features/rewards/presentation/state/rewards_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/rewards/presentation/state/rewards_view_model.dart index e50f92ad..7ae02021 100644 --- a/modules/legacy/modules/device_management/lib/src/features/rewards/presentation/state/rewards_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/rewards/presentation/state/rewards_view_model.dart @@ -62,7 +62,7 @@ class RewardsViewModel extends Notifier { try { state = state.copyWith(isLoading: true, isComplete: false); - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; final request = SendCommandRequestModel( device: device!.identificator.toString(), command: DeviceCommand.rewards, diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/state/scheduled_activities_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/state/scheduled_activities_view_model.dart index 8b73bb6e..37855e34 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/state/scheduled_activities_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/state/scheduled_activities_view_model.dart @@ -33,7 +33,7 @@ class ScheduledActivitiesViewModel return const ScheduledActivitiesViewState(); } - String? get _deviceId => ref.read(selectedDeviceProvider)?.id; + String? get _deviceId => ref.read(selectedDeviceProvider).value?.id; Future _init() async { if (_deviceId == null) { diff --git a/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/state/volume_control_view_model.dart b/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/state/volume_control_view_model.dart index 4bf3b34f..9ccf5486 100644 --- a/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/state/volume_control_view_model.dart +++ b/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/state/volume_control_view_model.dart @@ -27,7 +27,7 @@ class VolumeControlViewModel extends Notifier { Future _load() async { try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final volume = device.settings.volume; diff --git a/modules/legacy/modules/legacy_auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart b/modules/legacy/modules/legacy_auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart index 59d08006..bed4d993 100644 --- a/modules/legacy/modules/legacy_auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart +++ b/modules/legacy/modules/legacy_auth/lib/src/features/device_setup/presentation/state/device_setup_view_model.dart @@ -8,6 +8,7 @@ import 'package:legacy_auth/src/features/device_setup/presentation/state/device_ import 'package:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_shared/legacy_shared.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:sf_tracking/sf_tracking.dart'; import 'package:utils/utils.dart'; @@ -195,6 +196,10 @@ class LegacyDeviceSetupViewModel extends Notifier { if (!ref.mounted) return false; + // We don't have the new DeviceEntity locally (the create endpoint + // returns void), so re-read from the backend. + await ref.read(legacyDevicesProvider.notifier).refresh(); + unawaited( _tracking.legacyDeviceSetupCompleted( childGender: genrer, diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart index 74ba2110..e77f2daa 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart @@ -14,116 +14,127 @@ class LocationScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themePortProvider); - final controlPanelState = ref.watch(controlPanelViewModelProvider); - final locationState = ref.watch(locationViewModelProvider); + final asyncControlPanelState = ref.watch(controlPanelViewModelProvider); + final asyncLocationState = ref.watch(locationViewModelProvider); - ref.listen(locationViewModelProvider.select((s) => s.errorEvent), ( - previous, - next, - ) { - if (next != null) { - final message = switch (next) { - LocationErrorEvent.geofenceCreate => context.translate( - I18n.errorGeofenceCreate, - ), - LocationErrorEvent.geofenceUpdate => context.translate( - I18n.errorGeofenceUpdate, - ), - LocationErrorEvent.geofenceDelete => context.translate( - I18n.errorGeofenceDelete, - ), - LocationErrorEvent.frequentPlaceCreate => context.translate( - I18n.errorFrequentPlaceCreate, - ), - LocationErrorEvent.frequentPlaceUpdate => context.translate( - I18n.errorFrequentPlaceUpdate, - ), - LocationErrorEvent.frequentPlaceDelete => context.translate( - I18n.errorFrequentPlaceDelete, - ), - LocationErrorEvent.positionHistory => context.translate( - I18n.errorPositionHistory, - ), - LocationErrorEvent.locationFrequency => context.translate( - I18n.errorLocationFrequency, - ), - }; - showTopSnackbar(context, message: message, type: MessageType.error); - } - }); + ref.listen( + locationViewModelProvider.select((s) => s.value?.errorEvent), + (previous, next) { + if (next != null) { + final message = switch (next) { + LocationErrorEvent.geofenceCreate => context.translate( + I18n.errorGeofenceCreate, + ), + LocationErrorEvent.geofenceUpdate => context.translate( + I18n.errorGeofenceUpdate, + ), + LocationErrorEvent.geofenceDelete => context.translate( + I18n.errorGeofenceDelete, + ), + LocationErrorEvent.frequentPlaceCreate => context.translate( + I18n.errorFrequentPlaceCreate, + ), + LocationErrorEvent.frequentPlaceUpdate => context.translate( + I18n.errorFrequentPlaceUpdate, + ), + LocationErrorEvent.frequentPlaceDelete => context.translate( + I18n.errorFrequentPlaceDelete, + ), + LocationErrorEvent.positionHistory => context.translate( + I18n.errorPositionHistory, + ), + LocationErrorEvent.locationFrequency => context.translate( + I18n.errorLocationFrequency, + ), + }; + showTopSnackbar(context, message: message, type: MessageType.error); + } + }, + ); - ref.listen(controlPanelViewModelProvider.select((s) => s.errorMessage), ( - previous, - next, - ) { - if (next.isNotEmpty) { - showTopSnackbar( - context, - message: context.translate(I18n.errorGeneric), - type: MessageType.error, - ); - } - }); - - ref.listen(locationViewModelProvider.select((s) => s.successMessage), ( - previous, - next, - ) { - if (next != null) { - final message = switch (next) { - LocationSuccessEvent.geofenceCreated => context.translate( - I18n.geofenceCreated, - ), - LocationSuccessEvent.geofenceUpdated => context.translate( - I18n.geofenceUpdated, - ), - LocationSuccessEvent.geofenceDeleted => context.translate( - I18n.geofenceDeleted, - ), - LocationSuccessEvent.frequentPlaceCreated => context.translate( - I18n.frequentPlaceCreated, - ), - LocationSuccessEvent.frequentPlaceUpdated => context.translate( - I18n.frequentPlaceUpdated, - ), - LocationSuccessEvent.frequentPlaceDeleted => context.translate( - I18n.frequentPlaceDeleted, - ), - }; - showTopSnackbar(context, message: message, type: MessageType.success); - } - }); - - if (controlPanelState.isLoading) { - return LegacyPageLayout( - theme: theme, - title: context.translate(I18n.mapTitle), - showBack: false, - body: const Center(child: CircularProgressIndicator()), - ); - } + ref.listen( + locationViewModelProvider.select((s) => s.value?.successMessage), + (previous, next) { + if (next != null) { + final message = switch (next) { + LocationSuccessEvent.geofenceCreated => context.translate( + I18n.geofenceCreated, + ), + LocationSuccessEvent.geofenceUpdated => context.translate( + I18n.geofenceUpdated, + ), + LocationSuccessEvent.geofenceDeleted => context.translate( + I18n.geofenceDeleted, + ), + LocationSuccessEvent.frequentPlaceCreated => context.translate( + I18n.frequentPlaceCreated, + ), + LocationSuccessEvent.frequentPlaceUpdated => context.translate( + I18n.frequentPlaceUpdated, + ), + LocationSuccessEvent.frequentPlaceDeleted => context.translate( + I18n.frequentPlaceDeleted, + ), + }; + showTopSnackbar( + context, + message: message, + type: MessageType.success, + ); + } + }, + ); return LegacyPageLayout( theme: theme, title: context.translate(I18n.mapTitle), showBack: false, - body: LocationMap( - selectedPosition: controlPanelState.selectedPosition, - selectedDevice: controlPanelState.selectedDevice, - devices: controlPanelState.devices, - geofences: locationState.geofences, - frequentPlaces: locationState.frequentPlaces, - positionHistory: locationState.positionHistory, - showRouteTrail: locationState.showRouteTrail, - isLoadingHistory: locationState.isLoadingHistory, - onDeviceChanged: (device) { - ref - .read(controlPanelViewModelProvider.notifier) - .setSelectedDevice(device); - }, - onRefreshPosition: () { - ref.read(controlPanelViewModelProvider.notifier).refreshPositions(); - }, + body: asyncControlPanelState.when( + skipLoadingOnReload: true, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + context.translate(I18n.errorGeneric), + textAlign: TextAlign.center, + ), + ), + ), + data: (controlPanelState) => asyncLocationState.when( + skipLoadingOnReload: true, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, _) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Text( + context.translate(I18n.errorGeneric), + textAlign: TextAlign.center, + ), + ), + ), + data: (locationState) => LocationMap( + selectedPosition: controlPanelState.selectedPosition, + selectedDevice: controlPanelState.selectedDevice, + devices: controlPanelState.devices, + positions: controlPanelState.positions, + geofences: locationState.geofences, + frequentPlaces: locationState.frequentPlaces, + positionHistory: locationState.positionHistory, + showRouteTrail: locationState.showRouteTrail, + isLoadingHistory: locationState.isLoadingHistory, + onDeviceChanged: (device) { + ref + .read(controlPanelViewModelProvider.notifier) + .setSelectedDevice(device); + }, + onRefreshPosition: () { + ref + .read(controlPanelViewModelProvider.notifier) + .refreshPositions(); + }, + ), + ), ), ); } diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_model.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_model.dart index 2a429ae5..2a627559 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_model.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_model.dart @@ -16,42 +16,31 @@ import 'package:sf_shared/sf_shared.dart'; import 'package:sf_tracking/sf_tracking.dart'; import 'package:uuid/uuid.dart'; -final locationViewModelProvider = - NotifierProvider.autoDispose( - LocationViewModel.new, - ); +final locationViewModelProvider = AsyncNotifierProvider.autoDispose< + LocationViewModel, + LocationViewState>(LocationViewModel.new); -class LocationViewModel extends Notifier { - late final LocationRepository _locationRepository; - late final SfTrackingRepository _tracking; +class LocationViewModel extends AsyncNotifier { + late LocationRepository _locationRepository; + late SfTrackingRepository _tracking; @override - LocationViewState build() { + Future build() async { _locationRepository = ref.read(locationRepositoryProvider); _tracking = ref.read(sfTrackingProvider); - final device = ref.read(selectedDeviceProvider); - if (device != null) { - _fetchData(device.id); - } - return const LocationViewState(); - } - Future _fetchData(String deviceId) async { - try { - final results = await Future.wait([ - _locationRepository.getGeofences(deviceId: deviceId), - _locationRepository.getFrequentPlaces(deviceId: deviceId), - ]); - if (!ref.mounted) return; - state = state.copyWith( - geofences: results[0] as List, - frequentPlaces: results[1] as List, - isLoading: false, - ); - } catch (e) { - if (!ref.mounted) return; - state = state.copyWith(isLoading: false); - } + final device = ref.watch(selectedDeviceProvider).value; + if (device == null) return const LocationViewState(); + + final results = await Future.wait([ + _locationRepository.getGeofences(deviceId: device.id), + _locationRepository.getFrequentPlaces(deviceId: device.id), + ]); + + return LocationViewState( + geofences: results[0] as List, + frequentPlaces: results[1] as List, + ); } Future createGeofence({ @@ -61,14 +50,19 @@ class LocationViewModel extends Notifier { required double longitude, required double radius, }) async { - state = state.copyWith( - isSubmitting: true, - errorEvent: null, - successMessage: null, + final current = state.value; + if (current == null) return false; + + state = AsyncData( + current.copyWith( + isSubmitting: true, + errorEvent: null, + successMessage: null, + ), ); try { final user = await ref.read(userInfoProvider.future); - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; final request = CreateGeofenceRequestModel( id: const Uuid().v4(), name: name, @@ -88,10 +82,12 @@ class LocationViewModel extends Notifier { unawaited(_tracking.legacyLocationGeofenceCreated()); - state = state.copyWith( - geofences: [...state.geofences, created], - isSubmitting: false, - successMessage: LocationSuccessEvent.geofenceCreated, + state = AsyncData( + current.copyWith( + geofences: [...current.geofences, created], + isSubmitting: false, + successMessage: LocationSuccessEvent.geofenceCreated, + ), ); return true; } catch (e) { @@ -107,10 +103,15 @@ class LocationViewModel extends Notifier { required double longitude, required double radius, }) async { - state = state.copyWith( - isSubmitting: true, - errorEvent: null, - successMessage: null, + final current = state.value; + if (current == null) return false; + + state = AsyncData( + current.copyWith( + isSubmitting: true, + errorEvent: null, + successMessage: null, + ), ); try { final request = UpdateGeofenceRequestModel( @@ -128,12 +129,14 @@ class LocationViewModel extends Notifier { unawaited(_tracking.legacyLocationGeofenceUpdated()); - state = state.copyWith( - geofences: state.geofences - .map((g) => g.id == id ? updated : g) - .toList(), - isSubmitting: false, - successMessage: LocationSuccessEvent.geofenceUpdated, + state = AsyncData( + current.copyWith( + geofences: current.geofences + .map((g) => g.id == id ? updated : g) + .toList(), + isSubmitting: false, + successMessage: LocationSuccessEvent.geofenceUpdated, + ), ); return true; } catch (e) { @@ -142,16 +145,23 @@ class LocationViewModel extends Notifier { } Future deleteGeofence({required String id}) async { - state = state.copyWith(errorEvent: null, successMessage: null); + final current = state.value; + if (current == null) return false; + + state = AsyncData( + current.copyWith(errorEvent: null, successMessage: null), + ); try { await _locationRepository.deleteGeofence(geofenceId: id); if (!ref.mounted) return false; unawaited(_tracking.legacyLocationGeofenceDeleted()); - state = state.copyWith( - geofences: state.geofences.where((g) => g.id != id).toList(), - successMessage: LocationSuccessEvent.geofenceDeleted, + state = AsyncData( + current.copyWith( + geofences: current.geofences.where((g) => g.id != id).toList(), + successMessage: LocationSuccessEvent.geofenceDeleted, + ), ); return true; } catch (e) { @@ -165,14 +175,19 @@ class LocationViewModel extends Notifier { required double lng, List wifiList = const [], }) async { - state = state.copyWith( - isSubmitting: true, - errorEvent: null, - successMessage: null, + final current = state.value; + if (current == null) return false; + + state = AsyncData( + current.copyWith( + isSubmitting: true, + errorEvent: null, + successMessage: null, + ), ); try { final user = await ref.read(userInfoProvider.future); - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; final request = CreateFrequentPlaceRequestModel( id: const Uuid().v4(), name: name, @@ -199,10 +214,12 @@ class LocationViewModel extends Notifier { unawaited(_tracking.legacyLocationFrequentPlaceCreated()); - state = state.copyWith( - frequentPlaces: [...state.frequentPlaces, created], - isSubmitting: false, - successMessage: LocationSuccessEvent.frequentPlaceCreated, + state = AsyncData( + current.copyWith( + frequentPlaces: [...current.frequentPlaces, created], + isSubmitting: false, + successMessage: LocationSuccessEvent.frequentPlaceCreated, + ), ); return true; } catch (e) { @@ -217,10 +234,15 @@ class LocationViewModel extends Notifier { required double lng, List wifiList = const [], }) async { - state = state.copyWith( - isSubmitting: true, - errorEvent: null, - successMessage: null, + final current = state.value; + if (current == null) return false; + + state = AsyncData( + current.copyWith( + isSubmitting: true, + errorEvent: null, + successMessage: null, + ), ); try { final request = UpdateFrequentPlaceRequestModel( @@ -245,12 +267,14 @@ class LocationViewModel extends Notifier { unawaited(_tracking.legacyLocationFrequentPlaceUpdated()); - state = state.copyWith( - frequentPlaces: state.frequentPlaces - .map((f) => f.id == id ? updated : f) - .toList(), - isSubmitting: false, - successMessage: LocationSuccessEvent.frequentPlaceUpdated, + state = AsyncData( + current.copyWith( + frequentPlaces: current.frequentPlaces + .map((f) => f.id == id ? updated : f) + .toList(), + isSubmitting: false, + successMessage: LocationSuccessEvent.frequentPlaceUpdated, + ), ); return true; } catch (e) { @@ -259,16 +283,24 @@ class LocationViewModel extends Notifier { } Future deleteFrequentPlace({required String id}) async { - state = state.copyWith(errorEvent: null, successMessage: null); + final current = state.value; + if (current == null) return false; + + state = AsyncData( + current.copyWith(errorEvent: null, successMessage: null), + ); try { await _locationRepository.deleteFrequentPlace(frequentPlaceId: id); if (!ref.mounted) return false; unawaited(_tracking.legacyLocationFrequentPlaceDeleted()); - state = state.copyWith( - frequentPlaces: state.frequentPlaces.where((f) => f.id != id).toList(), - successMessage: LocationSuccessEvent.frequentPlaceDeleted, + state = AsyncData( + current.copyWith( + frequentPlaces: + current.frequentPlaces.where((f) => f.id != id).toList(), + successMessage: LocationSuccessEvent.frequentPlaceDeleted, + ), ); return true; } catch (e) { @@ -280,10 +312,14 @@ class LocationViewModel extends Notifier { required DateTime from, required DateTime to, }) async { - final device = ref.read(selectedDeviceProvider); + final current = state.value; + if (current == null) return; + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; - state = state.copyWith(isLoadingHistory: true, errorEvent: null); + state = AsyncData( + current.copyWith(isLoadingHistory: true, errorEvent: null), + ); try { final positions = await _locationRepository.getPositionHistory( deviceIdentificator: device.identificator, @@ -294,38 +330,54 @@ class LocationViewModel extends Notifier { unawaited(_tracking.legacyLocationHistoryLoaded()); - state = state.copyWith( - positionHistory: positions, - isLoadingHistory: false, - showRouteTrail: positions.isNotEmpty, + state = AsyncData( + current.copyWith( + positionHistory: positions, + isLoadingHistory: false, + showRouteTrail: positions.isNotEmpty, + ), ); } catch (e) { if (!ref.mounted) return; - state = state.copyWith( - isLoadingHistory: false, - errorEvent: LocationErrorEvent.positionHistory, + state = AsyncData( + current.copyWith( + isLoadingHistory: false, + errorEvent: LocationErrorEvent.positionHistory, + ), ); } } void clearPositionHistory() { - if (state.positionHistory.isNotEmpty) { + final current = state.value; + if (current == null) return; + + if (current.positionHistory.isNotEmpty) { unawaited(_tracking.legacyLocationHistoryCleared()); } - state = state.copyWith(positionHistory: [], showRouteTrail: false); + state = AsyncData( + current.copyWith(positionHistory: [], showRouteTrail: false), + ); } void toggleRouteTrail() { - final newVisible = !state.showRouteTrail; + final current = state.value; + if (current == null) return; + + final newVisible = !current.showRouteTrail; unawaited(_tracking.legacyLocationMapRouteTrailToggled(newVisible)); - state = state.copyWith(showRouteTrail: newVisible); + state = AsyncData(current.copyWith(showRouteTrail: newVisible)); } Future updateLocationFrequency({required int frequency}) async { - final device = ref.read(selectedDeviceProvider); + final current = state.value; + if (current == null) return false; + final device = ref.read(selectedDeviceProvider).value; if (device == null) return false; - state = state.copyWith(isSubmitting: true, errorEvent: null); + state = AsyncData( + current.copyWith(isSubmitting: true, errorEvent: null), + ); try { final updatedSettings = device.settings.copyWith(frequency: frequency); @@ -341,7 +393,7 @@ class LocationViewModel extends Notifier { unawaited(_tracking.legacyLocationFrequencyUpdated(frequency)); - state = state.copyWith(isSubmitting: false); + state = AsyncData(current.copyWith(isSubmitting: false)); return true; } catch (e) { return _handleErrorEvent(LocationErrorEvent.locationFrequency); @@ -350,7 +402,11 @@ class LocationViewModel extends Notifier { bool _handleErrorEvent(LocationErrorEvent event) { if (!ref.mounted) return false; - state = state.copyWith(isSubmitting: false, errorEvent: event); + final current = state.value; + if (current == null) return false; + state = AsyncData( + current.copyWith(isSubmitting: false, errorEvent: event), + ); return false; } } diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.dart index 96bd8921..4f4b070f 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.dart @@ -31,7 +31,6 @@ abstract class LocationViewState with _$LocationViewState { @Default([]) List geofences, @Default([]) List frequentPlaces, @Default([]) List positionHistory, - @Default(true) bool isLoading, @Default(false) bool isLoadingHistory, @Default(false) bool isSubmitting, @Default(false) bool showRouteTrail, diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.freezed.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.freezed.dart index 2e248c8b..ce7572c2 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.freezed.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/state/location_view_state.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$LocationViewState { - List get geofences; List get frequentPlaces; List get positionHistory; bool get isLoading; bool get isLoadingHistory; bool get isSubmitting; bool get showRouteTrail; LocationErrorEvent? get errorEvent; LocationSuccessEvent? get successMessage; + List get geofences; List get frequentPlaces; List get positionHistory; bool get isLoadingHistory; bool get isSubmitting; bool get showRouteTrail; LocationErrorEvent? get errorEvent; LocationSuccessEvent? get successMessage; /// Create a copy of LocationViewState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $LocationViewStateCopyWith get copyWith => _$LocationViewStat @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationViewState&&const DeepCollectionEquality().equals(other.geofences, geofences)&&const DeepCollectionEquality().equals(other.frequentPlaces, frequentPlaces)&&const DeepCollectionEquality().equals(other.positionHistory, positionHistory)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationViewState&&const DeepCollectionEquality().equals(other.geofences, geofences)&&const DeepCollectionEquality().equals(other.frequentPlaces, frequentPlaces)&&const DeepCollectionEquality().equals(other.positionHistory, positionHistory)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(geofences),const DeepCollectionEquality().hash(frequentPlaces),const DeepCollectionEquality().hash(positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(geofences),const DeepCollectionEquality().hash(frequentPlaces),const DeepCollectionEquality().hash(positionHistory),isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage); @override String toString() { - return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)'; + return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)'; } @@ -45,7 +45,7 @@ abstract mixin class $LocationViewStateCopyWith<$Res> { factory $LocationViewStateCopyWith(LocationViewState value, $Res Function(LocationViewState) _then) = _$LocationViewStateCopyWithImpl; @useResult $Res call({ - List geofences, List frequentPlaces, List positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage + List geofences, List frequentPlaces, List positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage }); @@ -62,13 +62,12 @@ class _$LocationViewStateCopyWithImpl<$Res> /// Create a copy of LocationViewState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoading = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) { return _then(_self.copyWith( geofences: null == geofences ? _self.geofences : geofences // ignore: cast_nullable_to_non_nullable as List,frequentPlaces: null == frequentPlaces ? _self.frequentPlaces : frequentPlaces // ignore: cast_nullable_to_non_nullable as List,positionHistory: null == positionHistory ? _self.positionHistory : positionHistory // ignore: cast_nullable_to_non_nullable -as List,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable +as List,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable as bool,showRouteTrail: null == showRouteTrail ? _self.showRouteTrail : showRouteTrail // ignore: cast_nullable_to_non_nullable as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable @@ -158,10 +157,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List geofences, List frequentPlaces, List positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List geofences, List frequentPlaces, List positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _LocationViewState() when $default != null: -return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _: +return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _: return orElse(); } @@ -179,10 +178,10 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List geofences, List frequentPlaces, List positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List geofences, List frequentPlaces, List positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage) $default,) {final _that = this; switch (_that) { case _LocationViewState(): -return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _: +return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _: throw StateError('Unexpected subclass'); } @@ -199,10 +198,10 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List geofences, List frequentPlaces, List positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List geofences, List frequentPlaces, List positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,) {final _that = this; switch (_that) { case _LocationViewState() when $default != null: -return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _: +return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _: return null; } @@ -214,7 +213,7 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that class _LocationViewState implements LocationViewState { - const _LocationViewState({final List geofences = const [], final List frequentPlaces = const [], final List positionHistory = const [], this.isLoading = true, this.isLoadingHistory = false, this.isSubmitting = false, this.showRouteTrail = false, this.errorEvent, this.successMessage}): _geofences = geofences,_frequentPlaces = frequentPlaces,_positionHistory = positionHistory; + const _LocationViewState({final List geofences = const [], final List frequentPlaces = const [], final List positionHistory = const [], this.isLoadingHistory = false, this.isSubmitting = false, this.showRouteTrail = false, this.errorEvent, this.successMessage}): _geofences = geofences,_frequentPlaces = frequentPlaces,_positionHistory = positionHistory; final List _geofences; @@ -238,7 +237,6 @@ class _LocationViewState implements LocationViewState { return EqualUnmodifiableListView(_positionHistory); } -@override@JsonKey() final bool isLoading; @override@JsonKey() final bool isLoadingHistory; @override@JsonKey() final bool isSubmitting; @override@JsonKey() final bool showRouteTrail; @@ -255,16 +253,16 @@ _$LocationViewStateCopyWith<_LocationViewState> get copyWith => __$LocationViewS @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationViewState&&const DeepCollectionEquality().equals(other._geofences, _geofences)&&const DeepCollectionEquality().equals(other._frequentPlaces, _frequentPlaces)&&const DeepCollectionEquality().equals(other._positionHistory, _positionHistory)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationViewState&&const DeepCollectionEquality().equals(other._geofences, _geofences)&&const DeepCollectionEquality().equals(other._frequentPlaces, _frequentPlaces)&&const DeepCollectionEquality().equals(other._positionHistory, _positionHistory)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_geofences),const DeepCollectionEquality().hash(_frequentPlaces),const DeepCollectionEquality().hash(_positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_geofences),const DeepCollectionEquality().hash(_frequentPlaces),const DeepCollectionEquality().hash(_positionHistory),isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage); @override String toString() { - return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)'; + return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)'; } @@ -275,7 +273,7 @@ abstract mixin class _$LocationViewStateCopyWith<$Res> implements $LocationViewS factory _$LocationViewStateCopyWith(_LocationViewState value, $Res Function(_LocationViewState) _then) = __$LocationViewStateCopyWithImpl; @override @useResult $Res call({ - List geofences, List frequentPlaces, List positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage + List geofences, List frequentPlaces, List positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage }); @@ -292,13 +290,12 @@ class __$LocationViewStateCopyWithImpl<$Res> /// Create a copy of LocationViewState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoading = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) { return _then(_LocationViewState( geofences: null == geofences ? _self._geofences : geofences // ignore: cast_nullable_to_non_nullable as List,frequentPlaces: null == frequentPlaces ? _self._frequentPlaces : frequentPlaces // ignore: cast_nullable_to_non_nullable as List,positionHistory: null == positionHistory ? _self._positionHistory : positionHistory // ignore: cast_nullable_to_non_nullable -as List,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable -as bool,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable +as List,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable as bool,showRouteTrail: null == showRouteTrail ? _self.showRouteTrail : showRouteTrail // ignore: cast_nullable_to_non_nullable as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart index bf0c9adb..27a12f86 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart @@ -119,10 +119,10 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> { final theme = ref.watch(themePortProvider); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); final isSubmitting = ref.watch( - locationViewModelProvider.select((s) => s.isSubmitting), + locationViewModelProvider.select((s) => s.value?.isSubmitting ?? false), ); final errorEvent = ref.watch( - locationViewModelProvider.select((s) => s.errorEvent), + locationViewModelProvider.select((s) => s.value?.errorEvent), ); return DraggableScrollableSheet( diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart index bb777e5e..1263328a 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart @@ -124,10 +124,10 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> { final theme = ref.watch(themePortProvider); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); final isSubmitting = ref.watch( - locationViewModelProvider.select((s) => s.isSubmitting), + locationViewModelProvider.select((s) => s.value?.isSubmitting ?? false), ); final errorEvent = ref.watch( - locationViewModelProvider.select((s) => s.errorEvent), + locationViewModelProvider.select((s) => s.value?.errorEvent), ); return DraggableScrollableSheet( diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart index f5321dd6..3dbaddea 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/device_banner.dart @@ -7,15 +7,15 @@ import 'package:utils/utils.dart'; class DeviceBanner extends ConsumerStatefulWidget { final DeviceEntity device; - final PositionEntity? position; final List devices; + final List positions; final ValueChanged onDeviceChanged; const DeviceBanner({ super.key, required this.device, - required this.position, required this.devices, + required this.positions, required this.onDeviceChanged, }); @@ -90,8 +90,9 @@ class _DeviceBannerState extends ConsumerState { }, itemBuilder: (context, index) { final dev = widget.devices[index]; - final isSelected = dev.id == widget.device.id; - final pos = isSelected ? widget.position : null; + final pos = widget.positions + .where((p) => p.deviceIdentificator == dev.identificator) + .firstOrNull; return _DeviceCard( device: dev, position: pos, diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart index 8608b486..993000b6 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart @@ -34,11 +34,13 @@ import 'route_history_layer.dart'; const _defaultCenter = LatLng(40.4168, -3.7038); const _defaultZoom = 17.0; +const _noPositionZoom = 5.8; class LocationMap extends ConsumerStatefulWidget { final PositionEntity? selectedPosition; final DeviceEntity? selectedDevice; final List devices; + final List positions; final List geofences; final List frequentPlaces; final List positionHistory; @@ -52,6 +54,7 @@ class LocationMap extends ConsumerStatefulWidget { required this.selectedPosition, required this.selectedDevice, required this.devices, + required this.positions, required this.geofences, required this.frequentPlaces, required this.positionHistory, @@ -103,14 +106,21 @@ class _LocationMapState extends ConsumerState @override void didUpdateWidget(LocationMap oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.selectedDevice?.id != oldWidget.selectedDevice?.id) { + + final deviceChanged = + widget.selectedDevice?.id != oldWidget.selectedDevice?.id; + + if (deviceChanged) { _startMonitoring(); - } - if (widget.selectedPosition != null && + if (widget.selectedPosition != null) { + _centerOnDevice(); + } + } else if (widget.selectedPosition != null && widget.selectedPosition != oldWidget.selectedPosition) { final mapState = ref.read(locationMapViewModelProvider); if (mapState.isFollowing) _centerOnDevice(); } + if (widget.positionHistory.length > 1 && widget.showRouteTrail && (oldWidget.positionHistory.length != widget.positionHistory.length || @@ -323,7 +333,8 @@ class _LocationMapState extends ConsumerState void _showListSheet() { unawaited(ref.read(sfTrackingProvider).legacyLocationListSheetOpened()); - final locationState = ref.read(locationViewModelProvider); + final locationState = ref.read(locationViewModelProvider).value; + if (locationState == null) return; final mapState = ref.read(locationMapViewModelProvider); showModalBottomSheet( context: context, @@ -472,8 +483,8 @@ class _LocationMapState extends ConsumerState alignment: Alignment.bottomCenter, child: DeviceBanner( device: widget.selectedDevice!, - position: widget.selectedPosition, devices: widget.devices, + positions: widget.positions, onDeviceChanged: widget.onDeviceChanged, ), ), @@ -621,7 +632,7 @@ class _LocationMapState extends ConsumerState const SizedBox(height: 8), FrequencySelector( currentFrequency: - ref.watch(selectedDeviceProvider)?.settings.frequency ?? 60, + ref.watch(selectedDeviceProvider).value?.settings.frequency ?? 60, options: widget.selectedDevice!.capabilities!.location!.options, onChanged: _updateFrequency, ), @@ -715,12 +726,14 @@ class _LocationMapState extends ConsumerState @override Widget build(BuildContext context) { final mapState = ref.watch(locationMapViewModelProvider); - final initialCenter = widget.selectedPosition != null + final hasPosition = widget.selectedPosition != null; + final initialCenter = hasPosition ? LatLng( widget.selectedPosition!.latitude, widget.selectedPosition!.longitude, ) : _defaultCenter; + final initialZoom = hasPosition ? _defaultZoom : _noPositionZoom; return Stack( children: [ @@ -728,7 +741,7 @@ class _LocationMapState extends ConsumerState mapController: _mapController, options: MapOptions( initialCenter: initialCenter, - initialZoom: _defaultZoom, + initialZoom: initialZoom, minZoom: 5, keepAlive: true, onPositionChanged: (camera, _) { diff --git a/modules/legacy/modules/settings/lib/src/features/alarm/presentation/state/alarm_view_model.dart b/modules/legacy/modules/settings/lib/src/features/alarm/presentation/state/alarm_view_model.dart index 5b63a19e..c500560f 100644 --- a/modules/legacy/modules/settings/lib/src/features/alarm/presentation/state/alarm_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/alarm/presentation/state/alarm_view_model.dart @@ -38,7 +38,7 @@ class AlarmViewModel extends Notifier { Future _load() async { try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final alarms = await _repository.getAlarms(deviceId: device.id); @@ -55,7 +55,7 @@ class AlarmViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final id = const Uuid().v4(); diff --git a/modules/legacy/modules/settings/lib/src/features/alerts/presentation/state/alerts_view_model.dart b/modules/legacy/modules/settings/lib/src/features/alerts/presentation/state/alerts_view_model.dart index 78bdffa4..129d441d 100644 --- a/modules/legacy/modules/settings/lib/src/features/alerts/presentation/state/alerts_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/alerts/presentation/state/alerts_view_model.dart @@ -24,7 +24,7 @@ class AlertsViewModel extends Notifier { } void _load() { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final available = device.capabilities?.alerts?.types ?? []; @@ -48,7 +48,7 @@ class AlertsViewModel extends Notifier { } Future save() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( diff --git a/modules/legacy/modules/settings/lib/src/features/battery/presentation/state/battery_view_model.dart b/modules/legacy/modules/settings/lib/src/features/battery/presentation/state/battery_view_model.dart index 9851abf7..b8a9b61a 100644 --- a/modules/legacy/modules/settings/lib/src/features/battery/presentation/state/battery_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/battery/presentation/state/battery_view_model.dart @@ -24,7 +24,7 @@ class BatteryViewModel extends Notifier { } void _load() { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( @@ -34,7 +34,7 @@ class BatteryViewModel extends Notifier { } Future toggleNightMode(bool value) async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart index 890945d0..2e81b7df 100644 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/state/block_phone_view_model.dart @@ -28,7 +28,7 @@ class BlockPhoneViewModel extends Notifier { Future _load() async { try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final contacts = await _repository.getWhitelist(deviceId: device.id); @@ -45,7 +45,7 @@ class BlockPhoneViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final updatedContacts = [...state.contacts, contact]; @@ -79,7 +79,7 @@ class BlockPhoneViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final updatedContacts = [...state.contacts]..removeAt(index); diff --git a/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/state/disable_functions_view_model.dart b/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/state/disable_functions_view_model.dart index b35e3a9f..5a61c0ae 100644 --- a/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/state/disable_functions_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/state/disable_functions_view_model.dart @@ -25,7 +25,7 @@ class DisableFunctionsViewModel extends Notifier { } void _load() { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( @@ -44,7 +44,7 @@ class DisableFunctionsViewModel extends Notifier { } Future save() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_model.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_model.dart index 69caec42..a46db410 100644 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/language/presentation/state/language_view_model.dart @@ -27,7 +27,7 @@ class LanguageViewModel extends Notifier { Future load() async { state = state.copyWith(isLoading: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; state = state.copyWith( isLoading: false, device: device, diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart index 80d7af6c..d1201e97 100644 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/state/remote_management_view_model.dart @@ -29,7 +29,7 @@ class RemoteManagementViewModel extends Notifier { } Future _init() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; state = state.copyWith(deviceId: device!.identificator, isLoading: false); } diff --git a/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/state/sos_contacts_view_model.dart b/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/state/sos_contacts_view_model.dart index b03e0d08..3faacb66 100644 --- a/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/state/sos_contacts_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/sos_contacts/presentation/state/sos_contacts_view_model.dart @@ -28,7 +28,7 @@ class SosContactsViewModel extends Notifier { Future _load() async { try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final contacts = await _repository.getEmergencyContacts( @@ -47,7 +47,7 @@ class SosContactsViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final updatedContacts = [...state.contacts, contact]; @@ -81,7 +81,7 @@ class SosContactsViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final updatedContacts = [...state.contacts]..removeAt(index); diff --git a/modules/legacy/modules/settings/lib/src/features/sound/presentation/state/sound_view_model.dart b/modules/legacy/modules/settings/lib/src/features/sound/presentation/state/sound_view_model.dart index d881d040..e9204435 100644 --- a/modules/legacy/modules/settings/lib/src/features/sound/presentation/state/sound_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/sound/presentation/state/sound_view_model.dart @@ -24,7 +24,7 @@ class SoundViewModel extends Notifier { } Future load() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( diff --git a/modules/legacy/modules/settings/lib/src/features/sync_clock/presentation/state/sync_clock_view_model.dart b/modules/legacy/modules/settings/lib/src/features/sync_clock/presentation/state/sync_clock_view_model.dart index b2bdcc48..6cefd4ef 100644 --- a/modules/legacy/modules/settings/lib/src/features/sync_clock/presentation/state/sync_clock_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/sync_clock/presentation/state/sync_clock_view_model.dart @@ -29,7 +29,7 @@ class SyncClockViewModel extends Notifier { } Future load() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; setDevice(device!); } diff --git a/modules/legacy/modules/settings/lib/src/features/timezone/presentation/state/timezone_view_model.dart b/modules/legacy/modules/settings/lib/src/features/timezone/presentation/state/timezone_view_model.dart index 2e57f065..96d82a23 100644 --- a/modules/legacy/modules/settings/lib/src/features/timezone/presentation/state/timezone_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/timezone/presentation/state/timezone_view_model.dart @@ -24,7 +24,7 @@ class TimezoneViewModel extends Notifier { } void _load() { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; state = state.copyWith( @@ -38,7 +38,7 @@ class TimezoneViewModel extends Notifier { } Future save() async { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; if (state.timezone == device.settings.timezone) { diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart index 2e3bbfa3..77af60a3 100644 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/state/wifi_settings_view_model.dart @@ -28,7 +28,7 @@ class WifiSettingsViewModel extends Notifier { Future _load() async { try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; final networks = await _repository.getWifiNetworks(deviceId: device.id); @@ -45,7 +45,7 @@ class WifiSettingsViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; await _repository.createWifiNetwork( @@ -75,7 +75,7 @@ class WifiSettingsViewModel extends Notifier { state = state.copyWith(isSaving: true, errorMessage: ''); try { - final device = ref.read(selectedDeviceProvider); + final device = ref.read(selectedDeviceProvider).value; if (device == null) return; await _repository.deleteWifiNetwork(networkId: networkId); diff --git a/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart b/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart index cc662e4e..30f16fbf 100644 --- a/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart +++ b/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart @@ -14,5 +14,6 @@ export 'src/domain/repositories/command_repository.dart'; export 'src/providers/commands_repository_provider.dart'; export 'src/domain/repositories/devices_repository.dart'; export 'src/providers/devices_repository_provider.dart'; +export 'src/providers/legacy_devices_provider.dart'; export 'src/data/datasources/device_settings_update_datasource.dart'; export 'src/providers/device_settings_update_provider.dart'; diff --git a/modules/legacy/packages/legacy_shared/lib/src/providers/legacy_devices_provider.dart b/modules/legacy/packages/legacy_shared/lib/src/providers/legacy_devices_provider.dart new file mode 100644 index 00000000..f2842be4 --- /dev/null +++ b/modules/legacy/packages/legacy_shared/lib/src/providers/legacy_devices_provider.dart @@ -0,0 +1,41 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sf_shared/sf_shared.dart'; + +import 'devices_repository_provider.dart'; + +class LegacyDevices extends AsyncNotifier> { + @override + Future> build() { + return ref.read(sharedDevicesRepositoryProvider).getDevices(); + } + + void removeDevice(String deviceId) { + final current = state.value ?? const []; + state = AsyncData( + current.where((d) => d.id != deviceId).toList(growable: false), + ); + } + + void renameDevice({ + required String deviceId, + required String newCarrierName, + }) { + final current = state.value ?? const []; + state = AsyncData([ + for (final d in current) + if (d.id == deviceId) d.copyWith(carrierName: newCarrierName) else d, + ]); + } + + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref.read(sharedDevicesRepositoryProvider).getDevices(), + ); + } +} + +final legacyDevicesProvider = + AsyncNotifierProvider>( + LegacyDevices.new, +); diff --git a/modules/legacy/packages/legacy_shared/lib/src/providers/selected_device_provider.dart b/modules/legacy/packages/legacy_shared/lib/src/providers/selected_device_provider.dart index bad616f2..b6417056 100644 --- a/modules/legacy/packages/legacy_shared/lib/src/providers/selected_device_provider.dart +++ b/modules/legacy/packages/legacy_shared/lib/src/providers/selected_device_provider.dart @@ -1,26 +1,62 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:legacy_shared/src/providers/legacy_devices_provider.dart'; import 'package:sf_shared/sf_shared.dart'; +import 'package:shared_preferences/shared_preferences.dart'; -final selectedDeviceProvider = - NotifierProvider( - SelectedDeviceNotifier.new, - ); +const _prefsKey = 'legacy_selected_device_id'; -class SelectedDeviceNotifier extends Notifier { +class SelectedDeviceNotifier extends AsyncNotifier { @override - DeviceEntity? build() { - return null; + Future build() async { + final devices = await ref.watch(legacyDevicesProvider.future); + if (devices.isEmpty) return null; + + final prefs = await SharedPreferences.getInstance(); + final persistedId = prefs.getString(_prefsKey); + + if (persistedId != null) { + final found = devices.where((d) => d.id == persistedId).firstOrNull; + if (found != null) return found; + } + + return devices.first; } - void setSelectedDevice(DeviceEntity device) { - state = device; + Future setSelectedDevice(DeviceEntity device) async { + state = AsyncData(device); + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_prefsKey, device.id); + } catch (e) { + debugPrint('[SelectedDeviceNotifier] failed to persist selection: $e'); + } + } + + void updateSettings(DeviceSettingsEntity settings) { + final current = state.value; + if (current == null) return; + state = AsyncData(current.copyWith(settings: settings)); + } + + Future clear() async { + state = const AsyncData(null); + try { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_prefsKey); + } catch (e) { + debugPrint('[SelectedDeviceNotifier] failed to clear selection: $e'); + } } } +final selectedDeviceProvider = + AsyncNotifierProvider( + SelectedDeviceNotifier.new, +); + extension DeviceSettingsSync on Ref { void syncDeviceSettings(DeviceEntity device, DeviceSettingsEntity settings) { - read( - selectedDeviceProvider.notifier, - ).setSelectedDevice(device.copyWith(settings: settings)); + read(selectedDeviceProvider.notifier).updateSettings(settings); } }