feat: type device settings and capabilities, centralize device updates, and improve error handling

- Type device.settings and device.capabilities from untyped maps to Freezed models
  - Centralize all device settings updates through shared DeviceSettingsUpdateDatasource
  - Add frequency selectors for location and heart rate, pedometer toggle, and health measurement countdown with Lottie animation
  - Replace raw backend error messages with typed i18n error events across location, health, and activity meter
  - Fix silent error swallowing in commands datasource and stuck dialog in locate device
This commit is contained in:
2026-03-25 02:15:25 +01:00
parent 5111d5d65f
commit 33f3dfa252
68 changed files with 8960 additions and 414 deletions

View File

@@ -93,6 +93,7 @@ class ControlPanelViewModel extends Notifier<ControlPanelViewState> {
Future<void> refreshPositions() async {
if (state.devices.isEmpty) return;
state = state.copyWith(errorMessage: '');
try {
final positionLists = await Future.wait(
state.devices.map(

View File

@@ -7,6 +7,7 @@ import 'package:utils/utils.dart';
import '../../../core/presentation/widgets/time_range_selector.dart';
import 'state/activity_meter_view_model.dart';
import 'state/activity_meter_view_state.dart';
import 'widgets/steps_bar_chart.dart';
import 'widgets/steps_history_section.dart';
import 'widgets/steps_progress_ring.dart';
@@ -20,12 +21,18 @@ 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);
ref.listen(
activityMeterViewModelProvider.select((s) => s.errorMessage),
activityMeterViewModelProvider.select((s) => s.errorEvent),
(previous, next) {
if (next.isNotEmpty) {
showTopSnackbar(context, message: next, type: MessageType.error);
if (next != null) {
final message = switch (next) {
ActivityMeterErrorEvent.loadData => context.translate(I18n.errorActivityData),
ActivityMeterErrorEvent.loadMore => context.translate(I18n.errorActivityData),
ActivityMeterErrorEvent.pedometer => context.translate(I18n.errorPedometer),
};
showTopSnackbar(context, message: message, type: MessageType.error);
}
},
);
@@ -69,6 +76,43 @@ class ActivityMeterScreen extends ConsumerWidget {
],
),
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
vertical: SizeUtils.getByScreen(small: 4, big: 4),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
context.translate(I18n.activityMeterPedometer),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 14, big: 15),
fontWeight: FontWeight.w500,
),
),
Switch.adaptive(
value: device?.settings.pedometer ?? false,
activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary),
onChanged: (value) async {
final success = await vm.togglePedometer(enabled: value);
if (!context.mounted) return;
if (success) {
showTopSnackbar(
context,
message: context.translate(
value
? I18n.activityMeterPedometerEnabled
: I18n.activityMeterPedometerDisabled,
),
type: MessageType.success,
);
}
},
),
],
),
),
StepsProgressRing(
steps: state.todayTotal,
goal: state.dailyGoal,

View File

@@ -79,7 +79,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
if (!ref.mounted) return;
state = state.copyWith(
isLoadingMore: false,
errorMessage: _formatError(e),
errorEvent: ActivityMeterErrorEvent.loadMore,
);
}
}
@@ -113,7 +113,7 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
await _loadFilteredData();
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
state = state.copyWith(isLoading: false, errorEvent: ActivityMeterErrorEvent.loadData);
}
}
@@ -157,11 +157,11 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
hasMoreHistory: histSteps.length >= _historyPageSize,
stats: _computeStats(chartDaily),
isLoading: false,
errorMessage: '',
errorEvent: null,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
state = state.copyWith(isLoading: false, errorEvent: ActivityMeterErrorEvent.loadData);
}
}
@@ -243,8 +243,24 @@ class ActivityMeterViewModel extends Notifier<ActivityMeterViewState> {
}
}
String _formatError(Object e) {
final msg = e.toString();
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
Future<bool> togglePedometer({required bool enabled}) async {
final device = ref.read(selectedDeviceProvider);
if (device == null) return false;
state = state.copyWith(errorEvent: null);
try {
final updatedSettings = device.settings.copyWith(pedometer: enabled);
await ref.read(deviceSettingsUpdateProvider).updateDeviceSettings(
device: device,
updatedSettings: updatedSettings,
);
if (!ref.mounted) return false;
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(errorEvent: ActivityMeterErrorEvent.pedometer);
return false;
}
}
}

View File

@@ -4,6 +4,12 @@ import '../../../../core/presentation/time_range.dart';
part 'activity_meter_view_state.freezed.dart';
enum ActivityMeterErrorEvent {
loadData,
loadMore,
pedometer,
}
@freezed
abstract class DailySteps with _$DailySteps {
const factory DailySteps({
@@ -36,6 +42,6 @@ abstract class ActivityMeterViewState with _$ActivityMeterViewState {
DateTime? customEnd,
@Default(true) bool isLoading,
@Default(false) bool isLoadingMore,
@Default('') String errorMessage,
ActivityMeterErrorEvent? errorEvent,
}) = _ActivityMeterViewState;
}

View File

@@ -537,7 +537,7 @@ as int,
/// @nodoc
mixin _$ActivityMeterViewState {
int get todayTotal; int get dailyGoal; List<DailySteps> get chartData; List<DailySteps> get historyData; int get currentHistoryPage; bool get hasMoreHistory; StepsStats get stats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; bool get isLoading; bool get isLoadingMore; String get errorMessage;
int get todayTotal; int get dailyGoal; List<DailySteps> get chartData; List<DailySteps> get historyData; int get currentHistoryPage; bool get hasMoreHistory; StepsStats get stats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; bool get isLoading; bool get isLoadingMore; ActivityMeterErrorEvent? get errorEvent;
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -548,16 +548,16 @@ $ActivityMeterViewStateCopyWith<ActivityMeterViewState> get copyWith => _$Activi
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other.chartData, chartData)&&const DeepCollectionEquality().equals(other.historyData, historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other.chartData, chartData)&&const DeepCollectionEquality().equals(other.historyData, historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(chartData),const DeepCollectionEquality().hash(historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorMessage);
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(chartData),const DeepCollectionEquality().hash(historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorEvent);
@override
String toString() {
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorMessage: $errorMessage)';
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
}
@@ -568,7 +568,7 @@ abstract mixin class $ActivityMeterViewStateCopyWith<$Res> {
factory $ActivityMeterViewStateCopyWith(ActivityMeterViewState value, $Res Function(ActivityMeterViewState) _then) = _$ActivityMeterViewStateCopyWithImpl;
@useResult
$Res call({
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
});
@@ -585,7 +585,7 @@ class _$ActivityMeterViewStateCopyWithImpl<$Res>
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
return _then(_self.copyWith(
todayTotal: null == todayTotal ? _self.todayTotal : todayTotal // ignore: cast_nullable_to_non_nullable
as int,dailyGoal: null == dailyGoal ? _self.dailyGoal : dailyGoal // ignore: cast_nullable_to_non_nullable
@@ -599,8 +599,8 @@ as TimeRange,customStart: freezed == customStart ? _self.customStart : customSta
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as ActivityMeterErrorEvent?,
));
}
/// Create a copy of ActivityMeterViewState
@@ -694,10 +694,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ActivityMeterViewState() when $default != null:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorMessage);case _:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return orElse();
}
@@ -715,10 +715,10 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent) $default,) {final _that = this;
switch (_that) {
case _ActivityMeterViewState():
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorMessage);case _:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
throw StateError('Unexpected subclass');
}
@@ -735,10 +735,10 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent)? $default,) {final _that = this;
switch (_that) {
case _ActivityMeterViewState() when $default != null:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorMessage);case _:
return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyData,_that.currentHistoryPage,_that.hasMoreHistory,_that.stats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorEvent);case _:
return null;
}
@@ -750,7 +750,7 @@ return $default(_that.todayTotal,_that.dailyGoal,_that.chartData,_that.historyDa
class _ActivityMeterViewState implements ActivityMeterViewState {
const _ActivityMeterViewState({this.todayTotal = 0, this.dailyGoal = 8000, final List<DailySteps> chartData = const [], final List<DailySteps> historyData = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.stats = const StepsStats(), this.timeRange = TimeRange.today, this.customStart, this.customEnd, this.isLoading = true, this.isLoadingMore = false, this.errorMessage = ''}): _chartData = chartData,_historyData = historyData;
const _ActivityMeterViewState({this.todayTotal = 0, this.dailyGoal = 8000, final List<DailySteps> chartData = const [], final List<DailySteps> historyData = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.stats = const StepsStats(), this.timeRange = TimeRange.today, this.customStart, this.customEnd, this.isLoading = true, this.isLoadingMore = false, this.errorEvent}): _chartData = chartData,_historyData = historyData;
@override@JsonKey() final int todayTotal;
@@ -777,7 +777,7 @@ class _ActivityMeterViewState implements ActivityMeterViewState {
@override final DateTime? customEnd;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingMore;
@override@JsonKey() final String errorMessage;
@override final ActivityMeterErrorEvent? errorEvent;
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@@ -789,16 +789,16 @@ _$ActivityMeterViewStateCopyWith<_ActivityMeterViewState> get copyWith => __$Act
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other._chartData, _chartData)&&const DeepCollectionEquality().equals(other._historyData, _historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActivityMeterViewState&&(identical(other.todayTotal, todayTotal) || other.todayTotal == todayTotal)&&(identical(other.dailyGoal, dailyGoal) || other.dailyGoal == dailyGoal)&&const DeepCollectionEquality().equals(other._chartData, _chartData)&&const DeepCollectionEquality().equals(other._historyData, _historyData)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.stats, stats) || other.stats == stats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(_chartData),const DeepCollectionEquality().hash(_historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorMessage);
int get hashCode => Object.hash(runtimeType,todayTotal,dailyGoal,const DeepCollectionEquality().hash(_chartData),const DeepCollectionEquality().hash(_historyData),currentHistoryPage,hasMoreHistory,stats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorEvent);
@override
String toString() {
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorMessage: $errorMessage)';
return 'ActivityMeterViewState(todayTotal: $todayTotal, dailyGoal: $dailyGoal, chartData: $chartData, historyData: $historyData, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, stats: $stats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorEvent: $errorEvent)';
}
@@ -809,7 +809,7 @@ abstract mixin class _$ActivityMeterViewStateCopyWith<$Res> implements $Activity
factory _$ActivityMeterViewStateCopyWith(_ActivityMeterViewState value, $Res Function(_ActivityMeterViewState) _then) = __$ActivityMeterViewStateCopyWithImpl;
@override @useResult
$Res call({
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage
int todayTotal, int dailyGoal, List<DailySteps> chartData, List<DailySteps> historyData, int currentHistoryPage, bool hasMoreHistory, StepsStats stats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, ActivityMeterErrorEvent? errorEvent
});
@@ -826,7 +826,7 @@ class __$ActivityMeterViewStateCopyWithImpl<$Res>
/// Create a copy of ActivityMeterViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? todayTotal = null,Object? dailyGoal = null,Object? chartData = null,Object? historyData = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? stats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorEvent = freezed,}) {
return _then(_ActivityMeterViewState(
todayTotal: null == todayTotal ? _self.todayTotal : todayTotal // ignore: cast_nullable_to_non_nullable
as int,dailyGoal: null == dailyGoal ? _self.dailyGoal : dailyGoal // ignore: cast_nullable_to_non_nullable
@@ -840,8 +840,8 @@ as TimeRange,customStart: freezed == customStart ? _self.customStart : customSta
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as ActivityMeterErrorEvent?,
));
}

View File

