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) {
final theme = ref.watch(themePortProvider);
final color = theme.getColorFor(ThemeCode.legacyPrimary);
final selectedDevice = ref.watch(selectedDeviceProvider);
final selectedDevice = ref.watch(selectedDeviceProvider).value;
final isLoggingOut = ref.watch(
accountSettingsViewModelProvider.select((s) => s.isLoggingOut),
);

View File

@@ -81,6 +81,8 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
ref.invalidate(selectedDeviceProvider);
}
ref.read(legacyDevicesProvider.notifier).removeDevice(device.id);
unawaited(_tracking.legacyAccountLinkedDeviceUnlinked());
state = state.copyWith(
@@ -109,7 +111,11 @@ class LinkedDevicesViewModel extends Notifier<LinkedDevicesViewState> {
if (deviceName.isEmpty) return;
try {
_devicesRepository.updateDevice(request: _toRequest(device));
await _devicesRepository.updateDevice(request: _toRequest(device));
ref.read(legacyDevicesProvider.notifier).renameDevice(
deviceId: device.id,
newCarrierName: deviceName.trim(),
);
unawaited(_tracking.legacyAccountLinkedDeviceRenamed());
} catch (e) {
state = state.copyWith(

View File

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

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ class ActivityMeterViewModel extends Notifier<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 {
if (range == state.timeRange) return;
@@ -272,7 +272,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
}
Future<bool> togglePedometer({required bool enabled}) async {
final device = ref.read(selectedDeviceProvider);
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return false;
state = state.copyWith(errorEvent: null);

View File

@@ -30,7 +30,7 @@ class AppsUseViewModel extends Notifier<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 {
if (range == state.timeRange) return;

View File

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

View File

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

View File

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

View File

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

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 {
if (range == state.timeRange) return;
@@ -310,7 +310,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
}
Future<bool> updateHeartRateFrequency({required int frequency}) async {
final device = ref.read(selectedDeviceProvider);
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return false;
try {
@@ -339,7 +339,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
}
Future<void> measure() async {
final device = ref.read(selectedDeviceProvider);
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return;
try {

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
Future<void> _load() async {
try {
final device = ref.read(selectedDeviceProvider);
final device = ref.read(selectedDeviceProvider).value;
if (device == null) return;
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:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_tracking/sf_tracking.dart';
import 'package:utils/utils.dart';
@@ -195,6 +196,10 @@ class LegacyDeviceSetupViewModel extends Notifier<LegacyDeviceSetupViewState> {
if (!ref.mounted) return false;
// We don't have the new DeviceEntity locally (the create endpoint
// returns void), so re-read from the backend.
await ref.read(legacyDevicesProvider.notifier).refresh();
unawaited(
_tracking.legacyDeviceSetupCompleted(
childGender: genrer,

View File

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

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
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
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LocationViewStateCopyWith<LocationViewState> get copyWith => _$LocationViewStat
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationViewState&&const DeepCollectionEquality().equals(other.geofences, geofences)&&const DeepCollectionEquality().equals(other.frequentPlaces, frequentPlaces)&&const DeepCollectionEquality().equals(other.positionHistory, positionHistory)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationViewState&&const DeepCollectionEquality().equals(other.geofences, geofences)&&const DeepCollectionEquality().equals(other.frequentPlaces, frequentPlaces)&&const DeepCollectionEquality().equals(other.positionHistory, positionHistory)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(geofences),const DeepCollectionEquality().hash(frequentPlaces),const DeepCollectionEquality().hash(positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage);
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(geofences),const DeepCollectionEquality().hash(frequentPlaces),const DeepCollectionEquality().hash(positionHistory),isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage);
@override
String toString() {
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)';
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)';
}
@@ -45,7 +45,7 @@ abstract mixin class $LocationViewStateCopyWith<$Res> {
factory $LocationViewStateCopyWith(LocationViewState value, $Res Function(LocationViewState) _then) = _$LocationViewStateCopyWithImpl;
@useResult
$Res call({
List<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
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoading = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) {
return _then(_self.copyWith(
geofences: null == geofences ? _self.geofences : geofences // ignore: cast_nullable_to_non_nullable
as List<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<PositionEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable
as List<PositionEntity>,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable
as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,showRouteTrail: null == showRouteTrail ? _self.showRouteTrail : showRouteTrail // ignore: cast_nullable_to_non_nullable
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
@@ -158,10 +157,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult 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) {
case _LocationViewState() when $default != null:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return orElse();
}
@@ -179,10 +178,10 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that
/// }
/// ```
@optionalTypeArgs TResult when<TResult 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) {
case _LocationViewState():
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -199,10 +198,10 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult 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) {
case _LocationViewState() when $default != null:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return null;
}
@@ -214,7 +213,7 @@ return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that
class _LocationViewState implements LocationViewState {
const _LocationViewState({final List<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;
@@ -238,7 +237,6 @@ class _LocationViewState implements LocationViewState {
return EqualUnmodifiableListView(_positionHistory);
}
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingHistory;
@override@JsonKey() final bool isSubmitting;
@override@JsonKey() final bool showRouteTrail;
@@ -255,16 +253,16 @@ _$LocationViewStateCopyWith<_LocationViewState> get copyWith => __$LocationViewS
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationViewState&&const DeepCollectionEquality().equals(other._geofences, _geofences)&&const DeepCollectionEquality().equals(other._frequentPlaces, _frequentPlaces)&&const DeepCollectionEquality().equals(other._positionHistory, _positionHistory)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationViewState&&const DeepCollectionEquality().equals(other._geofences, _geofences)&&const DeepCollectionEquality().equals(other._frequentPlaces, _frequentPlaces)&&const DeepCollectionEquality().equals(other._positionHistory, _positionHistory)&&(identical(other.isLoadingHistory, isLoadingHistory) || other.isLoadingHistory == isLoadingHistory)&&(identical(other.isSubmitting, isSubmitting) || other.isSubmitting == isSubmitting)&&(identical(other.showRouteTrail, showRouteTrail) || other.showRouteTrail == showRouteTrail)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent)&&(identical(other.successMessage, successMessage) || other.successMessage == successMessage));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_geofences),const DeepCollectionEquality().hash(_frequentPlaces),const DeepCollectionEquality().hash(_positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage);
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_geofences),const DeepCollectionEquality().hash(_frequentPlaces),const DeepCollectionEquality().hash(_positionHistory),isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage);
@override
String toString() {
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)';
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)';
}
@@ -275,7 +273,7 @@ abstract mixin class _$LocationViewStateCopyWith<$Res> implements $LocationViewS
factory _$LocationViewStateCopyWith(_LocationViewState value, $Res Function(_LocationViewState) _then) = __$LocationViewStateCopyWithImpl;
@override @useResult
$Res call({
List<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
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoading = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? geofences = null,Object? frequentPlaces = null,Object? positionHistory = null,Object? isLoadingHistory = null,Object? isSubmitting = null,Object? showRouteTrail = null,Object? errorEvent = freezed,Object? successMessage = freezed,}) {
return _then(_LocationViewState(
geofences: null == geofences ? _self._geofences : geofences // ignore: cast_nullable_to_non_nullable
as List<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<PositionEntity>,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable
as List<PositionEntity>,isLoadingHistory: null == isLoadingHistory ? _self.isLoadingHistory : isLoadingHistory // ignore: cast_nullable_to_non_nullable
as bool,isSubmitting: null == isSubmitting ? _self.isSubmitting : isSubmitting // ignore: cast_nullable_to_non_nullable
as bool,showRouteTrail: null == showRouteTrail ? _self.showRouteTrail : showRouteTrail // ignore: cast_nullable_to_non_nullable
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,5 +14,6 @@ export 'src/domain/repositories/command_repository.dart';
export 'src/providers/commands_repository_provider.dart';
export 'src/domain/repositories/devices_repository.dart';
export 'src/providers/devices_repository_provider.dart';
export 'src/providers/legacy_devices_provider.dart';
export 'src/data/datasources/device_settings_update_datasource.dart';
export 'src/providers/device_settings_update_provider.dart';

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:legacy_shared/src/providers/legacy_devices_provider.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:shared_preferences/shared_preferences.dart';
final selectedDeviceProvider =
NotifierProvider<SelectedDeviceNotifier, DeviceEntity?>(
SelectedDeviceNotifier.new,
);
const _prefsKey = 'legacy_selected_device_id';
class SelectedDeviceNotifier extends Notifier<DeviceEntity?> {
class SelectedDeviceNotifier extends AsyncNotifier<DeviceEntity?> {
@override
DeviceEntity? build() {
return null;
Future<DeviceEntity?> build() async {
final devices = await ref.watch(legacyDevicesProvider.future);
if (devices.isEmpty) return null;
final prefs = await SharedPreferences.getInstance();
final persistedId = prefs.getString(_prefsKey);
if (persistedId != null) {
final found = devices.where((d) => d.id == persistedId).firstOrNull;
if (found != null) return found;
}
return devices.first;
}
void setSelectedDevice(DeviceEntity device) {
state = device;
Future<void> setSelectedDevice(DeviceEntity device) async {
state = AsyncData(device);
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_prefsKey, device.id);
} catch (e) {
debugPrint('[SelectedDeviceNotifier] failed to persist selection: $e');
}
}
void updateSettings(DeviceSettingsEntity settings) {
final current = state.value;
if (current == null) return;
state = AsyncData(current.copyWith(settings: settings));
}
Future<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 {
void syncDeviceSettings(DeviceEntity device, DeviceSettingsEntity settings) {
read(
selectedDeviceProvider.notifier,
).setSelectedDevice(device.copyWith(settings: settings));
read(selectedDeviceProvider.notifier).updateSettings(settings);
}
}