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).
This commit is contained in:
2026-04-08 16:06:44 +02:00
parent d352aec5be
commit 63a4113d81
43 changed files with 619 additions and 462 deletions

View File

@@ -21,7 +21,7 @@ class AccountSettingsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final color = theme.getColorFor(ThemeCode.legacyPrimary); final color = theme.getColorFor(ThemeCode.legacyPrimary);
final selectedDevice = ref.watch(selectedDeviceProvider); final selectedDevice = ref.watch(selectedDeviceProvider).value;
final isLoggingOut = ref.watch( final isLoggingOut = ref.watch(
accountSettingsViewModelProvider.select((s) => s.isLoggingOut), accountSettingsViewModelProvider.select((s) => s.isLoggingOut),
); );

View File

@@ -81,6 +81,8 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
ref.invalidate(selectedDeviceProvider); ref.invalidate(selectedDeviceProvider);
} }
ref.read(legacyDevicesProvider.notifier).removeDevice(device.id);
unawaited(_tracking.legacyAccountLinkedDeviceUnlinked()); unawaited(_tracking.legacyAccountLinkedDeviceUnlinked());
state = state.copyWith( state = state.copyWith(
@@ -109,7 +111,11 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
if (deviceName.isEmpty) return; if (deviceName.isEmpty) return;
try { 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()); unawaited(_tracking.legacyAccountLinkedDeviceRenamed());
} catch (e) { } catch (e) {
state = state.copyWith( state = state.copyWith(

View File

@@ -1,5 +1,6 @@
import 'package:flutter_svg/svg.dart'; 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_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:control_panel/src/shared/widgets/device_map.dart';
import 'package:design_system/design_system.dart'; import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -18,52 +19,52 @@ class ControlPanelScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final state = ref.watch(controlPanelViewModelProvider); final asyncState = 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()),
);
}
return Scaffold( return Scaffold(
backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary), backgroundColor: theme.getColorFor(ThemeCode.backgroundPrimary),
body: SafeArea( body: asyncState.when(
child: Container( skipLoadingOnReload: true,
padding: EdgeInsets.symmetric(horizontal: 14), loading: () => const Center(child: CircularProgressIndicator()),
child: Column( error: (error, _) => Center(
children: [ child: Padding(
SizedBox(height: SizeUtils.getByScreen(small: 8, big: 4)), padding: const EdgeInsets.all(24),
_Header(), child: Text(
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)), formatErrorMessage(error),
Expanded( textAlign: TextAlign.center,
child: SingleChildScrollView( ),
child: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, ),
children: [ data: (state) => SafeArea(
_MenuSection(navigationContract: navigationContract), child: Container(
SizedBox( padding: const EdgeInsets.symmetric(horizontal: 14),
height: SizeUtils.getByScreen(small: 16, big: 22), child: Column(
), children: [
_MapSection(navigationContract: navigationContract), SizedBox(height: SizeUtils.getByScreen(small: 8, big: 4)),
SizedBox( _Header(state: state),
height: SizeUtils.getByScreen(small: 14, big: 13), 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 { class _Header extends ConsumerWidget {
const _Header(); final ControlPanelViewState state;
const _Header({required this.state});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(controlPanelViewModelProvider);
final vm = ref.read(controlPanelViewModelProvider.notifier); final vm = ref.read(controlPanelViewModelProvider.notifier);
return Stack( return Stack(
@@ -94,14 +96,14 @@ class _Header extends ConsumerWidget {
), ),
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 4)), SizedBox(width: SizeUtils.getByScreen(small: 8, big: 4)),
SizedBox( SizedBox(
width: SizeUtils.getByScreen(small: 130, big: 140), width: SizeUtils.getByScreen(small: 100, big: 110),
height: 32, height: 32,
child: CustomDropdown( child: CustomDropdown(
items: state.devices.map((DeviceEntity device) { items: state.devices.map((DeviceEntity device) {
final name = device.carrierName ?? '';
return Text( return Text(
name.length > 10 ? '${name.substring(0, 10)}...' : name, device.carrierName ?? '',
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1,
); );
}).toList(), }).toList(),
values: state.devices, values: state.devices,
@@ -216,14 +218,17 @@ class _SectionButton extends ConsumerWidget {
} }
class _MapSection extends ConsumerWidget { class _MapSection extends ConsumerWidget {
final ControlPanelViewState state;
final NavigationContract navigationContract; final NavigationContract navigationContract;
const _MapSection({required this.navigationContract}); const _MapSection({
required this.state,
required this.navigationContract,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.read(themePortProvider); final theme = ref.read(themePortProvider);
final state = ref.watch(controlPanelViewModelProvider);
final vm = ref.read(controlPanelViewModelProvider.notifier); final vm = ref.read(controlPanelViewModelProvider.notifier);
return GestureDetector( return GestureDetector(
@@ -243,7 +248,18 @@ class _MapSection extends ConsumerWidget {
), ),
), ),
IconButton( 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( icon: Icon(
Icons.refresh, Icons.refresh,
color: theme.getColorFor(ThemeCode.legacyPrimary), color: theme.getColorFor(ThemeCode.legacyPrimary),

View File

@@ -9,131 +9,114 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:sf_shared/sf_shared.dart'; import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart'; import 'package:sf_tracking/sf_tracking.dart';
final controlPanelViewModelProvider = final controlPanelViewModelProvider = AsyncNotifierProvider.autoDispose<
NotifierProvider.autoDispose<ControlPanelViewModel, ControlPanelViewState>( ControlPanelViewModel,
ControlPanelViewModel.new, ControlPanelViewState>(ControlPanelViewModel.new);
);
class ControlPanelViewModel extends Notifier<ControlPanelViewState> { class ControlPanelViewModel extends AsyncNotifier<ControlPanelViewState> {
late final ControlPanelRepository _controlPanelRepository; late ControlPanelRepository _controlPanelRepository;
late final SharedDevicesRepository _devicesRepository; late SfTrackingRepository _tracking;
late final SelectedDeviceNotifier _selectedDeviceNotifier;
late final SfTrackingRepository _tracking;
@override @override
ControlPanelViewState build() { Future<ControlPanelViewState> build() async {
_controlPanelRepository = ref.read(controlPanelRepositoryProvider); _controlPanelRepository = ref.read(controlPanelRepositoryProvider);
_devicesRepository = ref.read(sharedDevicesRepositoryProvider);
_selectedDeviceNotifier = ref.read(selectedDeviceProvider.notifier);
_tracking = ref.read(sfTrackingProvider); _tracking = ref.read(sfTrackingProvider);
_init();
return const ControlPanelViewState();
}
Future<void> _init() async { final devices = await ref.watch(legacyDevicesProvider.future);
try { if (devices.isEmpty) return const ControlPanelViewState();
final devices = await _devicesRepository.getDevices();
if (!ref.mounted) return;
if (devices.isEmpty) { final selected = await ref.watch(selectedDeviceProvider.future);
state = state.copyWith(isLoading: false);
return;
}
final previouslySelected = ref.read(selectedDeviceProvider); final positionLists = await Future.wait<List<PositionEntity>>(
final selected = devices.map(
previouslySelected != null && (d) => _controlPanelRepository.getLatestPositions(
devices.any( deviceId: d.identificator,
(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,
),
), ),
); ),
if (!ref.mounted) return; );
final latestPositions = _pickLatest(positionLists);
_applyPositions(positionLists); final selectedPosition = selected != null
state = state.copyWith(isLoading: false);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: formatErrorMessage(e),
);
}
}
void _applyPositions(List<List<PositionEntity>> 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
? latestPositions ? latestPositions
.where( .where((p) => p.deviceIdentificator == selected.identificator)
(p) =>
p.deviceIdentificator ==
state.selectedDevice!.identificator,
)
.firstOrNull .firstOrNull
: null; : null;
state = state.copyWith( return ControlPanelViewState(
devices: devices,
selectedDevice: selected,
positions: latestPositions, positions: latestPositions,
selectedPosition: selectedPosition, selectedPosition: selectedPosition,
); );
} }
Future<void> refreshPositions() async { Future<void> refreshPositions() async {
if (state.devices.isEmpty) return; final current = state.value;
state = state.copyWith(errorMessage: ''); if (current == null || current.devices.isEmpty) return;
try {
final positionLists = await Future.wait( final positionLists = await Future.wait<List<PositionEntity>>(
state.devices.map( current.devices.map(
(d) => _controlPanelRepository.getLatestPositions( (d) => _controlPanelRepository.getLatestPositions(
deviceId: d.identificator, deviceId: d.identificator,
),
), ),
); ),
if (!ref.mounted) return; );
_applyPositions(positionLists); if (!ref.mounted) return;
unawaited(_tracking.legacyControlPanelPositionsRefreshed());
} catch (e) { final latestPositions = _pickLatest(positionLists);
if (!ref.mounted) return; final selectedPosition = current.selectedDevice != null
state = state.copyWith(errorMessage: formatErrorMessage(e)); ? 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) { Future<void> setSelectedDevice(DeviceEntity device) async {
final selectedPosition = state.positions final current = state.value;
if (current == null) return;
final selectedPosition = current.positions
.where((p) => p.deviceIdentificator == device.identificator) .where((p) => p.deviceIdentificator == device.identificator)
.firstOrNull; .firstOrNull;
state = state.copyWith( state = AsyncData(
selectedDevice: device, current.copyWith(
selectedPosition: selectedPosition, selectedDevice: device,
selectedPosition: selectedPosition,
),
); );
_selectedDeviceNotifier.setSelectedDevice(device); await ref
.read(selectedDeviceProvider.notifier)
.setSelectedDevice(device);
unawaited( unawaited(
_tracking.legacyControlPanelDeviceSelected( _tracking.legacyControlPanelDeviceSelected(
totalDevices: state.devices.length, totalDevices: current.devices.length,
), ),
); );
} }
static List<PositionEntity> _pickLatest(
List<List<PositionEntity>> 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();
}
} }

View File

@@ -11,7 +11,5 @@ abstract class ControlPanelViewState with _$ControlPanelViewState {
DeviceEntity? selectedDevice, DeviceEntity? selectedDevice,
@Default([]) List<PositionEntity> positions, @Default([]) List<PositionEntity> positions,
PositionEntity? selectedPosition, PositionEntity? selectedPosition,
@Default(true) bool isLoading,
@Default('') String errorMessage,
}) = _ControlPanelViewState; }) = _ControlPanelViewState;
} }

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$ControlPanelViewState { mixin _$ControlPanelViewState {
List<DeviceEntity> get devices; DeviceEntity? get selectedDevice; List<PositionEntity> get positions; PositionEntity? get selectedPosition; bool get isLoading; String get errorMessage; List<DeviceEntity> get devices; DeviceEntity? get selectedDevice; List<PositionEntity> get positions; PositionEntity? get selectedPosition;
/// Create a copy of ControlPanelViewState /// Create a copy of ControlPanelViewState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $ControlPanelViewStateCopyWith<ControlPanelViewState> get copyWith => _$ControlP
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory $ControlPanelViewStateCopyWith(ControlPanelViewState value, $Res Function(ControlPanelViewState) _then) = _$ControlPanelViewStateCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition
}); });
@@ -62,15 +62,13 @@ class _$ControlPanelViewStateCopyWithImpl<$Res>
/// Create a copy of ControlPanelViewState /// Create a copy of ControlPanelViewState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
devices: null == devices ? _self.devices : devices // ignore: cast_nullable_to_non_nullable devices: null == devices ? _self.devices : devices // ignore: cast_nullable_to_non_nullable
as List<DeviceEntity>,selectedDevice: freezed == selectedDevice ? _self.selectedDevice : selectedDevice // ignore: cast_nullable_to_non_nullable as List<DeviceEntity>,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 DeviceEntity?,positions: null == positions ? _self.positions : positions // ignore: cast_nullable_to_non_nullable
as List<PositionEntity>,selectedPosition: freezed == selectedPosition ? _self.selectedPosition : selectedPosition // ignore: cast_nullable_to_non_nullable as List<PositionEntity>,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 PositionEntity?,
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }
/// Create a copy of ControlPanelViewState /// Create a copy of ControlPanelViewState
@@ -179,10 +177,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _ControlPanelViewState() when $default != null: 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(); return orElse();
} }
@@ -200,10 +198,10 @@ return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selecte
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _ControlPanelViewState(): 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'); throw StateError('Unexpected subclass');
} }
@@ -220,10 +218,10 @@ return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selecte
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _ControlPanelViewState() when $default != null: 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; return null;
} }
@@ -235,7 +233,7 @@ return $default(_that.devices,_that.selectedDevice,_that.positions,_that.selecte
class _ControlPanelViewState implements ControlPanelViewState { class _ControlPanelViewState implements ControlPanelViewState {
const _ControlPanelViewState({final List<DeviceEntity> devices = const [], this.selectedDevice, final List<PositionEntity> positions = const [], this.selectedPosition, this.isLoading = true, this.errorMessage = ''}): _devices = devices,_positions = positions; const _ControlPanelViewState({final List<DeviceEntity> devices = const [], this.selectedDevice, final List<PositionEntity> positions = const [], this.selectedPosition}): _devices = devices,_positions = positions;
final List<DeviceEntity> _devices; final List<DeviceEntity> _devices;
@@ -254,8 +252,6 @@ class _ControlPanelViewState implements ControlPanelViewState {
} }
@override final PositionEntity? selectedPosition; @override final PositionEntity? selectedPosition;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final String errorMessage;
/// Create a copy of ControlPanelViewState /// Create a copy of ControlPanelViewState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@@ -267,16 +263,16 @@ _$ControlPanelViewStateCopyWith<_ControlPanelViewState> get copyWith => __$Contr
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory _$ControlPanelViewStateCopyWith(_ControlPanelViewState value, $Res Function(_ControlPanelViewState) _then) = __$ControlPanelViewStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition, bool isLoading, String errorMessage List<DeviceEntity> devices, DeviceEntity? selectedDevice, List<PositionEntity> positions, PositionEntity? selectedPosition
}); });
@@ -304,15 +300,13 @@ class __$ControlPanelViewStateCopyWithImpl<$Res>
/// Create a copy of ControlPanelViewState /// Create a copy of ControlPanelViewState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_ControlPanelViewState(
devices: null == devices ? _self._devices : devices // ignore: cast_nullable_to_non_nullable devices: null == devices ? _self._devices : devices // ignore: cast_nullable_to_non_nullable
as List<DeviceEntity>,selectedDevice: freezed == selectedDevice ? _self.selectedDevice : selectedDevice // ignore: cast_nullable_to_non_nullable as List<DeviceEntity>,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 DeviceEntity?,positions: null == positions ? _self._positions : positions // ignore: cast_nullable_to_non_nullable
as List<PositionEntity>,selectedPosition: freezed == selectedPosition ? _self.selectedPosition : selectedPosition // ignore: cast_nullable_to_non_nullable as List<PositionEntity>,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 PositionEntity?,
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
)); ));
} }

View File

@@ -21,7 +21,7 @@ class ActivityMeterScreen extends ConsumerWidget {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final state = ref.watch(activityMeterViewModelProvider); final state = ref.watch(activityMeterViewModelProvider);
final vm = ref.read(activityMeterViewModelProvider.notifier); final vm = ref.read(activityMeterViewModelProvider.notifier);
final device = ref.watch(selectedDeviceProvider); final device = ref.watch(selectedDeviceProvider).value;
ref.listen(activityMeterViewModelProvider.select((s) => s.errorEvent), ( ref.listen(activityMeterViewModelProvider.select((s) => s.errorEvent), (
previous, previous,

View File

@@ -31,7 +31,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
return const ActivityMeterViewState(); return const ActivityMeterViewState();
} }
String? get _identificator => ref.read(selectedDeviceProvider)?.identificator; String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator;
Future<void> selectTimeRange(TimeRange range) async { Future<void> selectTimeRange(TimeRange range) async {
if (range == state.timeRange) return; if (range == state.timeRange) return;
@@ -272,7 +272,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
} }
Future<bool> togglePedometer({required bool enabled}) async { Future<bool> togglePedometer({required bool enabled}) async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return false; if (device == null) return false;
state = state.copyWith(errorEvent: null); state = state.copyWith(errorEvent: null);

View File

@@ -30,7 +30,7 @@ class AppsUseViewModel extends Notifier<AppsUseViewState> {
return const AppsUseViewState(); return const AppsUseViewState();
} }
String? get _identificator => ref.read(selectedDeviceProvider)?.identificator; String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator;
Future<void> selectTimeRange(TimeRange range) async { Future<void> selectTimeRange(TimeRange range) async {
if (range == state.timeRange) return; if (range == state.timeRange) return;

View File

@@ -32,7 +32,7 @@ class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final photos = await _repository.getPhotos(); final photos = await _repository.getPhotos();
@@ -68,7 +68,7 @@ class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
); );
if (image == null) return; if (image == null) return;
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(
@@ -108,7 +108,7 @@ class BackgroundImageViewModel extends Notifier<BackgroundImageViewState> {
} }
Future<void> setAsBackground(String photoId) async { Future<void> setAsBackground(String photoId) async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(

View File

@@ -27,7 +27,7 @@ class CallHistoryViewModel extends Notifier<CallHistoryViewState> {
} }
Future<void> _load() async { Future<void> _load() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) { if (device == null) {
state = state.copyWith(isLoading: false); state = state.copyWith(isLoading: false);
return; return;

View File

@@ -188,7 +188,7 @@ class ContactsViewModel extends Notifier<ContactsViewState> {
String userId, String userId,
List<ContactEntity> contacts, List<ContactEntity> contacts,
) async { ) async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
try { try {

View File

@@ -57,7 +57,7 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final state = ref.watch(healthViewModelProvider); final state = ref.watch(healthViewModelProvider);
final vm = ref.read(healthViewModelProvider.notifier); final vm = ref.read(healthViewModelProvider.notifier);
final device = ref.watch(selectedDeviceProvider); final device = ref.watch(selectedDeviceProvider).value;
ref.listen(healthViewModelProvider.select((s) => s.errorEvent), ( ref.listen(healthViewModelProvider.select((s) => s.errorEvent), (
previous, previous,

View File

@@ -65,7 +65,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
}); });
} }
String? get _identificator => ref.read(selectedDeviceProvider)?.identificator; String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator;
Future<void> selectTimeRange(TimeRange range) async { Future<void> selectTimeRange(TimeRange range) async {
if (range == state.timeRange) return; if (range == state.timeRange) return;
@@ -310,7 +310,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
} }
Future<bool> updateHeartRateFrequency({required int frequency}) async { Future<bool> updateHeartRateFrequency({required int frequency}) async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return false; if (device == null) return false;
try { try {
@@ -339,7 +339,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
} }
Future<void> measure() async { Future<void> measure() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
try { try {

View File

@@ -20,7 +20,7 @@ class LocateDeviceViewModel extends Notifier<LocateDeviceViewState> {
_commandsRepository = ref.read(commandsRepositoryProvider); _commandsRepository = ref.read(commandsRepositoryProvider);
_tracking = ref.read(sfTrackingProvider); _tracking = ref.read(sfTrackingProvider);
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
return LocateDeviceViewState(device: device); return LocateDeviceViewState(device: device);
} }

View File

@@ -44,7 +44,7 @@ class RemoteConnectionViewModel extends Notifier<RemoteConnectionViewState> {
} }
Future<void> load() async { Future<void> load() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith(deviceId: device.identificator); state = state.copyWith(deviceId: device.identificator);

View File

@@ -62,7 +62,7 @@ class RewardsViewModel extends Notifier<RewardsViewState> {
try { try {
state = state.copyWith(isLoading: true, isComplete: false); state = state.copyWith(isLoading: true, isComplete: false);
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
final request = SendCommandRequestModel( final request = SendCommandRequestModel(
device: device!.identificator.toString(), device: device!.identificator.toString(),
command: DeviceCommand.rewards, command: DeviceCommand.rewards,

View File

@@ -33,7 +33,7 @@ class ScheduledActivitiesViewModel
return const ScheduledActivitiesViewState(); return const ScheduledActivitiesViewState();
} }
String? get _deviceId => ref.read(selectedDeviceProvider)?.id; String? get _deviceId => ref.read(selectedDeviceProvider).value?.id;
Future<void> _init() async { Future<void> _init() async {
if (_deviceId == null) { if (_deviceId == null) {

View File

@@ -27,7 +27,7 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final volume = device.settings.volume; final volume = device.settings.volume;

View File

@@ -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:legacy_auth/src/features/device_setup/presentation/enums/add_kid_step.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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_localizations/sf_localizations.dart';
import 'package:sf_tracking/sf_tracking.dart'; import 'package:sf_tracking/sf_tracking.dart';
import 'package:utils/utils.dart'; import 'package:utils/utils.dart';
@@ -195,6 +196,10 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
if (!ref.mounted) return false; 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( unawaited(
_tracking.legacyDeviceSetupCompleted( _tracking.legacyDeviceSetupCompleted(
childGender: genrer, childGender: genrer,

View File

@@ -14,116 +14,127 @@ class LocationScreen extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final controlPanelState = ref.watch(controlPanelViewModelProvider); final asyncControlPanelState = ref.watch(controlPanelViewModelProvider);
final locationState = ref.watch(locationViewModelProvider); final asyncLocationState = ref.watch(locationViewModelProvider);
ref.listen(locationViewModelProvider.select((s) => s.errorEvent), ( ref.listen(
previous, locationViewModelProvider.select((s) => s.value?.errorEvent),
next, (previous, next) {
) { if (next != null) {
if (next != null) { final message = switch (next) {
final message = switch (next) { LocationErrorEvent.geofenceCreate => context.translate(
LocationErrorEvent.geofenceCreate => context.translate( I18n.errorGeofenceCreate,
I18n.errorGeofenceCreate, ),
), LocationErrorEvent.geofenceUpdate => context.translate(
LocationErrorEvent.geofenceUpdate => context.translate( I18n.errorGeofenceUpdate,
I18n.errorGeofenceUpdate, ),
), LocationErrorEvent.geofenceDelete => context.translate(
LocationErrorEvent.geofenceDelete => context.translate( I18n.errorGeofenceDelete,
I18n.errorGeofenceDelete, ),
), LocationErrorEvent.frequentPlaceCreate => context.translate(
LocationErrorEvent.frequentPlaceCreate => context.translate( I18n.errorFrequentPlaceCreate,
I18n.errorFrequentPlaceCreate, ),
), LocationErrorEvent.frequentPlaceUpdate => context.translate(
LocationErrorEvent.frequentPlaceUpdate => context.translate( I18n.errorFrequentPlaceUpdate,
I18n.errorFrequentPlaceUpdate, ),
), LocationErrorEvent.frequentPlaceDelete => context.translate(
LocationErrorEvent.frequentPlaceDelete => context.translate( I18n.errorFrequentPlaceDelete,
I18n.errorFrequentPlaceDelete, ),
), LocationErrorEvent.positionHistory => context.translate(
LocationErrorEvent.positionHistory => context.translate( I18n.errorPositionHistory,
I18n.errorPositionHistory, ),
), LocationErrorEvent.locationFrequency => context.translate(
LocationErrorEvent.locationFrequency => context.translate( I18n.errorLocationFrequency,
I18n.errorLocationFrequency, ),
), };
}; showTopSnackbar(context, message: message, type: MessageType.error);
showTopSnackbar(context, message: message, type: MessageType.error); }
} },
}); );
ref.listen(controlPanelViewModelProvider.select((s) => s.errorMessage), ( ref.listen(
previous, locationViewModelProvider.select((s) => s.value?.successMessage),
next, (previous, next) {
) { if (next != null) {
if (next.isNotEmpty) { final message = switch (next) {
showTopSnackbar( LocationSuccessEvent.geofenceCreated => context.translate(
context, I18n.geofenceCreated,
message: context.translate(I18n.errorGeneric), ),
type: MessageType.error, LocationSuccessEvent.geofenceUpdated => context.translate(
); I18n.geofenceUpdated,
} ),
}); LocationSuccessEvent.geofenceDeleted => context.translate(
I18n.geofenceDeleted,
ref.listen(locationViewModelProvider.select((s) => s.successMessage), ( ),
previous, LocationSuccessEvent.frequentPlaceCreated => context.translate(
next, I18n.frequentPlaceCreated,
) { ),
if (next != null) { LocationSuccessEvent.frequentPlaceUpdated => context.translate(
final message = switch (next) { I18n.frequentPlaceUpdated,
LocationSuccessEvent.geofenceCreated => context.translate( ),
I18n.geofenceCreated, LocationSuccessEvent.frequentPlaceDeleted => context.translate(
), I18n.frequentPlaceDeleted,
LocationSuccessEvent.geofenceUpdated => context.translate( ),
I18n.geofenceUpdated, };
), showTopSnackbar(
LocationSuccessEvent.geofenceDeleted => context.translate( context,
I18n.geofenceDeleted, message: message,
), type: MessageType.success,
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()),
);
}
return LegacyPageLayout( return LegacyPageLayout(
theme: theme, theme: theme,
title: context.translate(I18n.mapTitle), title: context.translate(I18n.mapTitle),
showBack: false, showBack: false,
body: LocationMap( body: asyncControlPanelState.when(
selectedPosition: controlPanelState.selectedPosition, skipLoadingOnReload: true,
selectedDevice: controlPanelState.selectedDevice, loading: () => const Center(child: CircularProgressIndicator()),
devices: controlPanelState.devices, error: (error, _) => Center(
geofences: locationState.geofences, child: Padding(
frequentPlaces: locationState.frequentPlaces, padding: const EdgeInsets.all(24),
positionHistory: locationState.positionHistory, child: Text(
showRouteTrail: locationState.showRouteTrail, context.translate(I18n.errorGeneric),
isLoadingHistory: locationState.isLoadingHistory, textAlign: TextAlign.center,
onDeviceChanged: (device) { ),
ref ),
.read(controlPanelViewModelProvider.notifier) ),
.setSelectedDevice(device); data: (controlPanelState) => asyncLocationState.when(
}, skipLoadingOnReload: true,
onRefreshPosition: () { loading: () => const Center(child: CircularProgressIndicator()),
ref.read(controlPanelViewModelProvider.notifier).refreshPositions(); 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();
},
),
),
), ),
); );
} }

View File

@@ -16,42 +16,31 @@ import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart'; import 'package:sf_tracking/sf_tracking.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
final locationViewModelProvider = final locationViewModelProvider = AsyncNotifierProvider.autoDispose<
NotifierProvider.autoDispose<LocationViewModel, LocationViewState>( LocationViewModel,
LocationViewModel.new, LocationViewState>(LocationViewModel.new);
);
class LocationViewModel extends Notifier<LocationViewState> { class LocationViewModel extends AsyncNotifier<LocationViewState> {
late final LocationRepository _locationRepository; late LocationRepository _locationRepository;
late final SfTrackingRepository _tracking; late SfTrackingRepository _tracking;
@override @override
LocationViewState build() { Future<LocationViewState> build() async {
_locationRepository = ref.read(locationRepositoryProvider); _locationRepository = ref.read(locationRepositoryProvider);
_tracking = ref.read(sfTrackingProvider); _tracking = ref.read(sfTrackingProvider);
final device = ref.read(selectedDeviceProvider);
if (device != null) {
_fetchData(device.id);
}
return const LocationViewState();
}
Future<void> _fetchData(String deviceId) async { final device = ref.watch(selectedDeviceProvider).value;
try { if (device == null) return const LocationViewState();
final results = await Future.wait([
_locationRepository.getGeofences(deviceId: deviceId), final results = await Future.wait([
_locationRepository.getFrequentPlaces(deviceId: deviceId), _locationRepository.getGeofences(deviceId: device.id),
]); _locationRepository.getFrequentPlaces(deviceId: device.id),
if (!ref.mounted) return; ]);
state = state.copyWith(
geofences: results[0] as List<GeofenceEntity>, return LocationViewState(
frequentPlaces: results[1] as List<FrequentPlaceEntity>, geofences: results[0] as List<GeofenceEntity>,
isLoading: false, frequentPlaces: results[1] as List<FrequentPlaceEntity>,
); );
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false);
}
} }
Future<bool> createGeofence({ Future<bool> createGeofence({
@@ -61,14 +50,19 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double longitude, required double longitude,
required double radius, required double radius,
}) async { }) async {
state = state.copyWith( final current = state.value;
isSubmitting: true, if (current == null) return false;
errorEvent: null,
successMessage: null, state = AsyncData(
current.copyWith(
isSubmitting: true,
errorEvent: null,
successMessage: null,
),
); );
try { try {
final user = await ref.read(userInfoProvider.future); final user = await ref.read(userInfoProvider.future);
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
final request = CreateGeofenceRequestModel( final request = CreateGeofenceRequestModel(
id: const Uuid().v4(), id: const Uuid().v4(),
name: name, name: name,
@@ -88,10 +82,12 @@ class LocationViewModel extends Notifier<LocationViewState> {
unawaited(_tracking.legacyLocationGeofenceCreated()); unawaited(_tracking.legacyLocationGeofenceCreated());
state = state.copyWith( state = AsyncData(
geofences: [...state.geofences, created], current.copyWith(
isSubmitting: false, geofences: [...current.geofences, created],
successMessage: LocationSuccessEvent.geofenceCreated, isSubmitting: false,
successMessage: LocationSuccessEvent.geofenceCreated,
),
); );
return true; return true;
} catch (e) { } catch (e) {
@@ -107,10 +103,15 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double longitude, required double longitude,
required double radius, required double radius,
}) async { }) async {
state = state.copyWith( final current = state.value;
isSubmitting: true, if (current == null) return false;
errorEvent: null,
successMessage: null, state = AsyncData(
current.copyWith(
isSubmitting: true,
errorEvent: null,
successMessage: null,
),
); );
try { try {
final request = UpdateGeofenceRequestModel( final request = UpdateGeofenceRequestModel(
@@ -128,12 +129,14 @@ class LocationViewModel extends Notifier<LocationViewState> {
unawaited(_tracking.legacyLocationGeofenceUpdated()); unawaited(_tracking.legacyLocationGeofenceUpdated());
state = state.copyWith( state = AsyncData(
geofences: state.geofences current.copyWith(
.map((g) => g.id == id ? updated : g) geofences: current.geofences
.toList(), .map((g) => g.id == id ? updated : g)
isSubmitting: false, .toList(),
successMessage: LocationSuccessEvent.geofenceUpdated, isSubmitting: false,
successMessage: LocationSuccessEvent.geofenceUpdated,
),
); );
return true; return true;
} catch (e) { } catch (e) {
@@ -142,16 +145,23 @@ class LocationViewModel extends Notifier<LocationViewState> {
} }
Future<bool> deleteGeofence({required String id}) async { Future<bool> 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 { try {
await _locationRepository.deleteGeofence(geofenceId: id); await _locationRepository.deleteGeofence(geofenceId: id);
if (!ref.mounted) return false; if (!ref.mounted) return false;
unawaited(_tracking.legacyLocationGeofenceDeleted()); unawaited(_tracking.legacyLocationGeofenceDeleted());
state = state.copyWith( state = AsyncData(
geofences: state.geofences.where((g) => g.id != id).toList(), current.copyWith(
successMessage: LocationSuccessEvent.geofenceDeleted, geofences: current.geofences.where((g) => g.id != id).toList(),
successMessage: LocationSuccessEvent.geofenceDeleted,
),
); );
return true; return true;
} catch (e) { } catch (e) {
@@ -165,14 +175,19 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double lng, required double lng,
List<WifiInfoEntity> wifiList = const [], List<WifiInfoEntity> wifiList = const [],
}) async { }) async {
state = state.copyWith( final current = state.value;
isSubmitting: true, if (current == null) return false;
errorEvent: null,
successMessage: null, state = AsyncData(
current.copyWith(
isSubmitting: true,
errorEvent: null,
successMessage: null,
),
); );
try { try {
final user = await ref.read(userInfoProvider.future); final user = await ref.read(userInfoProvider.future);
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
final request = CreateFrequentPlaceRequestModel( final request = CreateFrequentPlaceRequestModel(
id: const Uuid().v4(), id: const Uuid().v4(),
name: name, name: name,
@@ -199,10 +214,12 @@ class LocationViewModel extends Notifier<LocationViewState> {
unawaited(_tracking.legacyLocationFrequentPlaceCreated()); unawaited(_tracking.legacyLocationFrequentPlaceCreated());
state = state.copyWith( state = AsyncData(
frequentPlaces: [...state.frequentPlaces, created], current.copyWith(
isSubmitting: false, frequentPlaces: [...current.frequentPlaces, created],
successMessage: LocationSuccessEvent.frequentPlaceCreated, isSubmitting: false,
successMessage: LocationSuccessEvent.frequentPlaceCreated,
),
); );
return true; return true;
} catch (e) { } catch (e) {
@@ -217,10 +234,15 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double lng, required double lng,
List<WifiInfoEntity> wifiList = const [], List<WifiInfoEntity> wifiList = const [],
}) async { }) async {
state = state.copyWith( final current = state.value;
isSubmitting: true, if (current == null) return false;
errorEvent: null,
successMessage: null, state = AsyncData(
current.copyWith(
isSubmitting: true,
errorEvent: null,
successMessage: null,
),
); );
try { try {
final request = UpdateFrequentPlaceRequestModel( final request = UpdateFrequentPlaceRequestModel(
@@ -245,12 +267,14 @@ class LocationViewModel extends Notifier<LocationViewState> {
unawaited(_tracking.legacyLocationFrequentPlaceUpdated()); unawaited(_tracking.legacyLocationFrequentPlaceUpdated());
state = state.copyWith( state = AsyncData(
frequentPlaces: state.frequentPlaces current.copyWith(
.map((f) => f.id == id ? updated : f) frequentPlaces: current.frequentPlaces
.toList(), .map((f) => f.id == id ? updated : f)
isSubmitting: false, .toList(),
successMessage: LocationSuccessEvent.frequentPlaceUpdated, isSubmitting: false,
successMessage: LocationSuccessEvent.frequentPlaceUpdated,
),
); );
return true; return true;
} catch (e) { } catch (e) {
@@ -259,16 +283,24 @@ class LocationViewModel extends Notifier<LocationViewState> {
} }
Future<bool> deleteFrequentPlace({required String id}) async { Future<bool> 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 { try {
await _locationRepository.deleteFrequentPlace(frequentPlaceId: id); await _locationRepository.deleteFrequentPlace(frequentPlaceId: id);
if (!ref.mounted) return false; if (!ref.mounted) return false;
unawaited(_tracking.legacyLocationFrequentPlaceDeleted()); unawaited(_tracking.legacyLocationFrequentPlaceDeleted());
state = state.copyWith( state = AsyncData(
frequentPlaces: state.frequentPlaces.where((f) => f.id != id).toList(), current.copyWith(
successMessage: LocationSuccessEvent.frequentPlaceDeleted, frequentPlaces:
current.frequentPlaces.where((f) => f.id != id).toList(),
successMessage: LocationSuccessEvent.frequentPlaceDeleted,
),
); );
return true; return true;
} catch (e) { } catch (e) {
@@ -280,10 +312,14 @@ class LocationViewModel extends Notifier<LocationViewState> {
required DateTime from, required DateTime from,
required DateTime to, required DateTime to,
}) async { }) async {
final device = ref.read(selectedDeviceProvider); final current = state.value;
if (current == null) return;
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith(isLoadingHistory: true, errorEvent: null); state = AsyncData(
current.copyWith(isLoadingHistory: true, errorEvent: null),
);
try { try {
final positions = await _locationRepository.getPositionHistory( final positions = await _locationRepository.getPositionHistory(
deviceIdentificator: device.identificator, deviceIdentificator: device.identificator,
@@ -294,38 +330,54 @@ class LocationViewModel extends Notifier<LocationViewState> {
unawaited(_tracking.legacyLocationHistoryLoaded()); unawaited(_tracking.legacyLocationHistoryLoaded());
state = state.copyWith( state = AsyncData(
positionHistory: positions, current.copyWith(
isLoadingHistory: false, positionHistory: positions,
showRouteTrail: positions.isNotEmpty, isLoadingHistory: false,
showRouteTrail: positions.isNotEmpty,
),
); );
} catch (e) { } catch (e) {
if (!ref.mounted) return; if (!ref.mounted) return;
state = state.copyWith( state = AsyncData(
isLoadingHistory: false, current.copyWith(
errorEvent: LocationErrorEvent.positionHistory, isLoadingHistory: false,
errorEvent: LocationErrorEvent.positionHistory,
),
); );
} }
} }
void clearPositionHistory() { void clearPositionHistory() {
if (state.positionHistory.isNotEmpty) { final current = state.value;
if (current == null) return;
if (current.positionHistory.isNotEmpty) {
unawaited(_tracking.legacyLocationHistoryCleared()); unawaited(_tracking.legacyLocationHistoryCleared());
} }
state = state.copyWith(positionHistory: [], showRouteTrail: false); state = AsyncData(
current.copyWith(positionHistory: [], showRouteTrail: false),
);
} }
void toggleRouteTrail() { void toggleRouteTrail() {
final newVisible = !state.showRouteTrail; final current = state.value;
if (current == null) return;
final newVisible = !current.showRouteTrail;
unawaited(_tracking.legacyLocationMapRouteTrailToggled(newVisible)); unawaited(_tracking.legacyLocationMapRouteTrailToggled(newVisible));
state = state.copyWith(showRouteTrail: newVisible); state = AsyncData(current.copyWith(showRouteTrail: newVisible));
} }
Future<bool> updateLocationFrequency({required int frequency}) async { Future<bool> 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; if (device == null) return false;
state = state.copyWith(isSubmitting: true, errorEvent: null); state = AsyncData(
current.copyWith(isSubmitting: true, errorEvent: null),
);
try { try {
final updatedSettings = device.settings.copyWith(frequency: frequency); final updatedSettings = device.settings.copyWith(frequency: frequency);
@@ -341,7 +393,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
unawaited(_tracking.legacyLocationFrequencyUpdated(frequency)); unawaited(_tracking.legacyLocationFrequencyUpdated(frequency));
state = state.copyWith(isSubmitting: false); state = AsyncData(current.copyWith(isSubmitting: false));
return true; return true;
} catch (e) { } catch (e) {
return _handleErrorEvent(LocationErrorEvent.locationFrequency); return _handleErrorEvent(LocationErrorEvent.locationFrequency);
@@ -350,7 +402,11 @@ class LocationViewModel extends Notifier<LocationViewState> {
bool _handleErrorEvent(LocationErrorEvent event) { bool _handleErrorEvent(LocationErrorEvent event) {
if (!ref.mounted) return false; 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; return false;
} }
} }

View File

@@ -31,7 +31,6 @@ abstract class LocationViewState with _$LocationViewState {
@Default([]) List<GeofenceEntity> geofences, @Default([]) List<GeofenceEntity> geofences,
@Default([]) List<FrequentPlaceEntity> frequentPlaces, @Default([]) List<FrequentPlaceEntity> frequentPlaces,
@Default([]) List<PositionEntity> positionHistory, @Default([]) List<PositionEntity> positionHistory,
@Default(true) bool isLoading,
@Default(false) bool isLoadingHistory, @Default(false) bool isLoadingHistory,
@Default(false) bool isSubmitting, @Default(false) bool isSubmitting,
@Default(false) bool showRouteTrail, @Default(false) bool showRouteTrail,

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc /// @nodoc
mixin _$LocationViewState { mixin _$LocationViewState {
List<GeofenceEntity> get geofences; List<FrequentPlaceEntity> get frequentPlaces; List<PositionEntity> get positionHistory; bool get isLoading; bool get isLoadingHistory; bool get isSubmitting; bool get showRouteTrail; LocationErrorEvent? get errorEvent; LocationSuccessEvent? get successMessage; List<GeofenceEntity> get geofences; List<FrequentPlaceEntity> get frequentPlaces; List<PositionEntity> get positionHistory; bool get isLoadingHistory; bool get isSubmitting; bool get showRouteTrail; LocationErrorEvent? get errorEvent; LocationSuccessEvent? get successMessage;
/// Create a copy of LocationViewState /// Create a copy of LocationViewState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LocationViewStateCopyWith<LocationViewState> get copyWith => _$LocationViewStat
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory $LocationViewStateCopyWith(LocationViewState value, $Res Function(LocationViewState) _then) = _$LocationViewStateCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage
}); });
@@ -62,13 +62,12 @@ class _$LocationViewStateCopyWithImpl<$Res>
/// Create a copy of LocationViewState /// Create a copy of LocationViewState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_self.copyWith(
geofences: null == geofences ? _self.geofences : geofences // ignore: cast_nullable_to_non_nullable geofences: null == geofences ? _self.geofences : geofences // ignore: cast_nullable_to_non_nullable
as List<GeofenceEntity>,frequentPlaces: null == frequentPlaces ? _self.frequentPlaces : frequentPlaces // ignore: cast_nullable_to_non_nullable as List<GeofenceEntity>,frequentPlaces: null == frequentPlaces ? _self.frequentPlaces : frequentPlaces // ignore: cast_nullable_to_non_nullable
as List<FrequentPlaceEntity>,positionHistory: null == positionHistory ? _self.positionHistory : positionHistory // ignore: cast_nullable_to_non_nullable as List<FrequentPlaceEntity>,positionHistory: null == positionHistory ? _self.positionHistory : positionHistory // ignore: cast_nullable_to_non_nullable
as List<PositionEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as List<PositionEntity>,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable
as bool,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,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,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 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 extends Object?>(TResult Function( List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _LocationViewState() when $default != null: 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(); return orElse();
} }
@@ -179,10 +178,10 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _LocationViewState(): 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'); throw StateError('Unexpected subclass');
} }
@@ -199,10 +198,10 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _LocationViewState() when $default != null: 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; return null;
} }
@@ -214,7 +213,7 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that
class _LocationViewState implements LocationViewState { class _LocationViewState implements LocationViewState {
const _LocationViewState({final List<GeofenceEntity> geofences = const [], final List<FrequentPlaceEntity> frequentPlaces = const [], final List<PositionEntity> 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<GeofenceEntity> geofences = const [], final List<FrequentPlaceEntity> frequentPlaces = const [], final List<PositionEntity> positionHistory = const [], this.isLoadingHistory = false, this.isSubmitting = false, this.showRouteTrail = false, this.errorEvent, this.successMessage}): _geofences = geofences,_frequentPlaces = frequentPlaces,_positionHistory = positionHistory;
final List<GeofenceEntity> _geofences; final List<GeofenceEntity> _geofences;
@@ -238,7 +237,6 @@ class _LocationViewState implements LocationViewState {
return EqualUnmodifiableListView(_positionHistory); return EqualUnmodifiableListView(_positionHistory);
} }
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingHistory; @override@JsonKey() final bool isLoadingHistory;
@override@JsonKey() final bool isSubmitting; @override@JsonKey() final bool isSubmitting;
@override@JsonKey() final bool showRouteTrail; @override@JsonKey() final bool showRouteTrail;
@@ -255,16 +253,16 @@ _$LocationViewStateCopyWith<_LocationViewState> get copyWith => __$LocationViewS
@override @override
bool operator ==(Object other) { 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 @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 @override
String toString() { 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; factory _$LocationViewStateCopyWith(_LocationViewState value, $Res Function(_LocationViewState) _then) = __$LocationViewStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage
}); });
@@ -292,13 +290,12 @@ class __$LocationViewStateCopyWithImpl<$Res>
/// Create a copy of LocationViewState /// Create a copy of LocationViewState
/// with the given fields replaced by the non-null parameter values. /// 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( return _then(_LocationViewState(
geofences: null == geofences ? _self._geofences : geofences // ignore: cast_nullable_to_non_nullable geofences: null == geofences ? _self._geofences : geofences // ignore: cast_nullable_to_non_nullable
as List<GeofenceEntity>,frequentPlaces: null == frequentPlaces ? _self._frequentPlaces : frequentPlaces // ignore: cast_nullable_to_non_nullable as List<GeofenceEntity>,frequentPlaces: null == frequentPlaces ? _self._frequentPlaces : frequentPlaces // ignore: cast_nullable_to_non_nullable
as List<FrequentPlaceEntity>,positionHistory: null == positionHistory ? _self._positionHistory : positionHistory // ignore: cast_nullable_to_non_nullable as List<FrequentPlaceEntity>,positionHistory: null == positionHistory ? _self._positionHistory : positionHistory // ignore: cast_nullable_to_non_nullable
as List<PositionEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable as List<PositionEntity>,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable
as bool,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,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,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 as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable

View File

@@ -119,10 +119,10 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
final isSubmitting = ref.watch( final isSubmitting = ref.watch(
locationViewModelProvider.select((s) => s.isSubmitting), locationViewModelProvider.select((s) => s.value?.isSubmitting ?? false),
); );
final errorEvent = ref.watch( final errorEvent = ref.watch(
locationViewModelProvider.select((s) => s.errorEvent), locationViewModelProvider.select((s) => s.value?.errorEvent),
); );
return DraggableScrollableSheet( return DraggableScrollableSheet(

View File

@@ -124,10 +124,10 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> {
final theme = ref.watch(themePortProvider); final theme = ref.watch(themePortProvider);
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
final isSubmitting = ref.watch( final isSubmitting = ref.watch(
locationViewModelProvider.select((s) => s.isSubmitting), locationViewModelProvider.select((s) => s.value?.isSubmitting ?? false),
); );
final errorEvent = ref.watch( final errorEvent = ref.watch(
locationViewModelProvider.select((s) => s.errorEvent), locationViewModelProvider.select((s) => s.value?.errorEvent),
); );
return DraggableScrollableSheet( return DraggableScrollableSheet(

View File

@@ -7,15 +7,15 @@ import 'package:utils/utils.dart';
class DeviceBanner extends ConsumerStatefulWidget { class DeviceBanner extends ConsumerStatefulWidget {
final DeviceEntity device; final DeviceEntity device;
final PositionEntity? position;
final List<DeviceEntity> devices; final List<DeviceEntity> devices;
final List<PositionEntity> positions;
final ValueChanged<DeviceEntity> onDeviceChanged; final ValueChanged<DeviceEntity> onDeviceChanged;
const DeviceBanner({ const DeviceBanner({
super.key, super.key,
required this.device, required this.device,
required this.position,
required this.devices, required this.devices,
required this.positions,
required this.onDeviceChanged, required this.onDeviceChanged,
}); });
@@ -90,8 +90,9 @@ class _DeviceBannerState extends ConsumerState<DeviceBanner> {
}, },
itemBuilder: (context, index) { itemBuilder: (context, index) {
final dev = widget.devices[index]; final dev = widget.devices[index];
final isSelected = dev.id == widget.device.id; final pos = widget.positions
final pos = isSelected ? widget.position : null; .where((p) => p.deviceIdentificator == dev.identificator)
.firstOrNull;
return _DeviceCard( return _DeviceCard(
device: dev, device: dev,
position: pos, position: pos,

View File

@@ -34,11 +34,13 @@ import 'route_history_layer.dart';
const _defaultCenter = LatLng(40.4168, -3.7038); const _defaultCenter = LatLng(40.4168, -3.7038);
const _defaultZoom = 17.0; const _defaultZoom = 17.0;
const _noPositionZoom = 5.8;
class LocationMap extends ConsumerStatefulWidget { class LocationMap extends ConsumerStatefulWidget {
final PositionEntity? selectedPosition; final PositionEntity? selectedPosition;
final DeviceEntity? selectedDevice; final DeviceEntity? selectedDevice;
final List<DeviceEntity> devices; final List<DeviceEntity> devices;
final List<PositionEntity> positions;
final List<GeofenceEntity> geofences; final List<GeofenceEntity> geofences;
final List<FrequentPlaceEntity> frequentPlaces; final List<FrequentPlaceEntity> frequentPlaces;
final List<PositionEntity> positionHistory; final List<PositionEntity> positionHistory;
@@ -52,6 +54,7 @@ class LocationMap extends ConsumerStatefulWidget {
required this.selectedPosition, required this.selectedPosition,
required this.selectedDevice, required this.selectedDevice,
required this.devices, required this.devices,
required this.positions,
required this.geofences, required this.geofences,
required this.frequentPlaces, required this.frequentPlaces,
required this.positionHistory, required this.positionHistory,
@@ -103,14 +106,21 @@ class _LocationMapState extends ConsumerState<LocationMap>
@override @override
void didUpdateWidget(LocationMap oldWidget) { void didUpdateWidget(LocationMap oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.selectedDevice?.id != oldWidget.selectedDevice?.id) {
final deviceChanged =
widget.selectedDevice?.id != oldWidget.selectedDevice?.id;
if (deviceChanged) {
_startMonitoring(); _startMonitoring();
} if (widget.selectedPosition != null) {
if (widget.selectedPosition != null && _centerOnDevice();
}
} else if (widget.selectedPosition != null &&
widget.selectedPosition != oldWidget.selectedPosition) { widget.selectedPosition != oldWidget.selectedPosition) {
final mapState = ref.read(locationMapViewModelProvider); final mapState = ref.read(locationMapViewModelProvider);
if (mapState.isFollowing) _centerOnDevice(); if (mapState.isFollowing) _centerOnDevice();
} }
if (widget.positionHistory.length > 1 && if (widget.positionHistory.length > 1 &&
widget.showRouteTrail && widget.showRouteTrail &&
(oldWidget.positionHistory.length != widget.positionHistory.length || (oldWidget.positionHistory.length != widget.positionHistory.length ||
@@ -323,7 +333,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
void _showListSheet() { void _showListSheet() {
unawaited(ref.read(sfTrackingProvider).legacyLocationListSheetOpened()); 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); final mapState = ref.read(locationMapViewModelProvider);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -472,8 +483,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
alignment: Alignment.bottomCenter, alignment: Alignment.bottomCenter,
child: DeviceBanner( child: DeviceBanner(
device: widget.selectedDevice!, device: widget.selectedDevice!,
position: widget.selectedPosition,
devices: widget.devices, devices: widget.devices,
positions: widget.positions,
onDeviceChanged: widget.onDeviceChanged, onDeviceChanged: widget.onDeviceChanged,
), ),
), ),
@@ -621,7 +632,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
const SizedBox(height: 8), const SizedBox(height: 8),
FrequencySelector( FrequencySelector(
currentFrequency: currentFrequency:
ref.watch(selectedDeviceProvider)?.settings.frequency ?? 60, ref.watch(selectedDeviceProvider).value?.settings.frequency ?? 60,
options: widget.selectedDevice!.capabilities!.location!.options, options: widget.selectedDevice!.capabilities!.location!.options,
onChanged: _updateFrequency, onChanged: _updateFrequency,
), ),
@@ -715,12 +726,14 @@ class _LocationMapState extends ConsumerState<LocationMap>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mapState = ref.watch(locationMapViewModelProvider); final mapState = ref.watch(locationMapViewModelProvider);
final initialCenter = widget.selectedPosition != null final hasPosition = widget.selectedPosition != null;
final initialCenter = hasPosition
? LatLng( ? LatLng(
widget.selectedPosition!.latitude, widget.selectedPosition!.latitude,
widget.selectedPosition!.longitude, widget.selectedPosition!.longitude,
) )
: _defaultCenter; : _defaultCenter;
final initialZoom = hasPosition ? _defaultZoom : _noPositionZoom;
return Stack( return Stack(
children: [ children: [
@@ -728,7 +741,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
mapController: _mapController, mapController: _mapController,
options: MapOptions( options: MapOptions(
initialCenter: initialCenter, initialCenter: initialCenter,
initialZoom: _defaultZoom, initialZoom: initialZoom,
minZoom: 5, minZoom: 5,
keepAlive: true, keepAlive: true,
onPositionChanged: (camera, _) { onPositionChanged: (camera, _) {

View File

@@ -38,7 +38,7 @@ class AlarmViewModel extends Notifier<AlarmViewState> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final alarms = await _repository.getAlarms(deviceId: device.id); final alarms = await _repository.getAlarms(deviceId: device.id);
@@ -55,7 +55,7 @@ class AlarmViewModel extends Notifier<AlarmViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final id = const Uuid().v4(); final id = const Uuid().v4();

View File

@@ -24,7 +24,7 @@ class AlertsViewModel extends Notifier<AlertsViewState> {
} }
void _load() { void _load() {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final available = device.capabilities?.alerts?.types ?? []; final available = device.capabilities?.alerts?.types ?? [];
@@ -48,7 +48,7 @@ class AlertsViewModel extends Notifier<AlertsViewState> {
} }
Future<void> save() async { Future<void> save() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(

View File

@@ -24,7 +24,7 @@ class BatteryViewModel extends Notifier<BatteryViewState> {
} }
void _load() { void _load() {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(
@@ -34,7 +34,7 @@ class BatteryViewModel extends Notifier<BatteryViewState> {
} }
Future<void> toggleNightMode(bool value) async { Future<void> toggleNightMode(bool value) async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(

View File

@@ -28,7 +28,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final contacts = await _repository.getWhitelist(deviceId: device.id); final contacts = await _repository.getWhitelist(deviceId: device.id);
@@ -45,7 +45,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final updatedContacts = [...state.contacts, contact]; final updatedContacts = [...state.contacts, contact];
@@ -79,7 +79,7 @@ class BlockPhoneViewModel extends Notifier<BlockPhoneViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final updatedContacts = [...state.contacts]..removeAt(index); final updatedContacts = [...state.contacts]..removeAt(index);

View File

@@ -25,7 +25,7 @@ class DisableFunctionsViewModel extends Notifier<DisableFunctionsViewState> {
} }
void _load() { void _load() {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(
@@ -44,7 +44,7 @@ class DisableFunctionsViewModel extends Notifier<DisableFunctionsViewState> {
} }
Future<void> save() async { Future<void> save() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(

View File

@@ -27,7 +27,7 @@ class LanguageViewModel extends Notifier<LanguageViewState> {
Future<void> load() async { Future<void> load() async {
state = state.copyWith(isLoading: true, errorMessage: ''); state = state.copyWith(isLoading: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
state = state.copyWith( state = state.copyWith(
isLoading: false, isLoading: false,
device: device, device: device,

View File

@@ -29,7 +29,7 @@ class RemoteManagementViewModel extends Notifier<RemoteManagementViewState> {
} }
Future<void> _init() async { Future<void> _init() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
state = state.copyWith(deviceId: device!.identificator, isLoading: false); state = state.copyWith(deviceId: device!.identificator, isLoading: false);
} }

View File

@@ -28,7 +28,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final contacts = await _repository.getEmergencyContacts( final contacts = await _repository.getEmergencyContacts(
@@ -47,7 +47,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final updatedContacts = [...state.contacts, contact]; final updatedContacts = [...state.contacts, contact];
@@ -81,7 +81,7 @@ class SosContactsViewModel extends Notifier<SosContactsViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final updatedContacts = [...state.contacts]..removeAt(index); final updatedContacts = [...state.contacts]..removeAt(index);

View File

@@ -24,7 +24,7 @@ class SoundViewModel extends Notifier<SoundViewState> {
} }
Future<void> load() async { Future<void> load() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(

View File

@@ -29,7 +29,7 @@ class SyncClockViewModel extends Notifier<SyncClockViewState> {
} }
Future<void> load() async { Future<void> load() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
setDevice(device!); setDevice(device!);
} }

View File

@@ -24,7 +24,7 @@ class TimezoneViewModel extends Notifier<TimezoneViewState> {
} }
void _load() { void _load() {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
state = state.copyWith( state = state.copyWith(
@@ -38,7 +38,7 @@ class TimezoneViewModel extends Notifier<TimezoneViewState> {
} }
Future<void> save() async { Future<void> save() async {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
if (state.timezone == device.settings.timezone) { if (state.timezone == device.settings.timezone) {

View File

@@ -28,7 +28,7 @@ class WifiSettingsViewModel extends Notifier<WifiSettingsViewState> {
Future<void> _load() async { Future<void> _load() async {
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
final networks = await _repository.getWifiNetworks(deviceId: device.id); final networks = await _repository.getWifiNetworks(deviceId: device.id);
@@ -45,7 +45,7 @@ class WifiSettingsViewModel extends Notifier<WifiSettingsViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
await _repository.createWifiNetwork( await _repository.createWifiNetwork(
@@ -75,7 +75,7 @@ class WifiSettingsViewModel extends Notifier<WifiSettingsViewState> {
state = state.copyWith(isSaving: true, errorMessage: ''); state = state.copyWith(isSaving: true, errorMessage: '');
try { try {
final device = ref.read(selectedDeviceProvider); final device = ref.read(selectedDeviceProvider).value;
if (device == null) return; if (device == null) return;
await _repository.deleteWifiNetwork(networkId: networkId); await _repository.deleteWifiNetwork(networkId: networkId);

View File

@@ -14,5 +14,6 @@ export 'src/domain/repositories/command_repository.dart';
export 'src/providers/commands_repository_provider.dart'; export 'src/providers/commands_repository_provider.dart';
export 'src/domain/repositories/devices_repository.dart'; export 'src/domain/repositories/devices_repository.dart';
export 'src/providers/devices_repository_provider.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/data/datasources/device_settings_update_datasource.dart';
export 'src/providers/device_settings_update_provider.dart'; export 'src/providers/device_settings_update_provider.dart';

View File

@@ -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<List<DeviceEntity>> {
@override
Future<List<DeviceEntity>> 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<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(sharedDevicesRepositoryProvider).getDevices(),
);
}
}
final legacyDevicesProvider =
AsyncNotifierProvider<LegacyDevices, List<DeviceEntity>>(
LegacyDevices.new,
);

View File

@@ -1,26 +1,62 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.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:sf_shared/sf_shared.dart';
import 'package:shared_preferences/shared_preferences.dart';
final selectedDeviceProvider = const _prefsKey = 'legacy_selected_device_id';
NotifierProvider<SelectedDeviceNotifier, DeviceEntity?>(
SelectedDeviceNotifier.new,
);
class SelectedDeviceNotifier extends Notifier<DeviceEntity?> { class SelectedDeviceNotifier extends AsyncNotifier<DeviceEntity?> {
@override @override
DeviceEntity? build() { Future<DeviceEntity?> build() async {
return null; 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) { Future<void> setSelectedDevice(DeviceEntity device) async {
state = device; 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<void> 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, DeviceEntity?>(
SelectedDeviceNotifier.new,
);
extension DeviceSettingsSync on Ref { extension DeviceSettingsSync on Ref {
void syncDeviceSettings(DeviceEntity device, DeviceSettingsEntity settings) { void syncDeviceSettings(DeviceEntity device, DeviceSettingsEntity settings) {
read( read(selectedDeviceProvider.notifier).updateSettings(settings);
selectedDeviceProvider.notifier,
).setSelectedDevice(device.copyWith(settings: settings));
} }
} }