@@ -2,11 +2,13 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:lottie/lottie.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import 'state/health_view_model.dart';
import 'state/health_view_state.dart';
import 'widgets/blood_pressure_tab.dart';
import 'widgets/health_summary_cards.dart';
import 'widgets/heart_rate_tab.dart';
@@ -55,13 +57,20 @@ 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);
ref.listen(healthViewModelProvider.select((s) => s.errorMessage), (
ref.listen(healthViewModelProvider.select((s) => s.errorEvent), (
previous,
next,
) {
if (next.isNotEmpty) {
showTopSnackbar(context, message: next, type: MessageType.error);
if (next != null) {
final message = switch (next) {
HealthErrorEvent.loadData => context.translate(I18n.errorHealthData),
HealthErrorEvent.loadMore => context.translate(I18n.errorHealthData),
HealthErrorEvent.measure => context.translate(I18n.errorHealthMeasure),
HealthErrorEvent.heartRateFrequency => context.translate(I18n.errorHeartRateFrequency),
};
showTopSnackbar(context, message: message, type: MessageType.error);
}
});
@@ -70,7 +79,12 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
title: context.translate(I18n.health),
body: state.isLoading
? const Center(child: CircularProgressIndicator())
: Column(
: state.isMeasuringCountdown
? _MeasuringOverlay(
remainingSeconds: state.measureRemainingSeconds,
theme: theme,
)
: Column(
children: [
HealthSummaryCards(
heartbeats: state.latestHeartbeats,
@@ -84,6 +98,31 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
onCustomTap: () => _pickCustomRange(vm),
theme: theme,
),
if (device?.capabilities?.heartbeats != null &&
device!.capabilities!.heartbeats!.options.isNotEmpty)
_HeartRateFrequencySelector(
currentFrequency:
device.settings.frequencyHeartRate,
options:
device.capabilities!.heartbeats!.options,
theme: theme,
onChanged: (frequency) async {
final success =
await vm.updateHeartRateFrequency(
frequency: frequency);
if (!context.mounted) return;
if (success) {
showTopSnackbar(
context,
message: context.translate(
I18n.locationFrequencyUpdated,
args: {'minutes': '${frequency ~/ 60}'},
),
type: MessageType.success,
);
}
},
),
TabBar(
controller: _tabController,
labelColor: theme.getColorFor(ThemeCode.legacyPrimary),
@@ -141,18 +180,123 @@ class _SaveSection extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.read(themePortProvider);
final vm = ref.read(healthViewModelProvider.notifier);
final isMeasuring = ref.watch(
healthViewModelProvider.select((s) => s.isMeasuring),
);
final isCountdown = ref.watch(
healthViewModelProvider.select((s) => s.isMeasuringCountdown),
);
if (isCountdown) return const SizedBox.shrink();
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: PrimaryButton(
onPressed: () async {
await vm.measure();
},
text: context.translate(I18n.measure),
onPressed: isMeasuring ? null : () => vm.measure(),
text: isMeasuring
? '...'
: context.translate(I18n.measure),
color: theme.getColorFor(ThemeCode.legacyPrimary),
),
);
}
}
class _HeartRateFrequencySelector extends StatelessWidget {
final int currentFrequency;
final List<int> options;
final ThemePort theme;
final ValueChanged<int> onChanged;
const _HeartRateFrequencySelector({
required this.currentFrequency,
required this.options,
required this.theme,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Row(
children: [
Icon(Icons.timer_outlined, size: 18, color: primaryColor),
const SizedBox(width: 8),
Text(
context.translate(I18n.healthFrequency),
style: TextStyle(fontSize: 13, color: primaryColor),
),
const Spacer(),
...options.map(
(opt) => Padding(
padding: const EdgeInsets.only(left: 6),
child: ChoiceChip(
label: Text('${opt ~/ 60}m'),
selected: opt == currentFrequency,
selectedColor: primaryColor,
labelStyle: TextStyle(
fontSize: 12,
color: opt == currentFrequency ? Colors.white : primaryColor,
),
side: BorderSide(color: primaryColor),
onSelected: (_) => onChanged(opt),
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
),
),
],
),
);
}
}
class _MeasuringOverlay extends StatelessWidget {
final int remainingSeconds;
final ThemePort theme;
const _MeasuringOverlay({
required this.remainingSeconds,
required this.theme,
});
@override
Widget build(BuildContext context) {
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Lottie.asset(
'assets/shared/animations/fitness_tracker.json',
width: 200,
height: 200,
),
const SizedBox(height: 24),
Text(
context.translate(I18n.healthMeasuring),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: primaryColor,
),
),
const SizedBox(height: 12),
Text(
'${remainingSeconds}s',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: primaryColor,
),
),
],
),
);
}
}

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
@@ -7,6 +9,18 @@ import '../../../../core/domain/repositories/health_repository.dart';
import '../../../../core/providers/health_repository_provider.dart';
import 'health_view_state.dart';
final _measureEndTimeProvider =
NotifierProvider<_MeasureEndTimeNotifier, DateTime?>(
_MeasureEndTimeNotifier.new,
);
class _MeasureEndTimeNotifier extends Notifier<DateTime?> {
@override
DateTime? build() => null;
void set(DateTime? value) => state = value;
}
final healthViewModelProvider =
NotifierProvider.autoDispose<HealthViewModel, HealthViewState>(
HealthViewModel.new,
@@ -15,24 +29,46 @@ final healthViewModelProvider =
class HealthViewModel extends Notifier<HealthViewState> {
late final HealthRepository _repository;
late final CommandsRepository _commandsRepository;
Timer? _measureTimer;
static const int _historyPageSize = 20;
static const int _measureDurationSeconds = 35;
@override
HealthViewState build() {
_repository = ref.read(healthRepositoryProvider);
_commandsRepository = ref.read(commandsRepositoryProvider);
_init();
_resumeMeasureIfNeeded();
return const HealthViewState();
}
void _resumeMeasureIfNeeded() {
final endTime = ref.read(_measureEndTimeProvider);
if (endTime == null) return;
final remaining = endTime.difference(DateTime.now()).inSeconds;
if (remaining <= 0) {
ref.read(_measureEndTimeProvider.notifier).set(null);
return;
}
Future.microtask(() {
state = state.copyWith(
isMeasuringCountdown: true,
measureRemainingSeconds: remaining,
);
_startCountdownTimer();
});
}
String? get _identificator =>
ref.read(selectedDeviceProvider)?.identificator;
Future<void> selectTimeRange(TimeRange range) async {
if (range == state.timeRange) return;
state = state.copyWith(timeRange: range, isLoading: true);
state = state.copyWith(timeRange: range, isLoading: true, errorEvent: null);
await _loadFilteredData();
}
@@ -42,6 +78,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
customStart: start,
customEnd: end,
isLoading: true,
errorEvent: null,
);
await _loadFilteredData();
}
@@ -91,7 +128,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
if (!ref.mounted) return;
state = state.copyWith(
isLoadingMore: false,
errorMessage: _formatError(e),
errorEvent: HealthErrorEvent.loadMore,
);
}
}
@@ -131,7 +168,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
await _loadFilteredData();
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
state = state.copyWith(isLoading: false, errorEvent: HealthErrorEvent.loadData);
}
}
@@ -201,11 +238,11 @@ class HealthViewModel extends Notifier<HealthViewState> {
chartOxygens.map((e) => e.oxygen).toList(),
),
isLoading: false,
errorMessage: '',
errorEvent: null,
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
state = state.copyWith(isLoading: false, errorEvent: HealthErrorEvent.loadData);
}
}
@@ -245,9 +282,25 @@ class HealthViewModel extends Notifier<HealthViewState> {
);
}
String _formatError(Object e) {
final msg = e.toString();
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
Future<bool> updateHeartRateFrequency({required int frequency}) async {
final device = ref.read(selectedDeviceProvider);
if (device == null) return false;
try {
final updatedSettings = device.settings.copyWith(
frequencyHeartRate: frequency,
);
await ref.read(deviceSettingsUpdateProvider).updateDeviceSettings(
device: device,
updatedSettings: updatedSettings,
);
if (!ref.mounted) return false;
return true;
} catch (e) {
if (!ref.mounted) return false;
state = state.copyWith(errorEvent: HealthErrorEvent.heartRateFrequency);
return false;
}
}
Future<void> measure() async {
@@ -255,23 +308,54 @@ class HealthViewModel extends Notifier<HealthViewState> {
if (device == null) return;
try {
state = state.copyWith(isLoading: true);
state = state.copyWith(isMeasuring: true, errorEvent: null);
final request = SendCommandRequestModel(
device: device.identificator,
command: DeviceCommand.requestHeartRate,
);
await _commandsRepository.send(request: request);
if (!ref.mounted) return;
state = state.copyWith(isLoading: false);
ref.read(_measureEndTimeProvider.notifier).set(
DateTime.now().add(const Duration(seconds: _measureDurationSeconds)));
state = state.copyWith(
isMeasuring: false,
isMeasuringCountdown: true,
measureRemainingSeconds: _measureDurationSeconds,
);
_startCountdownTimer();
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: _formatError(e),
isMeasuring: false,
errorEvent: HealthErrorEvent.measure,
);
}
}
void _startCountdownTimer() {
_measureTimer?.cancel();
_measureTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!ref.mounted) {
timer.cancel();
return;
}
final remaining = state.measureRemainingSeconds - 1;
if (remaining <= 0) {
timer.cancel();
_measureTimer = null;
ref.read(_measureEndTimeProvider.notifier).set(null);
state = state.copyWith(
isMeasuringCountdown: false,
measureRemainingSeconds: 0,
);
_init();
} else {
state = state.copyWith(measureRemainingSeconds: remaining);
}
});
}
}

View File

@@ -6,6 +6,13 @@ import '../../domain/entities/oxygen_entity.dart';
part 'health_view_state.freezed.dart';
enum HealthErrorEvent {
loadData,
loadMore,
measure,
heartRateFrequency,
}
@freezed
abstract class HealthStats with _$HealthStats {
const factory HealthStats({
@@ -33,6 +40,9 @@ abstract class HealthViewState with _$HealthViewState {
@Default(null) DateTime? customEnd,
@Default(true) bool isLoading,
@Default(false) bool isLoadingMore,
@Default('') String errorMessage,
@Default(false) bool isMeasuring,
@Default(false) bool isMeasuringCountdown,
@Default(0) int measureRemainingSeconds,
HealthErrorEvent? errorEvent,
}) = _HealthViewState;
}

View File

@@ -277,7 +277,7 @@ as int,
/// @nodoc
mixin _$HealthViewState {
List<HeartbeatEntity> get latestHeartbeats; List<OxygenEntity> get latestOxygens; List<HeartbeatEntity> get chartHeartbeats; List<OxygenEntity> get chartOxygens; List<HeartbeatEntity> get historyHeartbeats; List<OxygenEntity> get historyOxygens; int get currentHistoryPage; bool get hasMoreHistory; HealthStats get heartRateStats; HealthStats get oxygenStats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; bool get isLoading; bool get isLoadingMore; String get errorMessage;
List<HeartbeatEntity> get latestHeartbeats; List<OxygenEntity> get latestOxygens; List<HeartbeatEntity> get chartHeartbeats; List<OxygenEntity> get chartOxygens; List<HeartbeatEntity> get historyHeartbeats; List<OxygenEntity> get historyOxygens; int get currentHistoryPage; bool get hasMoreHistory; HealthStats get heartRateStats; HealthStats get oxygenStats; TimeRange get timeRange; DateTime? get customStart; DateTime? get customEnd; bool get isLoading; bool get isLoadingMore; bool get isMeasuring; bool get isMeasuringCountdown; int get measureRemainingSeconds; HealthErrorEvent? get errorEvent;
/// Create a copy of HealthViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -288,16 +288,16 @@ $HealthViewStateCopyWith<HealthViewState> get copyWith => _$HealthViewStateCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is HealthViewState&&const DeepCollectionEquality().equals(other.latestHeartbeats, latestHeartbeats)&&const DeepCollectionEquality().equals(other.latestOxygens, latestOxygens)&&const DeepCollectionEquality().equals(other.chartHeartbeats, chartHeartbeats)&&const DeepCollectionEquality().equals(other.chartOxygens, chartOxygens)&&const DeepCollectionEquality().equals(other.historyHeartbeats, historyHeartbeats)&&const DeepCollectionEquality().equals(other.historyOxygens, historyOxygens)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.heartRateStats, heartRateStats) || other.heartRateStats == heartRateStats)&&(identical(other.oxygenStats, oxygenStats) || other.oxygenStats == oxygenStats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is HealthViewState&&const DeepCollectionEquality().equals(other.latestHeartbeats, latestHeartbeats)&&const DeepCollectionEquality().equals(other.latestOxygens, latestOxygens)&&const DeepCollectionEquality().equals(other.chartHeartbeats, chartHeartbeats)&&const DeepCollectionEquality().equals(other.chartOxygens, chartOxygens)&&const DeepCollectionEquality().equals(other.historyHeartbeats, historyHeartbeats)&&const DeepCollectionEquality().equals(other.historyOxygens, historyOxygens)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.heartRateStats, heartRateStats) || other.heartRateStats == heartRateStats)&&(identical(other.oxygenStats, oxygenStats) || other.oxygenStats == oxygenStats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.isMeasuring, isMeasuring) || other.isMeasuring == isMeasuring)&&(identical(other.isMeasuringCountdown, isMeasuringCountdown) || other.isMeasuringCountdown == isMeasuringCountdown)&&(identical(other.measureRemainingSeconds, measureRemainingSeconds) || other.measureRemainingSeconds == measureRemainingSeconds)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(latestHeartbeats),const DeepCollectionEquality().hash(latestOxygens),const DeepCollectionEquality().hash(chartHeartbeats),const DeepCollectionEquality().hash(chartOxygens),const DeepCollectionEquality().hash(historyHeartbeats),const DeepCollectionEquality().hash(historyOxygens),currentHistoryPage,hasMoreHistory,heartRateStats,oxygenStats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorMessage);
int get hashCode => Object.hashAll([runtimeType,const DeepCollectionEquality().hash(latestHeartbeats),const DeepCollectionEquality().hash(latestOxygens),const DeepCollectionEquality().hash(chartHeartbeats),const DeepCollectionEquality().hash(chartOxygens),const DeepCollectionEquality().hash(historyHeartbeats),const DeepCollectionEquality().hash(historyOxygens),currentHistoryPage,hasMoreHistory,heartRateStats,oxygenStats,timeRange,customStart,customEnd,isLoading,isLoadingMore,isMeasuring,isMeasuringCountdown,measureRemainingSeconds,errorEvent]);
@override
String toString() {
return 'HealthViewState(latestHeartbeats: $latestHeartbeats, latestOxygens: $latestOxygens, chartHeartbeats: $chartHeartbeats, chartOxygens: $chartOxygens, historyHeartbeats: $historyHeartbeats, historyOxygens: $historyOxygens, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, heartRateStats: $heartRateStats, oxygenStats: $oxygenStats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorMessage: $errorMessage)';
return 'HealthViewState(latestHeartbeats: $latestHeartbeats, latestOxygens: $latestOxygens, chartHeartbeats: $chartHeartbeats, chartOxygens: $chartOxygens, historyHeartbeats: $historyHeartbeats, historyOxygens: $historyOxygens, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, heartRateStats: $heartRateStats, oxygenStats: $oxygenStats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, isMeasuring: $isMeasuring, isMeasuringCountdown: $isMeasuringCountdown, measureRemainingSeconds: $measureRemainingSeconds, errorEvent: $errorEvent)';
}
@@ -308,7 +308,7 @@ abstract mixin class $HealthViewStateCopyWith<$Res> {
factory $HealthViewStateCopyWith(HealthViewState value, $Res Function(HealthViewState) _then) = _$HealthViewStateCopyWithImpl;
@useResult
$Res call({
List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage
List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent
});
@@ -325,7 +325,7 @@ class _$HealthViewStateCopyWithImpl<$Res>
/// Create a copy of HealthViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? latestHeartbeats = null,Object? latestOxygens = null,Object? chartHeartbeats = null,Object? chartOxygens = null,Object? historyHeartbeats = null,Object? historyOxygens = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? heartRateStats = null,Object? oxygenStats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? latestHeartbeats = null,Object? latestOxygens = null,Object? chartHeartbeats = null,Object? chartOxygens = null,Object? historyHeartbeats = null,Object? historyOxygens = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? heartRateStats = null,Object? oxygenStats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? isMeasuring = null,Object? isMeasuringCountdown = null,Object? measureRemainingSeconds = null,Object? errorEvent = freezed,}) {
return _then(_self.copyWith(
latestHeartbeats: null == latestHeartbeats ? _self.latestHeartbeats : latestHeartbeats // ignore: cast_nullable_to_non_nullable
as List<HeartbeatEntity>,latestOxygens: null == latestOxygens ? _self.latestOxygens : latestOxygens // ignore: cast_nullable_to_non_nullable
@@ -342,8 +342,11 @@ as TimeRange,customStart: freezed == customStart ? _self.customStart : customSta
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as bool,isMeasuring: null == isMeasuring ? _self.isMeasuring : isMeasuring // ignore: cast_nullable_to_non_nullable
as bool,isMeasuringCountdown: null == isMeasuringCountdown ? _self.isMeasuringCountdown : isMeasuringCountdown // ignore: cast_nullable_to_non_nullable
as bool,measureRemainingSeconds: null == measureRemainingSeconds ? _self.measureRemainingSeconds : measureRemainingSeconds // ignore: cast_nullable_to_non_nullable
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as HealthErrorEvent?,
));
}
/// Create a copy of HealthViewState
@@ -446,10 +449,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _HealthViewState() when $default != null:
return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats,_that.chartOxygens,_that.historyHeartbeats,_that.historyOxygens,_that.currentHistoryPage,_that.hasMoreHistory,_that.heartRateStats,_that.oxygenStats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorMessage);case _:
return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats,_that.chartOxygens,_that.historyHeartbeats,_that.historyOxygens,_that.currentHistoryPage,_that.hasMoreHistory,_that.heartRateStats,_that.oxygenStats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.isMeasuring,_that.isMeasuringCountdown,_that.measureRemainingSeconds,_that.errorEvent);case _:
return orElse();
}
@@ -467,10 +470,10 @@ return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent) $default,) {final _that = this;
switch (_that) {
case _HealthViewState():
return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats,_that.chartOxygens,_that.historyHeartbeats,_that.historyOxygens,_that.currentHistoryPage,_that.hasMoreHistory,_that.heartRateStats,_that.oxygenStats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorMessage);case _:
return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats,_that.chartOxygens,_that.historyHeartbeats,_that.historyOxygens,_that.currentHistoryPage,_that.hasMoreHistory,_that.heartRateStats,_that.oxygenStats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.isMeasuring,_that.isMeasuringCountdown,_that.measureRemainingSeconds,_that.errorEvent);case _:
throw StateError('Unexpected subclass');
}
@@ -487,10 +490,10 @@ return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent)? $default,) {final _that = this;
switch (_that) {
case _HealthViewState() when $default != null:
return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats,_that.chartOxygens,_that.historyHeartbeats,_that.historyOxygens,_that.currentHistoryPage,_that.hasMoreHistory,_that.heartRateStats,_that.oxygenStats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.errorMessage);case _:
return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats,_that.chartOxygens,_that.historyHeartbeats,_that.historyOxygens,_that.currentHistoryPage,_that.hasMoreHistory,_that.heartRateStats,_that.oxygenStats,_that.timeRange,_that.customStart,_that.customEnd,_that.isLoading,_that.isLoadingMore,_that.isMeasuring,_that.isMeasuringCountdown,_that.measureRemainingSeconds,_that.errorEvent);case _:
return null;
}
@@ -502,7 +505,7 @@ return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats
class _HealthViewState implements HealthViewState {
const _HealthViewState({final List<HeartbeatEntity> latestHeartbeats = const [], final List<OxygenEntity> latestOxygens = const [], final List<HeartbeatEntity> chartHeartbeats = const [], final List<OxygenEntity> chartOxygens = const [], final List<HeartbeatEntity> historyHeartbeats = const [], final List<OxygenEntity> historyOxygens = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.heartRateStats = const HealthStats(), this.oxygenStats = const HealthStats(), this.timeRange = TimeRange.today, this.customStart = null, this.customEnd = null, this.isLoading = true, this.isLoadingMore = false, this.errorMessage = ''}): _latestHeartbeats = latestHeartbeats,_latestOxygens = latestOxygens,_chartHeartbeats = chartHeartbeats,_chartOxygens = chartOxygens,_historyHeartbeats = historyHeartbeats,_historyOxygens = historyOxygens;
const _HealthViewState({final List<HeartbeatEntity> latestHeartbeats = const [], final List<OxygenEntity> latestOxygens = const [], final List<HeartbeatEntity> chartHeartbeats = const [], final List<OxygenEntity> chartOxygens = const [], final List<HeartbeatEntity> historyHeartbeats = const [], final List<OxygenEntity> historyOxygens = const [], this.currentHistoryPage = 1, this.hasMoreHistory = false, this.heartRateStats = const HealthStats(), this.oxygenStats = const HealthStats(), this.timeRange = TimeRange.today, this.customStart = null, this.customEnd = null, this.isLoading = true, this.isLoadingMore = false, this.isMeasuring = false, this.isMeasuringCountdown = false, this.measureRemainingSeconds = 0, this.errorEvent}): _latestHeartbeats = latestHeartbeats,_latestOxygens = latestOxygens,_chartHeartbeats = chartHeartbeats,_chartOxygens = chartOxygens,_historyHeartbeats = historyHeartbeats,_historyOxygens = historyOxygens;
final List<HeartbeatEntity> _latestHeartbeats;
@@ -556,7 +559,10 @@ class _HealthViewState implements HealthViewState {
@override@JsonKey() final DateTime? customEnd;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingMore;
@override@JsonKey() final String errorMessage;
@override@JsonKey() final bool isMeasuring;
@override@JsonKey() final bool isMeasuringCountdown;
@override@JsonKey() final int measureRemainingSeconds;
@override final HealthErrorEvent? errorEvent;
/// Create a copy of HealthViewState
/// with the given fields replaced by the non-null parameter values.
@@ -568,16 +574,16 @@ _$HealthViewStateCopyWith<_HealthViewState> get copyWith => __$HealthViewStateCo
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HealthViewState&&const DeepCollectionEquality().equals(other._latestHeartbeats, _latestHeartbeats)&&const DeepCollectionEquality().equals(other._latestOxygens, _latestOxygens)&&const DeepCollectionEquality().equals(other._chartHeartbeats, _chartHeartbeats)&&const DeepCollectionEquality().equals(other._chartOxygens, _chartOxygens)&&const DeepCollectionEquality().equals(other._historyHeartbeats, _historyHeartbeats)&&const DeepCollectionEquality().equals(other._historyOxygens, _historyOxygens)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.heartRateStats, heartRateStats) || other.heartRateStats == heartRateStats)&&(identical(other.oxygenStats, oxygenStats) || other.oxygenStats == oxygenStats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HealthViewState&&const DeepCollectionEquality().equals(other._latestHeartbeats, _latestHeartbeats)&&const DeepCollectionEquality().equals(other._latestOxygens, _latestOxygens)&&const DeepCollectionEquality().equals(other._chartHeartbeats, _chartHeartbeats)&&const DeepCollectionEquality().equals(other._chartOxygens, _chartOxygens)&&const DeepCollectionEquality().equals(other._historyHeartbeats, _historyHeartbeats)&&const DeepCollectionEquality().equals(other._historyOxygens, _historyOxygens)&&(identical(other.currentHistoryPage, currentHistoryPage) || other.currentHistoryPage == currentHistoryPage)&&(identical(other.hasMoreHistory, hasMoreHistory) || other.hasMoreHistory == hasMoreHistory)&&(identical(other.heartRateStats, heartRateStats) || other.heartRateStats == heartRateStats)&&(identical(other.oxygenStats, oxygenStats) || other.oxygenStats == oxygenStats)&&(identical(other.timeRange, timeRange) || other.timeRange == timeRange)&&(identical(other.customStart, customStart) || other.customStart == customStart)&&(identical(other.customEnd, customEnd) || other.customEnd == customEnd)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isLoadingMore, isLoadingMore) || other.isLoadingMore == isLoadingMore)&&(identical(other.isMeasuring, isMeasuring) || other.isMeasuring == isMeasuring)&&(identical(other.isMeasuringCountdown, isMeasuringCountdown) || other.isMeasuringCountdown == isMeasuringCountdown)&&(identical(other.measureRemainingSeconds, measureRemainingSeconds) || other.measureRemainingSeconds == measureRemainingSeconds)&&(identical(other.errorEvent, errorEvent) || other.errorEvent == errorEvent));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_latestHeartbeats),const DeepCollectionEquality().hash(_latestOxygens),const DeepCollectionEquality().hash(_chartHeartbeats),const DeepCollectionEquality().hash(_chartOxygens),const DeepCollectionEquality().hash(_historyHeartbeats),const DeepCollectionEquality().hash(_historyOxygens),currentHistoryPage,hasMoreHistory,heartRateStats,oxygenStats,timeRange,customStart,customEnd,isLoading,isLoadingMore,errorMessage);
int get hashCode => Object.hashAll([runtimeType,const DeepCollectionEquality().hash(_latestHeartbeats),const DeepCollectionEquality().hash(_latestOxygens),const DeepCollectionEquality().hash(_chartHeartbeats),const DeepCollectionEquality().hash(_chartOxygens),const DeepCollectionEquality().hash(_historyHeartbeats),const DeepCollectionEquality().hash(_historyOxygens),currentHistoryPage,hasMoreHistory,heartRateStats,oxygenStats,timeRange,customStart,customEnd,isLoading,isLoadingMore,isMeasuring,isMeasuringCountdown,measureRemainingSeconds,errorEvent]);
@override
String toString() {
return 'HealthViewState(latestHeartbeats: $latestHeartbeats, latestOxygens: $latestOxygens, chartHeartbeats: $chartHeartbeats, chartOxygens: $chartOxygens, historyHeartbeats: $historyHeartbeats, historyOxygens: $historyOxygens, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, heartRateStats: $heartRateStats, oxygenStats: $oxygenStats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, errorMessage: $errorMessage)';
return 'HealthViewState(latestHeartbeats: $latestHeartbeats, latestOxygens: $latestOxygens, chartHeartbeats: $chartHeartbeats, chartOxygens: $chartOxygens, historyHeartbeats: $historyHeartbeats, historyOxygens: $historyOxygens, currentHistoryPage: $currentHistoryPage, hasMoreHistory: $hasMoreHistory, heartRateStats: $heartRateStats, oxygenStats: $oxygenStats, timeRange: $timeRange, customStart: $customStart, customEnd: $customEnd, isLoading: $isLoading, isLoadingMore: $isLoadingMore, isMeasuring: $isMeasuring, isMeasuringCountdown: $isMeasuringCountdown, measureRemainingSeconds: $measureRemainingSeconds, errorEvent: $errorEvent)';
}
@@ -588,7 +594,7 @@ abstract mixin class _$HealthViewStateCopyWith<$Res> implements $HealthViewState
factory _$HealthViewStateCopyWith(_HealthViewState value, $Res Function(_HealthViewState) _then) = __$HealthViewStateCopyWithImpl;
@override @useResult
$Res call({
List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, String errorMessage
List<HeartbeatEntity> latestHeartbeats, List<OxygenEntity> latestOxygens, List<HeartbeatEntity> chartHeartbeats, List<OxygenEntity> chartOxygens, List<HeartbeatEntity> historyHeartbeats, List<OxygenEntity> historyOxygens, int currentHistoryPage, bool hasMoreHistory, HealthStats heartRateStats, HealthStats oxygenStats, TimeRange timeRange, DateTime? customStart, DateTime? customEnd, bool isLoading, bool isLoadingMore, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent
});
@@ -605,7 +611,7 @@ class __$HealthViewStateCopyWithImpl<$Res>
/// Create a copy of HealthViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? latestHeartbeats = null,Object? latestOxygens = null,Object? chartHeartbeats = null,Object? chartOxygens = null,Object? historyHeartbeats = null,Object? historyOxygens = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? heartRateStats = null,Object? oxygenStats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? latestHeartbeats = null,Object? latestOxygens = null,Object? chartHeartbeats = null,Object? chartOxygens = null,Object? historyHeartbeats = null,Object? historyOxygens = null,Object? currentHistoryPage = null,Object? hasMoreHistory = null,Object? heartRateStats = null,Object? oxygenStats = null,Object? timeRange = null,Object? customStart = freezed,Object? customEnd = freezed,Object? isLoading = null,Object? isLoadingMore = null,Object? isMeasuring = null,Object? isMeasuringCountdown = null,Object? measureRemainingSeconds = null,Object? errorEvent = freezed,}) {
return _then(_HealthViewState(
latestHeartbeats: null == latestHeartbeats ? _self._latestHeartbeats : latestHeartbeats // ignore: cast_nullable_to_non_nullable
as List<HeartbeatEntity>,latestOxygens: null == latestOxygens ? _self._latestOxygens : latestOxygens // ignore: cast_nullable_to_non_nullable
@@ -622,8 +628,11 @@ as TimeRange,customStart: freezed == customStart ? _self.customStart : customSta
as DateTime?,customEnd: freezed == customEnd ? _self.customEnd : customEnd // ignore: cast_nullable_to_non_nullable
as DateTime?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isLoadingMore: null == isLoadingMore ? _self.isLoadingMore : isLoadingMore // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as bool,isMeasuring: null == isMeasuring ? _self.isMeasuring : isMeasuring // ignore: cast_nullable_to_non_nullable
as bool,isMeasuringCountdown: null == isMeasuringCountdown ? _self.isMeasuringCountdown : isMeasuringCountdown // ignore: cast_nullable_to_non_nullable
as bool,measureRemainingSeconds: null == measureRemainingSeconds ? _self.measureRemainingSeconds : measureRemainingSeconds // ignore: cast_nullable_to_non_nullable
as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as HealthErrorEvent?,
));
}

View File

@@ -46,7 +46,11 @@ class LocateDeviceViewModel extends Notifier<LocateDeviceViewState> {
}
}
void resetError() {
state = state.copyWith(errorMessage: '');
void reset() {
state = state.copyWith(
isLoading: false,
isComplete: false,
errorMessage: '',
);
}
}

View File

@@ -15,6 +15,20 @@ class LocateDeviceDialog extends ConsumerWidget {
final state = ref.watch(locateDeviceViewModelProvider);
final vm = ref.read(locateDeviceViewModelProvider.notifier);
ref.listen(
locateDeviceViewModelProvider.select((s) => s.isComplete),
(previous, next) {
if (next && !(previous ?? false)) {
Future.delayed(const Duration(seconds: 1), () {
if (context.mounted) {
Navigator.pop(context);
vm.reset();
}
});
}
},
);
return Container(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 32, big: 30),
@@ -44,7 +58,7 @@ class LocateDeviceDialog extends ConsumerWidget {
PrimaryButton(
onPressed: () {
Navigator.pop(context);
vm.resetError();
vm.reset();
},
text: context.translate(I18n.ok),
color: theme.getColorFor(ThemeCode.legacyPrimary),

View File

@@ -1,9 +0,0 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'device_update_datasource.dart';
final deviceUpdateDatasourceProvider = Provider<DeviceUpdateDatasource>((ref) {
return DeviceUpdateDatasource(GetIt.I<QuestiaRepository>());
});

View File

@@ -1,8 +1,7 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../data/device_update_datasource.dart';
import '../../data/device_update_datasource_provider.dart';
import 'volume_control_view_state.dart';
final volumeControlViewModelProvider =
@@ -11,11 +10,11 @@ final volumeControlViewModelProvider =
);
class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
late final DeviceUpdateDatasource _datasource;
late final DeviceSettingsUpdateDatasource _datasource;
@override
VolumeControlViewState build() {
_datasource = ref.read(deviceUpdateDatasourceProvider);
_datasource = ref.read(deviceSettingsUpdateProvider);
Future.microtask(() => _load());
return const VolumeControlViewState();
}
@@ -25,14 +24,14 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
final volume = device.settings['volume'] as Map<String, dynamic>? ?? {};
final volume = device.settings.volume;
state = state.copyWith(
isLoading: false,
device: device,
media: (volume['media'] as num?)?.toInt() ?? 50,
ringtone: (volume['ringtone'] as num?)?.toInt() ?? 50,
alarm: (volume['alarm'] as num?)?.toInt() ?? 50,
media: volume.media,
ringtone: volume.ringtone,
alarm: volume.alarm,
);
} catch (e) {
if (!ref.mounted) return;
@@ -51,16 +50,17 @@ class VolumeControlViewModel extends Notifier<VolumeControlViewState> {
try {
state = state.copyWith(isLoading: true, isComplete: false, errorMessage: '');
final settings = Map<String, dynamic>.from(device.settings);
settings['volume'] = {
'media': state.media,
'ringtone': state.ringtone,
'alarm': state.alarm,
};
final updatedSettings = device.settings.copyWith(
volume: DeviceVolumeEntity(
media: state.media,
ringtone: state.ringtone,
alarm: state.alarm,
),
);
await _datasource.updateDeviceSettings(
device: device,
updatedSettings: settings,
updatedSettings: updatedSettings,
);
if (!ref.mounted) return;

View File

@@ -634,7 +634,7 @@ packages:
source: hosted
version: "1.3.0"
lottie:
dependency: transitive
dependency: "direct main"
description:
name: lottie
sha256: "8ae0be46dbd9e19641791dc12ee480d34e1fd3f84c749adc05f3ad9342b71b95"

View File

@@ -56,6 +56,7 @@ dependencies:
uuid: ^4.5.1
flutter_contacts: ^1.1.9+2
fl_chart: ^1.1.1
lottie: ^3.3.1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@@ -75,12 +76,6 @@ flutter:
# the material Icons class.
uses-material-design: true
# To add Flutter specific assets to your application, add an assets section,
# like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:location/src/features/location/presentation/state/location_view_model.dart';
import 'package:location/src/features/location/presentation/state/location_view_state.dart';
import 'package:location/src/features/location/presentation/widgets/location_map.dart';
import 'package:sf_localizations/sf_localizations.dart';
@@ -16,6 +17,55 @@ class LocationScreen extends ConsumerWidget {
final controlPanelState = ref.watch(controlPanelViewModelProvider);
final locationState = 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(
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,

View File

@@ -119,6 +119,14 @@ class LocationMapViewModel extends Notifier<LocationMapViewState> {
state = state.copyWith(actionsExpanded: !state.actionsExpanded);
}
void toggleFrequencyExpanded() {
state = state.copyWith(frequencyExpanded: !state.frequencyExpanded);
}
void collapseFrequency() {
state = state.copyWith(frequencyExpanded: false);
}
void updateMapZoom(double zoom) {
state = state.copyWith(mapZoom: zoom);
}

View File

@@ -25,6 +25,7 @@ abstract class LocationMapViewState with _$LocationMapViewState {
PositionEntity? selectedHistoryPosition,
@Default(false) bool isFollowing,
@Default(false) bool actionsExpanded,
@Default(false) bool frequencyExpanded,
@Default(_defaultZoom) double mapZoom,
}) = _LocationMapViewState;
}

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$LocationMapViewState {
bool get showGeofences; bool get showFrequentPlaces; PlacingMode get placingMode; bool get adjustingRadius; double get previewRadius; LatLng? get previewPoint; GeofenceEntity? get selectedGeofence; GeofenceEntity? get editingGeofence; FrequentPlaceEntity? get selectedFrequentPlace; PositionEntity? get selectedHistoryPosition; bool get isFollowing; bool get actionsExpanded; double get mapZoom;
bool get showGeofences; bool get showFrequentPlaces; PlacingMode get placingMode; bool get adjustingRadius; double get previewRadius; LatLng? get previewPoint; GeofenceEntity? get selectedGeofence; GeofenceEntity? get editingGeofence; FrequentPlaceEntity? get selectedFrequentPlace; PositionEntity? get selectedHistoryPosition; bool get isFollowing; bool get actionsExpanded; bool get frequencyExpanded; double get mapZoom;
/// Create a copy of LocationMapViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $LocationMapViewStateCopyWith<LocationMapViewState> get copyWith => _$LocationMa
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationMapViewState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom));
return identical(this, other) || (other.runtimeType == runtimeType&&other is LocationMapViewState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom));
}
@override
int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,mapZoom);
int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom);
@override
String toString() {
return 'LocationMapViewState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, mapZoom: $mapZoom)';
return 'LocationMapViewState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom)';
}
@@ -45,7 +45,7 @@ abstract mixin class $LocationMapViewStateCopyWith<$Res> {
factory $LocationMapViewStateCopyWith(LocationMapViewState value, $Res Function(LocationMapViewState) _then) = _$LocationMapViewStateCopyWithImpl;
@useResult
$Res call({
bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, double mapZoom
bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom
});
@@ -62,7 +62,7 @@ class _$LocationMapViewStateCopyWithImpl<$Res>
/// Create a copy of LocationMapViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? mapZoom = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,}) {
return _then(_self.copyWith(
showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable
as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable
@@ -76,6 +76,7 @@ as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _se
as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable
as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable
as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable
as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable
as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable
as double,
));
@@ -210,10 +211,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, double mapZoom)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _LocationMapViewState() when $default != null:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.mapZoom);case _:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _:
return orElse();
}
@@ -231,10 +232,10 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, double mapZoom) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom) $default,) {final _that = this;
switch (_that) {
case _LocationMapViewState():
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.mapZoom);case _:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _:
throw StateError('Unexpected subclass');
}
@@ -251,10 +252,10 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, double mapZoom)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom)? $default,) {final _that = this;
switch (_that) {
case _LocationMapViewState() when $default != null:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.mapZoom);case _:
return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_that.adjustingRadius,_that.previewRadius,_that.previewPoint,_that.selectedGeofence,_that.editingGeofence,_that.selectedFrequentPlace,_that.selectedHistoryPosition,_that.isFollowing,_that.actionsExpanded,_that.frequencyExpanded,_that.mapZoom);case _:
return null;
}
@@ -266,7 +267,7 @@ return $default(_that.showGeofences,_that.showFrequentPlaces,_that.placingMode,_
class _LocationMapViewState implements LocationMapViewState {
const _LocationMapViewState({this.showGeofences = true, this.showFrequentPlaces = true, this.placingMode = PlacingMode.none, this.adjustingRadius = false, this.previewRadius = 200.0, this.previewPoint, this.selectedGeofence, this.editingGeofence, this.selectedFrequentPlace, this.selectedHistoryPosition, this.isFollowing = false, this.actionsExpanded = false, this.mapZoom = _defaultZoom});
const _LocationMapViewState({this.showGeofences = true, this.showFrequentPlaces = true, this.placingMode = PlacingMode.none, this.adjustingRadius = false, this.previewRadius = 200.0, this.previewPoint, this.selectedGeofence, this.editingGeofence, this.selectedFrequentPlace, this.selectedHistoryPosition, this.isFollowing = false, this.actionsExpanded = false, this.frequencyExpanded = false, this.mapZoom = _defaultZoom});
@override@JsonKey() final bool showGeofences;
@@ -281,6 +282,7 @@ class _LocationMapViewState implements LocationMapViewState {
@override final PositionEntity? selectedHistoryPosition;
@override@JsonKey() final bool isFollowing;
@override@JsonKey() final bool actionsExpanded;
@override@JsonKey() final bool frequencyExpanded;
@override@JsonKey() final double mapZoom;
/// Create a copy of LocationMapViewState
@@ -293,16 +295,16 @@ _$LocationMapViewStateCopyWith<_LocationMapViewState> get copyWith => __$Locatio
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationMapViewState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _LocationMapViewState&&(identical(other.showGeofences, showGeofences) || other.showGeofences == showGeofences)&&(identical(other.showFrequentPlaces, showFrequentPlaces) || other.showFrequentPlaces == showFrequentPlaces)&&(identical(other.placingMode, placingMode) || other.placingMode == placingMode)&&(identical(other.adjustingRadius, adjustingRadius) || other.adjustingRadius == adjustingRadius)&&(identical(other.previewRadius, previewRadius) || other.previewRadius == previewRadius)&&(identical(other.previewPoint, previewPoint) || other.previewPoint == previewPoint)&&(identical(other.selectedGeofence, selectedGeofence) || other.selectedGeofence == selectedGeofence)&&(identical(other.editingGeofence, editingGeofence) || other.editingGeofence == editingGeofence)&&(identical(other.selectedFrequentPlace, selectedFrequentPlace) || other.selectedFrequentPlace == selectedFrequentPlace)&&(identical(other.selectedHistoryPosition, selectedHistoryPosition) || other.selectedHistoryPosition == selectedHistoryPosition)&&(identical(other.isFollowing, isFollowing) || other.isFollowing == isFollowing)&&(identical(other.actionsExpanded, actionsExpanded) || other.actionsExpanded == actionsExpanded)&&(identical(other.frequencyExpanded, frequencyExpanded) || other.frequencyExpanded == frequencyExpanded)&&(identical(other.mapZoom, mapZoom) || other.mapZoom == mapZoom));
}
@override
int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,mapZoom);
int get hashCode => Object.hash(runtimeType,showGeofences,showFrequentPlaces,placingMode,adjustingRadius,previewRadius,previewPoint,selectedGeofence,editingGeofence,selectedFrequentPlace,selectedHistoryPosition,isFollowing,actionsExpanded,frequencyExpanded,mapZoom);
@override
String toString() {
return 'LocationMapViewState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, mapZoom: $mapZoom)';
return 'LocationMapViewState(showGeofences: $showGeofences, showFrequentPlaces: $showFrequentPlaces, placingMode: $placingMode, adjustingRadius: $adjustingRadius, previewRadius: $previewRadius, previewPoint: $previewPoint, selectedGeofence: $selectedGeofence, editingGeofence: $editingGeofence, selectedFrequentPlace: $selectedFrequentPlace, selectedHistoryPosition: $selectedHistoryPosition, isFollowing: $isFollowing, actionsExpanded: $actionsExpanded, frequencyExpanded: $frequencyExpanded, mapZoom: $mapZoom)';
}
@@ -313,7 +315,7 @@ abstract mixin class _$LocationMapViewStateCopyWith<$Res> implements $LocationMa
factory _$LocationMapViewStateCopyWith(_LocationMapViewState value, $Res Function(_LocationMapViewState) _then) = __$LocationMapViewStateCopyWithImpl;
@override @useResult
$Res call({
bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, double mapZoom
bool showGeofences, bool showFrequentPlaces, PlacingMode placingMode, bool adjustingRadius, double previewRadius, LatLng? previewPoint, GeofenceEntity? selectedGeofence, GeofenceEntity? editingGeofence, FrequentPlaceEntity? selectedFrequentPlace, PositionEntity? selectedHistoryPosition, bool isFollowing, bool actionsExpanded, bool frequencyExpanded, double mapZoom
});
@@ -330,7 +332,7 @@ class __$LocationMapViewStateCopyWithImpl<$Res>
/// Create a copy of LocationMapViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? mapZoom = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? showGeofences = null,Object? showFrequentPlaces = null,Object? placingMode = null,Object? adjustingRadius = null,Object? previewRadius = null,Object? previewPoint = freezed,Object? selectedGeofence = freezed,Object? editingGeofence = freezed,Object? selectedFrequentPlace = freezed,Object? selectedHistoryPosition = freezed,Object? isFollowing = null,Object? actionsExpanded = null,Object? frequencyExpanded = null,Object? mapZoom = null,}) {
return _then(_LocationMapViewState(
showGeofences: null == showGeofences ? _self.showGeofences : showGeofences // ignore: cast_nullable_to_non_nullable
as bool,showFrequentPlaces: null == showFrequentPlaces ? _self.showFrequentPlaces : showFrequentPlaces // ignore: cast_nullable_to_non_nullable
@@ -344,6 +346,7 @@ as GeofenceEntity?,selectedFrequentPlace: freezed == selectedFrequentPlace ? _se
as FrequentPlaceEntity?,selectedHistoryPosition: freezed == selectedHistoryPosition ? _self.selectedHistoryPosition : selectedHistoryPosition // ignore: cast_nullable_to_non_nullable
as PositionEntity?,isFollowing: null == isFollowing ? _self.isFollowing : isFollowing // ignore: cast_nullable_to_non_nullable
as bool,actionsExpanded: null == actionsExpanded ? _self.actionsExpanded : actionsExpanded // ignore: cast_nullable_to_non_nullable
as bool,frequencyExpanded: null == frequencyExpanded ? _self.frequencyExpanded : frequencyExpanded // ignore: cast_nullable_to_non_nullable
as bool,mapZoom: null == mapZoom ? _self.mapZoom : mapZoom // ignore: cast_nullable_to_non_nullable
as double,
));

View File

@@ -45,7 +45,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: _formatError(e));
state = state.copyWith(isLoading: false);
}
}
@@ -56,7 +56,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double longitude,
required double radius,
}) async {
state = state.copyWith(isSubmitting: true, errorMessage: '');
state = state.copyWith(isSubmitting: true, errorEvent: null, successMessage: null);
try {
final user = await ref.read(userInfoProvider.future);
final device = ref.read(selectedDeviceProvider);
@@ -78,10 +78,11 @@ class LocationViewModel extends Notifier<LocationViewState> {
state = state.copyWith(
geofences: [...state.geofences, created],
isSubmitting: false,
successMessage: LocationSuccessEvent.geofenceCreated,
);
return true;
} catch (e) {
return _handleError(e);
return _handleErrorEvent(LocationErrorEvent.geofenceCreate);
}
}
@@ -93,7 +94,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double longitude,
required double radius,
}) async {
state = state.copyWith(isSubmitting: true, errorMessage: '');
state = state.copyWith(isSubmitting: true, errorEvent: null, successMessage: null);
try {
final request = UpdateGeofenceRequestModel(
id: id,
@@ -110,24 +111,26 @@ class LocationViewModel extends Notifier<LocationViewState> {
geofences:
state.geofences.map((g) => g.id == id ? updated : g).toList(),
isSubmitting: false,
successMessage: LocationSuccessEvent.geofenceUpdated,
);
return true;
} catch (e) {
return _handleError(e);
return _handleErrorEvent(LocationErrorEvent.geofenceUpdate);
}
}
Future<bool> deleteGeofence({required String id}) async {
state = state.copyWith(errorMessage: '');
state = state.copyWith(errorEvent: null, successMessage: null);
try {
await _locationRepository.deleteGeofence(geofenceId: id);
if (!ref.mounted) return false;
state = state.copyWith(
geofences: state.geofences.where((g) => g.id != id).toList(),
successMessage: LocationSuccessEvent.geofenceDeleted,
);
return true;
} catch (e) {
return _handleError(e);
return _handleErrorEvent(LocationErrorEvent.geofenceDelete);
}
}
@@ -137,7 +140,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double lng,
List<WifiInfoEntity> wifiList = const [],
}) async {
state = state.copyWith(isSubmitting: true, errorMessage: '');
state = state.copyWith(isSubmitting: true, errorEvent: null, successMessage: null);
try {
final user = await ref.read(userInfoProvider.future);
final device = ref.read(selectedDeviceProvider);
@@ -164,10 +167,11 @@ class LocationViewModel extends Notifier<LocationViewState> {
state = state.copyWith(
frequentPlaces: [...state.frequentPlaces, created],
isSubmitting: false,
successMessage: LocationSuccessEvent.frequentPlaceCreated,
);
return true;
} catch (e) {
return _handleError(e);
return _handleErrorEvent(LocationErrorEvent.frequentPlaceCreate);
}
}
@@ -178,7 +182,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
required double lng,
List<WifiInfoEntity> wifiList = const [],
}) async {
state = state.copyWith(isSubmitting: true, errorMessage: '');
state = state.copyWith(isSubmitting: true, errorEvent: null, successMessage: null);
try {
final request = UpdateFrequentPlaceRequestModel(
id: id,
@@ -200,25 +204,27 @@ class LocationViewModel extends Notifier<LocationViewState> {
frequentPlaces:
state.frequentPlaces.map((f) => f.id == id ? updated : f).toList(),
isSubmitting: false,
successMessage: LocationSuccessEvent.frequentPlaceUpdated,
);
return true;
} catch (e) {
return _handleError(e);
return _handleErrorEvent(LocationErrorEvent.frequentPlaceUpdate);
}
}
Future<bool> deleteFrequentPlace({required String id}) async {
state = state.copyWith(errorMessage: '');
state = state.copyWith(errorEvent: null, successMessage: null);
try {
await _locationRepository.deleteFrequentPlace(frequentPlaceId: id);
if (!ref.mounted) return false;
state = state.copyWith(
frequentPlaces:
state.frequentPlaces.where((f) => f.id != id).toList(),
successMessage: LocationSuccessEvent.frequentPlaceDeleted,
);
return true;
} catch (e) {
return _handleError(e);
return _handleErrorEvent(LocationErrorEvent.frequentPlaceDelete);
}
}
@@ -229,7 +235,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
state = state.copyWith(isLoadingHistory: true, errorMessage: '');
state = state.copyWith(isLoadingHistory: true, errorEvent: null);
try {
final positions = await _locationRepository.getPositionHistory(
deviceIdentificator: device.identificator,
@@ -246,7 +252,7 @@ class LocationViewModel extends Notifier<LocationViewState> {
if (!ref.mounted) return;
state = state.copyWith(
isLoadingHistory: false,
errorMessage: _formatError(e),
errorEvent: LocationErrorEvent.positionHistory,
);
}
}
@@ -262,17 +268,33 @@ class LocationViewModel extends Notifier<LocationViewState> {
state = state.copyWith(showRouteTrail: !state.showRouteTrail);
}
bool _handleError(Object e) {
Future<bool> updateLocationFrequency({required int frequency}) async {
final device = ref.read(selectedDeviceProvider);
if (device == null) return false;
state = state.copyWith(isSubmitting: true, errorEvent: null);
try {
final updatedSettings = device.settings.copyWith(frequency: frequency);
await ref.read(deviceSettingsUpdateProvider).updateDeviceSettings(
device: device,
updatedSettings: updatedSettings,
);
if (!ref.mounted) return false;
state = state.copyWith(isSubmitting: false);
return true;
} catch (e) {
return _handleErrorEvent(LocationErrorEvent.locationFrequency);
}
}
bool _handleErrorEvent(LocationErrorEvent event) {
if (!ref.mounted) return false;
state = state.copyWith(
isSubmitting: false,
errorMessage: _formatError(e),
errorEvent: event,
);
return false;
}
String _formatError(Object e) {
final msg = e.toString();
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
}
}

View File

@@ -5,6 +5,26 @@ import 'package:location/src/core/domain/entities/frequent_place_entity.dart';
part 'location_view_state.freezed.dart';
enum LocationSuccessEvent {
geofenceCreated,
geofenceUpdated,
geofenceDeleted,
frequentPlaceCreated,
frequentPlaceUpdated,
frequentPlaceDeleted,
}
enum LocationErrorEvent {
geofenceCreate,
geofenceUpdate,
geofenceDelete,
frequentPlaceCreate,
frequentPlaceUpdate,
frequentPlaceDelete,
positionHistory,
locationFrequency,
}
@freezed
abstract class LocationViewState with _$LocationViewState {
const factory LocationViewState({
@@ -15,6 +35,7 @@ abstract class LocationViewState with _$LocationViewState {
@Default(false) bool isLoadingHistory,
@Default(false) bool isSubmitting,
@Default(false) bool showRouteTrail,
@Default('') String errorMessage,
LocationErrorEvent? errorEvent,
LocationSuccessEvent? successMessage,
}) = _LocationViewState;
}

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; String get errorMessage;
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;
/// 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.errorMessage, errorMessage) || other.errorMessage == errorMessage));
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));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(geofences),const DeepCollectionEquality().hash(frequentPlaces),const DeepCollectionEquality().hash(positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorMessage);
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(geofences),const DeepCollectionEquality().hash(frequentPlaces),const DeepCollectionEquality().hash(positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage);
@override
String toString() {
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorMessage: $errorMessage)';
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, 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, String errorMessage
List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage
});
@@ -62,7 +62,7 @@ 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? errorMessage = null,}) {
@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,}) {
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
@@ -71,8 +71,9 @@ as List<PositionEntity>,isLoading: null == isLoading ? _self.isLoading : isLoadi
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,showRouteTrail: null == showRouteTrail ? _self.showRouteTrail : showRouteTrail // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as LocationErrorEvent?,successMessage: freezed == successMessage ? _self.successMessage : successMessage // ignore: cast_nullable_to_non_nullable
as LocationSuccessEvent?,
));
}
@@ -157,10 +158,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, String errorMessage)? $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 isLoading, 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.errorMessage);case _:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return orElse();
}
@@ -178,10 +179,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, String errorMessage) $default,) {final _that = this;
@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;
switch (_that) {
case _LocationViewState():
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorMessage);case _:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -198,10 +199,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, String errorMessage)? $default,) {final _that = this;
@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;
switch (_that) {
case _LocationViewState() when $default != null:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorMessage);case _:
return $default(_that.geofences,_that.frequentPlaces,_that.positionHistory,_that.isLoading,_that.isLoadingHistory,_that.isSubmitting,_that.showRouteTrail,_that.errorEvent,_that.successMessage);case _:
return null;
}
@@ -213,7 +214,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.errorMessage = ''}): _geofences = geofences,_frequentPlaces = frequentPlaces,_positionHistory = positionHistory;
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;
final List<GeofenceEntity> _geofences;
@@ -241,7 +242,8 @@ class _LocationViewState implements LocationViewState {
@override@JsonKey() final bool isLoadingHistory;
@override@JsonKey() final bool isSubmitting;
@override@JsonKey() final bool showRouteTrail;
@override@JsonKey() final String errorMessage;
@override final LocationErrorEvent? errorEvent;
@override final LocationSuccessEvent? successMessage;
/// Create a copy of LocationViewState
/// with the given fields replaced by the non-null parameter values.
@@ -253,16 +255,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.errorMessage, errorMessage) || other.errorMessage == errorMessage));
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));
}
@override
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_geofences),const DeepCollectionEquality().hash(_frequentPlaces),const DeepCollectionEquality().hash(_positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorMessage);
int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_geofences),const DeepCollectionEquality().hash(_frequentPlaces),const DeepCollectionEquality().hash(_positionHistory),isLoading,isLoadingHistory,isSubmitting,showRouteTrail,errorEvent,successMessage);
@override
String toString() {
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorMessage: $errorMessage)';
return 'LocationViewState(geofences: $geofences, frequentPlaces: $frequentPlaces, positionHistory: $positionHistory, isLoading: $isLoading, isLoadingHistory: $isLoadingHistory, isSubmitting: $isSubmitting, showRouteTrail: $showRouteTrail, errorEvent: $errorEvent, successMessage: $successMessage)';
}
@@ -273,7 +275,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, String errorMessage
List<GeofenceEntity> geofences, List<FrequentPlaceEntity> frequentPlaces, List<PositionEntity> positionHistory, bool isLoading, bool isLoadingHistory, bool isSubmitting, bool showRouteTrail, LocationErrorEvent? errorEvent, LocationSuccessEvent? successMessage
});
@@ -290,7 +292,7 @@ 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? errorMessage = null,}) {
@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,}) {
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
@@ -299,8 +301,9 @@ as List<PositionEntity>,isLoading: null == isLoading ? _self.isLoading : isLoadi
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,showRouteTrail: null == showRouteTrail ? _self.showRouteTrail : showRouteTrail // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
as bool,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // ignore: cast_nullable_to_non_nullable
as LocationErrorEvent?,successMessage: freezed == successMessage ? _self.successMessage : successMessage // ignore: cast_nullable_to_non_nullable
as LocationSuccessEvent?,
));
}

View File

@@ -124,8 +124,8 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> {
final isSubmitting = ref.watch(
locationViewModelProvider.select((s) => s.isSubmitting),
);
final errorMessage = ref.watch(
locationViewModelProvider.select((s) => s.errorMessage),
final errorEvent = ref.watch(
locationViewModelProvider.select((s) => s.errorEvent),
);
return DraggableScrollableSheet(
@@ -270,11 +270,11 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> {
);
}),
],
if (errorMessage.isNotEmpty)
if (errorEvent != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
errorMessage,
context.translate(I18n.errorGeneric),
style:
const TextStyle(color: Colors.red, fontSize: 13),
),

View File

@@ -130,8 +130,8 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> {
final isSubmitting = ref.watch(
locationViewModelProvider.select((s) => s.isSubmitting),
);
final errorMessage = ref.watch(
locationViewModelProvider.select((s) => s.errorMessage),
final errorEvent = ref.watch(
locationViewModelProvider.select((s) => s.errorEvent),
);
return DraggableScrollableSheet(
@@ -252,11 +252,11 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> {
primaryColor: primaryColor,
),
),
if (errorMessage.isNotEmpty)
if (errorEvent != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
errorMessage,
context.translate(I18n.errorGeneric),
style: const TextStyle(color: Colors.red, fontSize: 13),
),
),

View File

@@ -80,6 +80,15 @@ class _LocationMapState extends ConsumerState<LocationMap>
void initState() {
super.initState();
_mapController = MapController();
_startMonitoring();
}
void _startMonitoring() {
_followTimer?.cancel();
final frequency = widget.selectedDevice?.settings.frequency ?? 60;
_followTimer = Timer.periodic(Duration(seconds: frequency), (_) {
widget.onRefreshPosition();
});
}
@override
@@ -94,7 +103,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
void didUpdateWidget(LocationMap oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.selectedDevice?.id != oldWidget.selectedDevice?.id) {
_stopFollowing();
_startMonitoring();
}
if (widget.selectedPosition != null &&
widget.selectedPosition != oldWidget.selectedPosition) {
@@ -176,28 +185,6 @@ class _LocationMapState extends ConsumerState<LocationMap>
_animatedMove(target.center, target.zoom);
}
void _toggleFollowing() {
final wasFollowing = ref.read(locationMapViewModelProvider).isFollowing;
_vm.toggleFollowing();
if (!wasFollowing) {
_centerOnDevice();
widget.onRefreshPosition();
_followTimer = Timer.periodic(const Duration(seconds: 30), (_) {
widget.onRefreshPosition();
});
} else {
_stopFollowing();
}
}
void _stopFollowing() {
_followTimer?.cancel();
_followTimer = null;
_vm.stopFollowing();
}
void _shareLocation() {
final position = widget.selectedPosition;
if (position == null) return;
@@ -220,6 +207,33 @@ class _LocationMapState extends ConsumerState<LocationMap>
Share.share(text.toString().trim());
}
Future<void> _updateFrequency(int frequency) async {
final success = await ref
.read(locationViewModelProvider.notifier)
.updateLocationFrequency(frequency: frequency);
if (!mounted) return;
if (success) {
_followTimer?.cancel();
_followTimer = Timer.periodic(Duration(seconds: frequency), (_) {
widget.onRefreshPosition();
});
showTopSnackbar(
context,
message: context.translate(
I18n.locationFrequencyUpdated,
args: {'minutes': '${frequency ~/ 60}'},
),
type: MessageType.success,
);
} else {
showTopSnackbar(
context,
message: context.translate(I18n.errorLocationFrequency),
type: MessageType.error,
);
}
}
void _confirmPlacement() {
final center = _mapController.camera.center;
final mapState = ref.read(locationMapViewModelProvider);
@@ -540,7 +554,27 @@ class _LocationMapState extends ConsumerState<LocationMap>
}
return [
Positioned(top: 12, left: 12, child: const MapStyleSelector()),
Positioned(
top: 12,
left: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const MapStyleSelector(),
if (widget.selectedDevice?.capabilities?.location != null &&
widget.selectedDevice!.capabilities!.location!.options.isNotEmpty) ...[
const SizedBox(height: 8),
FrequencySelector(
currentFrequency:
widget.selectedDevice!.settings.frequency,
options:
widget.selectedDevice!.capabilities!.location!.options,
onChanged: _updateFrequency,
),
],
],
),
),
Positioned(
top: 12,
right: 12,
@@ -561,14 +595,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
right: 12,
child: MapActionsPanel(
actionsExpanded: mapState.actionsExpanded,
isFollowing: mapState.isFollowing,
hasPosition: widget.selectedPosition != null,
onToggleExpanded: _vm.toggleActionsExpanded,
onListTap: _showListSheet,
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
onShareTap: _shareLocation,
onFollowTap: _toggleFollowing,
onRefreshTap: widget.onRefreshPosition,
onCenterTap: _centerOnDevice,
),

View File

@@ -1,31 +1,28 @@
import 'package:flutter/material.dart';
import 'package:location/src/features/location/presentation/state/location_map_view_state.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:location/src/features/location/presentation/state/location_map_view_model.dart';
import 'map_action_button.dart';
class MapActionsPanel extends StatelessWidget {
final bool actionsExpanded;
final bool isFollowing;
final bool hasPosition;
final VoidCallback onToggleExpanded;
final VoidCallback onListTap;
final VoidCallback onAddGeofence;
final VoidCallback onAddFrequentPlace;
final VoidCallback onShareTap;
final VoidCallback onFollowTap;
final VoidCallback onRefreshTap;
final VoidCallback onCenterTap;
const MapActionsPanel({
super.key,
required this.actionsExpanded,
required this.isFollowing,
required this.hasPosition,
required this.onToggleExpanded,
required this.onListTap,
required this.onAddGeofence,
required this.onAddFrequentPlace,
required this.onShareTap,
required this.onFollowTap,
required this.onRefreshTap,
required this.onCenterTap,
});
@@ -53,12 +50,6 @@ class MapActionsPanel extends StatelessWidget {
const SizedBox(height: 8),
MapActionButton(icon: Icons.share, onTap: onShareTap),
const SizedBox(height: 8),
MapActionButton(
icon: isFollowing ? Icons.gps_fixed : Icons.gps_not_fixed,
onTap: onFollowTap,
isActive: isFollowing,
),
const SizedBox(height: 8),
],
)
: const SizedBox.shrink(),
@@ -77,3 +68,96 @@ class MapActionsPanel extends StatelessWidget {
);
}
}
class FrequencySelector extends ConsumerWidget {
final int currentFrequency;
final List<int> options;
final ValueChanged<int> onChanged;
const FrequencySelector({
super.key,
required this.currentFrequency,
required this.options,
required this.onChanged,
});
String _formatSeconds(int seconds) {
if (seconds >= 60) return '${seconds ~/ 60}m';
return '${seconds}s';
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final expanded = ref.watch(
locationMapViewModelProvider.select((s) => s.frequencyExpanded),
);
final vm = ref.read(locationMapViewModelProvider.notifier);
return AnimatedSize(
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: expanded
? _buildSegmented(context, vm)
: MapActionButton(
icon: Icons.timer_outlined,
onTap: vm.toggleFrequencyExpanded,
),
);
}
Widget _buildSegmented(BuildContext context, LocationMapViewModel vm) {
return Material(
elevation: 2,
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).colorScheme.surface,
child: Padding(
padding: const EdgeInsets.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
...options.map((opt) {
final selected = opt == currentFrequency;
return GestureDetector(
onTap: () {
onChanged(opt);
vm.collapseFrequency();
},
child: Container(
width: 40,
height: 32,
margin: const EdgeInsets.symmetric(vertical: 2),
decoration: BoxDecoration(
color: selected
? Theme.of(context).primaryColor
: Colors.transparent,
borderRadius: BorderRadius.circular(6),
),
child: Center(
child: Text(
_formatSeconds(opt),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: selected ? Theme.of(context).colorScheme.surface : Theme.of(context).colorScheme.onSurface,
),
),
),
),
);
}),
const SizedBox(height: 2),
GestureDetector(
onTap: vm.collapseFrequency,
child: const SizedBox(
width: 40,
height: 24,
child: Icon(Icons.close, size: 16, color: Colors.black54),
),
),
],
),
),
);
}
}

View File

@@ -450,7 +450,7 @@ packages:
source: hosted
version: "4.0.0"
get_it:
dependency: transitive
dependency: "direct main"
description:
name: get_it
sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66"

View File

@@ -24,6 +24,7 @@ dependencies:
path: ../../../../packages/sf_shared
utils:
path: ../../../../packages/utils
get_it: ^9.0.5
flutter_riverpod: ^3.0.3
go_router: ^17.0.0
freezed_annotation: ^3.1.0

View File

@@ -1,33 +1,22 @@
import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
import 'language_remote_datasource.dart';
class LanguageRemoteDatasourceImpl implements LanguageRemoteDatasource {
LanguageRemoteDatasourceImpl(this._repository);
LanguageRemoteDatasourceImpl(this._datasource);
final QuestiaRepository _repository;
final DeviceSettingsUpdateDatasource _datasource;
@override
Future<void> updateDeviceLanguage({
required DeviceEntity device,
required String newLanguage,
}) async {
final settings = Map<String, dynamic>.from(device.settings);
settings['language'] = newLanguage;
final csvBase64 = DeviceCsvBuilder.buildBase64Csv(
final updatedSettings = device.settings.copyWith(language: newLanguage);
await _datasource.updateDeviceSettings(
device: device,
settings: settings,
);
await safeCall(
() => _repository.put<dynamic>(
'/devices',
body: {'csv': csvBase64},
),
'Error updating device language',
updatedSettings: updatedSettings,
);
}
}

View File

@@ -1,10 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:settings/src/core/data/datasources/language_remote_datasource.dart';
import 'package:settings/src/core/data/datasources/language_remote_datasource_impl.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
final languageRemoteDatasourceProvider =
Provider<LanguageRemoteDatasource>((ref) {
final questiaRepository = getIt<QuestiaRepository>();
return LanguageRemoteDatasourceImpl(questiaRepository);
final datasource = ref.read(deviceSettingsUpdateProvider);
return LanguageRemoteDatasourceImpl(datasource);
});

View File

@@ -26,7 +26,7 @@ class LanguageViewModel extends Notifier<LanguageViewState> {
state = state.copyWith(
isLoading: false,
device: device,
language: device?.settings['language'] ?? 'es',
language: device?.settings.language ?? 'es',
);
} catch (e) {
if (!ref.mounted) return;
@@ -43,7 +43,7 @@ class LanguageViewModel extends Notifier<LanguageViewState> {
final device = state.device;
if (device == null) return;
if (state.language == device.settings['language']) {
if (state.language == device.settings.language) {
state = state.copyWith(isComplete: true);
return;
}

View File

@@ -4,19 +4,17 @@ import 'package:legacy_shared/legacy_shared.dart';
import 'sound_view_state.dart';
final soundViewModelProvider =
NotifierProvider.autoDispose<SoundViewModel, SoundViewState>(
NotifierProvider.autoDispose<SoundViewModel, SoundViewState>(
SoundViewModel.new,
);
class SoundViewModel extends Notifier<SoundViewState> {
late final CommandsRepository _commandsRepository;
late final DeviceSettingsUpdateDatasource _datasource;
@override
SoundViewState build() {
_commandsRepository = ref.read(commandsRepositoryProvider);
_datasource = ref.read(deviceSettingsUpdateProvider);
Future.microtask(() => load());
return const SoundViewState();
}
@@ -25,43 +23,47 @@ class SoundViewModel extends Notifier<SoundViewState> {
if (device == null) return;
state = state.copyWith(
deviceId: device.identificator,
soundOption: device.settings['soundMode'] ?? 'VIBRATION',
device: device,
soundOption: device.settings.soundMode,
isLoading: false,
);
}
void setSoundOption(String value) {
if (state.soundOption == value) return;
state = state.copyWith(
soundOption: value
);
state = state.copyWith(soundOption: value);
}
Future<void> submit() async {
final device = state.device;
if (device == null) return;
if (state.soundOption == device.settings.soundMode) {
state = state.copyWith(isComplete: true);
return;
}
try {
state = state.copyWith(
isLoading: true,
isComplete: false,
errorMessage: '',
);
final request = SendCommandRequestModel(
device: state.deviceId,
command: DeviceCommand.setSoundMode,
data: {'soundMode': state.soundOption}
final updatedSettings = device.settings.copyWith(
soundMode: state.soundOption,
);
await _commandsRepository.send(request: request);
state = state.copyWith(
isLoading: false,
isComplete: true,
await _datasource.updateDeviceSettings(
device: device,
updatedSettings: updatedSettings,
);
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, isComplete: true);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString()
);
if (!ref.mounted) return;
state = state.copyWith(isLoading: false, errorMessage: e.toString());
}
}
}

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:sf_shared/sf_shared.dart';
part 'sound_view_state.freezed.dart';
@freezed
abstract class SoundViewState with _$SoundViewState {
const factory SoundViewState({
@Default('') String deviceId,
DeviceEntity? device,
String? soundOption,
@Default(true) bool isLoading,
@Default(false) bool isComplete,

View File

@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$SoundViewState {
String get deviceId; String? get soundOption; bool get isLoading; bool get isComplete; String get errorMessage;
DeviceEntity? get device; String? get soundOption; bool get isLoading; bool get isComplete; String get errorMessage;
/// Create a copy of SoundViewState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $SoundViewStateCopyWith<SoundViewState> get copyWith => _$SoundViewStateCopyWith
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SoundViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.soundOption, soundOption) || other.soundOption == soundOption)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SoundViewState&&(identical(other.device, device) || other.device == device)&&(identical(other.soundOption, soundOption) || other.soundOption == soundOption)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,deviceId,soundOption,isLoading,isComplete,errorMessage);
int get hashCode => Object.hash(runtimeType,device,soundOption,isLoading,isComplete,errorMessage);
@override
String toString() {
return 'SoundViewState(deviceId: $deviceId, soundOption: $soundOption, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)';
return 'SoundViewState(device: $device, soundOption: $soundOption, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)';
}
@@ -45,11 +45,11 @@ abstract mixin class $SoundViewStateCopyWith<$Res> {
factory $SoundViewStateCopyWith(SoundViewState value, $Res Function(SoundViewState) _then) = _$SoundViewStateCopyWithImpl;
@useResult
$Res call({
String deviceId, String? soundOption, bool isLoading, bool isComplete, String errorMessage
DeviceEntity? device, String? soundOption, bool isLoading, bool isComplete, String errorMessage
});
$DeviceEntityCopyWith<$Res>? get device;
}
/// @nodoc
@@ -62,17 +62,29 @@ class _$SoundViewStateCopyWithImpl<$Res>
/// Create a copy of SoundViewState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? deviceId = null,Object? soundOption = freezed,Object? isLoading = null,Object? isComplete = null,Object? errorMessage = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? device = freezed,Object? soundOption = freezed,Object? isLoading = null,Object? isComplete = null,Object? errorMessage = null,}) {
return _then(_self.copyWith(
deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,soundOption: freezed == soundOption ? _self.soundOption : soundOption // ignore: cast_nullable_to_non_nullable
device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,soundOption: freezed == soundOption ? _self.soundOption : soundOption // ignore: cast_nullable_to_non_nullable
as String?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String,
));
}
/// Create a copy of SoundViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DeviceEntityCopyWith<$Res>? get device {
if (_self.device == null) {
return null;
}
return $DeviceEntityCopyWith<$Res>(_self.device!, (value) {
return _then(_self.copyWith(device: value));
});
}
}
@@ -154,10 +166,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String deviceId, String? soundOption, bool isLoading, bool isComplete, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( DeviceEntity? device, String? soundOption, bool isLoading, bool isComplete, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SoundViewState() when $default != null:
return $default(_that.deviceId,_that.soundOption,_that.isLoading,_that.isComplete,_that.errorMessage);case _:
return $default(_that.device,_that.soundOption,_that.isLoading,_that.isComplete,_that.errorMessage);case _:
return orElse();
}
@@ -175,10 +187,10 @@ return $default(_that.deviceId,_that.soundOption,_that.isLoading,_that.isComplet
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String deviceId, String? soundOption, bool isLoading, bool isComplete, String errorMessage) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( DeviceEntity? device, String? soundOption, bool isLoading, bool isComplete, String errorMessage) $default,) {final _that = this;
switch (_that) {
case _SoundViewState():
return $default(_that.deviceId,_that.soundOption,_that.isLoading,_that.isComplete,_that.errorMessage);case _:
return $default(_that.device,_that.soundOption,_that.isLoading,_that.isComplete,_that.errorMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -195,10 +207,10 @@ return $default(_that.deviceId,_that.soundOption,_that.isLoading,_that.isComplet
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String deviceId, String? soundOption, bool isLoading, bool isComplete, String errorMessage)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( DeviceEntity? device, String? soundOption, bool isLoading, bool isComplete, String errorMessage)? $default,) {final _that = this;
switch (_that) {
case _SoundViewState() when $default != null:
return $default(_that.deviceId,_that.soundOption,_that.isLoading,_that.isComplete,_that.errorMessage);case _:
return $default(_that.device,_that.soundOption,_that.isLoading,_that.isComplete,_that.errorMessage);case _:
return null;
}
@@ -210,10 +222,10 @@ return $default(_that.deviceId,_that.soundOption,_that.isLoading,_that.isComplet
class _SoundViewState implements SoundViewState {
const _SoundViewState({this.deviceId = '', this.soundOption, this.isLoading = true, this.isComplete = false, this.errorMessage = ''});
const _SoundViewState({this.device, this.soundOption, this.isLoading = true, this.isComplete = false, this.errorMessage = ''});
@override@JsonKey() final String deviceId;
@override final DeviceEntity? device;
@override final String? soundOption;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isComplete;
@@ -229,16 +241,16 @@ _$SoundViewStateCopyWith<_SoundViewState> get copyWith => __$SoundViewStateCopyW
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SoundViewState&&(identical(other.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.soundOption, soundOption) || other.soundOption == soundOption)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SoundViewState&&(identical(other.device, device) || other.device == device)&&(identical(other.soundOption, soundOption) || other.soundOption == soundOption)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.isComplete, isComplete) || other.isComplete == isComplete)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage));
}
@override
int get hashCode => Object.hash(runtimeType,deviceId,soundOption,isLoading,isComplete,errorMessage);
int get hashCode => Object.hash(runtimeType,device,soundOption,isLoading,isComplete,errorMessage);
@override
String toString() {
return 'SoundViewState(deviceId: $deviceId, soundOption: $soundOption, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)';
return 'SoundViewState(device: $device, soundOption: $soundOption, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)';
}
@@ -249,11 +261,11 @@ abstract mixin class _$SoundViewStateCopyWith<$Res> implements $SoundViewStateCo
factory _$SoundViewStateCopyWith(_SoundViewState value, $Res Function(_SoundViewState) _then) = __$SoundViewStateCopyWithImpl;
@override @useResult
$Res call({
String deviceId, String? soundOption, bool isLoading, bool isComplete, String errorMessage
DeviceEntity? device, String? soundOption, bool isLoading, bool isComplete, String errorMessage
});
@override $DeviceEntityCopyWith<$Res>? get device;
}
/// @nodoc
@@ -266,10 +278,10 @@ class __$SoundViewStateCopyWithImpl<$Res>
/// Create a copy of SoundViewState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? deviceId = null,Object? soundOption = freezed,Object? isLoading = null,Object? isComplete = null,Object? errorMessage = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? device = freezed,Object? soundOption = freezed,Object? isLoading = null,Object? isComplete = null,Object? errorMessage = null,}) {
return _then(_SoundViewState(
deviceId: null == deviceId ? _self.deviceId : deviceId // ignore: cast_nullable_to_non_nullable
as String,soundOption: freezed == soundOption ? _self.soundOption : soundOption // ignore: cast_nullable_to_non_nullable
device: freezed == device ? _self.device : device // ignore: cast_nullable_to_non_nullable
as DeviceEntity?,soundOption: freezed == soundOption ? _self.soundOption : soundOption // ignore: cast_nullable_to_non_nullable
as String?,isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,isComplete: null == isComplete ? _self.isComplete : isComplete // ignore: cast_nullable_to_non_nullable
as bool,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
@@ -277,7 +289,19 @@ as String,
));
}
/// Create a copy of SoundViewState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$DeviceEntityCopyWith<$Res>? get device {
if (_self.device == null) {
return null;
}
return $DeviceEntityCopyWith<$Res>(_self.device!, (value) {
return _then(_self.copyWith(device: value));
});
}
}
// dart format on

View File

@@ -15,4 +15,6 @@ export 'src/utils/device_csv_builder.dart';
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/devices_repository_provider.dart';
export 'src/data/datasources/device_settings_update_datasource.dart';
export 'src/providers/device_settings_update_provider.dart';

View File

@@ -11,28 +11,14 @@ class CommandsRemoteDatasourceImpl implements CommandsRemoteDatasource {
@override
Future<void> send({
required SendCommandRequestModel request
required SendCommandRequestModel request,
}) async {
try{
final response = await safeCall(
() => _repository.post<Map<String, dynamic>>(
'/commands',
body: request.toJson(),
),
'Error in command ${request.command}',
);
final data = response.data;
if (data == null || data.isEmpty) {
throw Exception('Empty response from command ${request.command}');
}
if (response.statusCode == 500) {
throw Exception('Server error from command ${request.command}');
}
// return CommandsResponseModel.fromJson(data);
} catch(e) {
return;
}
await safeCall(
() => _repository.post<Map<String, dynamic>>(
'/commands',
body: request.toJson(),
),
'Error in command ${request.command}',
);
}
}

View File

@@ -1,15 +1,17 @@
import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import 'package:sf_shared/sf_shared.dart';
class DeviceUpdateDatasource {
DeviceUpdateDatasource(this._repository);
import '../../utils/device_csv_builder.dart';
import '../../utils/dio_error_mapper.dart';
class DeviceSettingsUpdateDatasource {
DeviceSettingsUpdateDatasource(this._repository);
final QuestiaRepository _repository;
Future<void> updateDeviceSettings({
required DeviceEntity device,
required Map<String, dynamic> updatedSettings,
required DeviceSettingsEntity updatedSettings,
}) async {
final csvBase64 = DeviceCsvBuilder.buildBase64Csv(
device: device,

View File

@@ -75,11 +75,13 @@ extension GetDevicesResponseModelMapper on GetDevicesResponseModel {
phone: item.phone,
simId: item.simId,
paymentOptions: item.paymentOptions,
settings: item.settings,
settings: DeviceSettingsModel.fromJson(item.settings).toEntity(),
connectionServer: item.connectionServer,
protocol: item.protocol,
type: item.type,
capabilities: item.capabilities,
capabilities: item.capabilities != null
? DeviceCapabilitiesModel.fromJson(item.capabilities!).toEntity()
: null,
createdAt: item.createdAt.toString(),
updatedAt: item.updatedAt?.toString(),
),

View File

@@ -0,0 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:get_it/get_it.dart';
import 'package:sf_infrastructure/sf_infrastructure.dart';
import '../data/datasources/device_settings_update_datasource.dart';
final deviceSettingsUpdateProvider =
Provider<DeviceSettingsUpdateDatasource>((ref) {
return DeviceSettingsUpdateDatasource(GetIt.I<QuestiaRepository>());
});

View File

@@ -7,7 +7,7 @@ class DeviceCsvBuilder {
static String buildBase64Csv({
required DeviceEntity device,
required Map<String, dynamic> settings,
required DeviceSettingsEntity settings,
}) {
final csvHeader =
'id,carrierName,flags,settings,battery,carrierBirthday,'
@@ -18,7 +18,7 @@ class DeviceCsvBuilder {
device.id,
device.carrierName ?? '',
_csvEscape(device.flags),
_csvEscape(settings),
_csvEscape(settings.toMap()),
device.battery ?? '',
device.carrierBirthday ?? '',
device.carrierWeight ?? '',