refactor(device_management): migrate health to Riverpod + improve charts

This commit is contained in:
2026-04-22 23:04:14 +02:00
parent 0f2d9ba601
commit 81c3eaec70
13 changed files with 553 additions and 309 deletions

View File

@@ -1,23 +1,22 @@
import 'package:design_system/design_system.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:device_management/src/core/presentation/widgets/time_range_selector.dart';
import 'package:device_management/src/features/health/presentation/providers/health_controller.dart';
import 'package:device_management/src/features/health/presentation/providers/health_state.dart';
import 'package:device_management/src/features/health/presentation/widgets/blood_pressure_tab.dart';
import 'package:device_management/src/features/health/presentation/widgets/health_summary_cards.dart';
import 'package:device_management/src/features/health/presentation/widgets/heart_rate_tab.dart';
import 'package:device_management/src/features/health/presentation/widgets/oxygen_tab.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:legacy_theme/legacy_theme.dart';
import 'package:legacy_ui/legacy_ui.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:lottie/lottie.dart';
import 'package:navigation/navigation.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:sf_shared/sf_shared.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';
import 'widgets/oxygen_tab.dart';
import '../../../core/presentation/widgets/time_range_selector.dart';
class HealthScreen extends ConsumerStatefulWidget {
final NavigationContract navigationContract;
@@ -43,7 +42,7 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
super.dispose();
}
Future<void> _pickCustomRange(HealthViewModel vm) async {
Future<void> _pickCustomRange(HealthController notifier) async {
final now = DateTime.now();
final picked = await showDateRangePicker(
context: context,
@@ -51,125 +50,120 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
lastDate: now,
);
if (picked != null) {
vm.selectCustomRange(picked.start, picked.end);
notifier.selectCustomRange(picked.start, picked.end);
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(healthViewModelProvider);
final vm = ref.read(healthViewModelProvider.notifier);
final state = ref.watch(healthControllerProvider);
final notifier = ref.read(healthControllerProvider.notifier);
final device = ref.watch(selectedDeviceProvider).value;
ref.listen(healthViewModelProvider.select((s) => s.errorEvent), (
previous,
next,
) {
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(
ref.listen(
healthControllerProvider.select((s) => s.errorEvent),
(_, next) async {
if (next == null) return;
final key = switch (next) {
HealthErrorEvent.loadData ||
HealthErrorEvent.loadMore =>
I18n.errorHealthData,
HealthErrorEvent.measure => I18n.errorHealthMeasure,
HealthErrorEvent.heartRateFrequency =>
I18n.errorHeartRateFrequency,
),
};
showTopSnackbar(context, message: message, type: MessageType.error);
}
});
await showErrorDialog(context, key);
notifier.clearError();
},
);
return LegacyPageLayout(
title: context.translate(I18n.healthTitle),
body: state.isLoading
? const Center(child: CircularProgressIndicator())
: state.isMeasuringCountdown
? _MeasuringOverlay(
remainingSeconds: state.measureRemainingSeconds,
)
: Column(
children: [
HealthSummaryCards(
heartbeats: state.latestHeartbeats,
oxygens: state.latestOxygens,
tabController: _tabController,
),
TimeRangeSelector(
selected: state.timeRange,
onSelected: (range) => vm.selectTimeRange(range),
onCustomTap: () => _pickCustomRange(vm),
),
if (device?.capabilities?.heartbeats != null &&
device!.capabilities!.heartbeats!.options.isNotEmpty)
_HeartRateFrequencySelector(
currentFrequency: device.settings.frequencyHeartRate,
options: device.capabilities!.heartbeats!.options,
onChanged: (frequency) async {
if (!await guardDeviceCommand(context, ref)) return;
final success = await vm.updateHeartRateFrequency(
frequency: frequency,
);
if (!context.mounted) return;
if (success) {
showTopSnackbar(
context,
message: context.translate(
I18n.locationFrequencyUpdated,
args: {'minutes': '${frequency ~/ 60}'},
? _MeasuringOverlay(
remainingSeconds: state.measureRemainingSeconds,
)
: Column(
children: [
HealthSummaryCards(
heartbeats: state.latestHeartbeats,
oxygens: state.latestOxygens,
tabController: _tabController,
),
TimeRangeSelector(
selected: state.timeRange,
onSelected: notifier.selectTimeRange,
onCustomTap: () => _pickCustomRange(notifier),
),
if (device?.capabilities?.heartbeats != null &&
device!.capabilities!.heartbeats!.options.isNotEmpty)
_HeartRateFrequencySelector(
currentFrequency: device.settings.frequencyHeartRate,
options: device.capabilities!.heartbeats!.options,
onChanged: (frequency) async {
if (!await guardDeviceCommand(context, ref)) return;
final success = await notifier
.updateHeartRateFrequency(frequency: frequency);
if (!context.mounted) return;
if (success) {
await showSuccessDialog(
context,
I18n.locationFrequencyUpdated,
args: {'minutes': '${frequency ~/ 60}'},
);
}
},
),
TabBar(
controller: _tabController,
labelColor: context.sfColors.legacyPrimary,
unselectedLabelColor: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.5),
indicatorColor: context.sfColors.legacyPrimary,
labelStyle: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
fontWeight: FontWeight.w600,
),
tabs: [
Tab(text: context.translate(I18n.heartRate)),
Tab(text: context.translate(I18n.bloodPressure)),
Tab(text: context.translate(I18n.oxygenLevel)),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
HeartRateTab(
chartData: state.chartHeartbeats,
historyData: state.historyHeartbeats,
stats: state.heartRateStats,
hasMore: state.hasMoreHistory,
isLoadingMore: state.isLoadingMore,
),
type: MessageType.success,
);
}
},
),
TabBar(
controller: _tabController,
labelColor: context.sfColors.legacyPrimary,
unselectedLabelColor: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.5),
indicatorColor: context.sfColors.legacyPrimary,
labelStyle: TextStyle(
fontSize: SizeUtils.getByScreen(small: 13, big: 12),
fontWeight: FontWeight.w600,
),
tabs: [
Tab(text: context.translate(I18n.heartRate)),
Tab(text: context.translate(I18n.bloodPressure)),
Tab(text: context.translate(I18n.oxygenLevel)),
BloodPressureTab(
chartData: state.chartHeartbeats,
historyData: state.historyHeartbeats,
hasMore: state.hasMoreHistory,
isLoadingMore: state.isLoadingMore,
),
OxygenTab(
chartData: state.chartOxygens,
historyData: state.historyOxygens,
stats: state.oxygenStats,
hasMore: state.hasMoreHistory,
isLoadingMore: state.isLoadingMore,
),
],
),
),
],
),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
HeartRateTab(
chartData: state.chartHeartbeats,
historyData: state.historyHeartbeats,
stats: state.heartRateStats,
hasMore: state.hasMoreHistory,
isLoadingMore: state.isLoadingMore,
),
BloodPressureTab(
chartData: state.chartHeartbeats,
historyData: state.historyHeartbeats,
hasMore: state.hasMoreHistory,
isLoadingMore: state.isLoadingMore,
),
OxygenTab(
chartData: state.chartOxygens,
historyData: state.historyOxygens,
stats: state.oxygenStats,
hasMore: state.hasMoreHistory,
isLoadingMore: state.isLoadingMore,
),
],
),
),
],
),
footer: _SaveSection(),
footer: const _SaveSection(),
);
}
}
@@ -179,24 +173,22 @@ class _SaveSection extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(healthViewModelProvider.notifier);
final isMeasuring = ref.watch(
healthViewModelProvider.select((s) => s.isMeasuring),
);
final isCountdown = ref.watch(
healthViewModelProvider.select((s) => s.isMeasuringCountdown),
);
final notifier = ref.read(healthControllerProvider.notifier);
final isMeasuring =
ref.watch(healthControllerProvider.select((s) => s.isMeasuring));
final isCountdown = ref
.watch(healthControllerProvider.select((s) => s.isMeasuringCountdown));
if (isCountdown) return const SizedBox.shrink();
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child: PrimaryButton(
onPressed: isMeasuring
? null
: () async {
if (!await guardDeviceCommand(context, ref)) return;
vm.measure();
notifier.measure();
},
text: isMeasuring ? '...' : context.translate(I18n.measure),
color: context.sfColors.legacyPrimary,

View File

@@ -1,59 +1,42 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:device_management/src/core/data/datasources/health_query_builder.dart';
import 'package:device_management/src/core/presentation/time_range.dart';
import 'package:device_management/src/core/providers/health_repository_provider.dart';
import 'package:device_management/src/features/health/presentation/providers/health_state.dart';
import 'package:device_management/src/features/health/presentation/providers/measure_end_time_provider.dart';
import 'package:legacy_device_state/legacy_device_state.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sf_shared/sf_shared.dart';
import 'package:sf_tracking/sf_tracking.dart';
import '../../../../core/data/datasources/health_query_builder.dart';
import '../../../../core/presentation/time_range.dart';
import '../../../../core/domain/repositories/health_repository.dart';
import '../../../../core/providers/health_repository_provider.dart';
import 'health_view_state.dart';
part 'health_controller.g.dart';
final _measureEndTimeProvider =
NotifierProvider<_MeasureEndTimeNotifier, DateTime?>(
_MeasureEndTimeNotifier.new,
);
const int _historyPageSize = 20;
const int _measureDurationSeconds = 60;
class _MeasureEndTimeNotifier extends Notifier<DateTime?> {
@override
DateTime? build() => null;
void set(DateTime? value) => state = value;
}
final healthViewModelProvider =
NotifierProvider.autoDispose<HealthViewModel, HealthViewState>(
HealthViewModel.new,
);
class HealthViewModel extends Notifier<HealthViewState> {
late final HealthRepository _repository;
late final CommandsRepository _commandsRepository;
late final SfTrackingRepository _tracking;
@riverpod
class HealthController extends _$HealthController {
Timer? _measureTimer;
static const int _historyPageSize = 20;
static const int _measureDurationSeconds = 60;
@override
HealthViewState build() {
_repository = ref.read(healthRepositoryProvider);
_commandsRepository = ref.read(commandsRepositoryProvider);
_tracking = ref.read(sfTrackingProvider);
HealthState build() {
ref.onDispose(() => _measureTimer?.cancel());
_init();
_resumeMeasureIfNeeded();
return const HealthViewState();
return const HealthState();
}
String? get _identificator =>
ref.read(selectedDeviceProvider).value?.identificator;
void _resumeMeasureIfNeeded() {
final endTime = ref.read(_measureEndTimeProvider);
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);
ref.read(measureEndTimeProvider.notifier).set(null);
return;
}
@@ -66,13 +49,13 @@ class HealthViewModel extends Notifier<HealthViewState> {
});
}
String? get _identificator => ref.read(selectedDeviceProvider).value?.identificator;
Future<void> selectTimeRange(TimeRange range) async {
if (range == state.timeRange) return;
state = state.copyWith(timeRange: range, isLoading: true, errorEvent: null);
unawaited(
_tracking.legacyDeviceHealthTimeRangeChanged(_timeRangeName(range)),
ref
.read(sfTrackingProvider)
.legacyDeviceHealthTimeRangeChanged(_timeRangeName(range)),
);
await _loadFilteredData();
}
@@ -85,22 +68,20 @@ class HealthViewModel extends Notifier<HealthViewState> {
isLoading: true,
errorEvent: null,
);
unawaited(_tracking.legacyDeviceHealthTimeRangeChanged('custom'));
unawaited(
ref
.read(sfTrackingProvider)
.legacyDeviceHealthTimeRangeChanged('custom'),
);
await _loadFilteredData();
}
String _timeRangeName(TimeRange range) {
switch (range) {
case TimeRange.today:
return 'today';
case TimeRange.sevenDays:
return 'seven_days';
case TimeRange.thirtyDays:
return 'thirty_days';
case TimeRange.custom:
return 'custom';
}
}
String _timeRangeName(TimeRange range) => switch (range) {
TimeRange.today => 'today',
TimeRange.sevenDays => 'seven_days',
TimeRange.thirtyDays => 'thirty_days',
TimeRange.custom => 'custom',
};
Future<void> loadMoreHistory() async {
if (state.isLoadingMore || !state.hasMoreHistory) return;
@@ -108,13 +89,14 @@ class HealthViewModel extends Notifier<HealthViewState> {
if (identificator == null) return;
state = state.copyWith(isLoadingMore: true);
final repo = ref.read(healthRepositoryProvider);
try {
final nextPage = state.currentHistoryPage + 1;
final filters = _buildTimeFilters();
final (heartbeats, oxygens) = await (
_repository.getHeartbeats(
repo.getHeartbeats(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
@@ -123,7 +105,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
filters: filters,
),
),
_repository.getOxygens(
repo.getOxygens(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
@@ -139,12 +121,11 @@ class HealthViewModel extends Notifier<HealthViewState> {
historyHeartbeats: [...state.historyHeartbeats, ...heartbeats],
historyOxygens: [...state.historyOxygens, ...oxygens],
currentHistoryPage: nextPage,
hasMoreHistory:
heartbeats.length >= _historyPageSize ||
hasMoreHistory: heartbeats.length >= _historyPageSize ||
oxygens.length >= _historyPageSize,
isLoadingMore: false,
);
} catch (e) {
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoadingMore: false,
@@ -159,17 +140,18 @@ class HealthViewModel extends Notifier<HealthViewState> {
state = state.copyWith(isLoading: false);
return;
}
final repo = ref.read(healthRepositoryProvider);
try {
final (latestHeartbeats, latestOxygens) = await (
_repository.getHeartbeats(
repo.getHeartbeats(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
pageSize: 5,
),
),
_repository.getOxygens(
repo.getOxygens(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
@@ -185,7 +167,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
);
await _loadFilteredData();
} catch (e) {
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
@@ -200,6 +182,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
state = state.copyWith(isLoading: false);
return;
}
final repo = ref.read(healthRepositoryProvider);
try {
final filters = _buildTimeFilters();
@@ -210,7 +193,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
histHeartbeats,
histOxygens,
) = await (
_repository.getHeartbeats(
repo.getHeartbeats(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.asc,
@@ -219,7 +202,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
filters: filters,
),
),
_repository.getOxygens(
repo.getOxygens(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.asc,
@@ -228,7 +211,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
filters: filters,
),
),
_repository.getHeartbeats(
repo.getHeartbeats(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
@@ -237,7 +220,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
filters: filters,
),
),
_repository.getOxygens(
repo.getOxygens(
identificator: identificator,
queryParameters: HealthQueryBuilder.build(
orderDirection: OrderDirection.desc,
@@ -255,17 +238,17 @@ class HealthViewModel extends Notifier<HealthViewState> {
historyHeartbeats: histHeartbeats,
historyOxygens: histOxygens,
currentHistoryPage: 1,
hasMoreHistory:
histHeartbeats.length >= _historyPageSize ||
hasMoreHistory: histHeartbeats.length >= _historyPageSize ||
histOxygens.length >= _historyPageSize,
heartRateStats: _computeStats(
chartHeartbeats.map((e) => e.heartbeats).toList(),
),
oxygenStats: _computeStats(chartOxygens.map((e) => e.oxygen).toList()),
oxygenStats:
_computeStats(chartOxygens.map((e) => e.oxygen).toList()),
isLoading: false,
errorEvent: null,
);
} catch (e) {
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
@@ -277,7 +260,6 @@ class HealthViewModel extends Notifier<HealthViewState> {
List<HealthFilter>? _buildTimeFilters() {
final range = _getTimeRange();
if (range == null) return null;
final (start, end) = range;
return HealthQueryBuilder.timeRangeFilters(start: start, end: end);
}
@@ -328,11 +310,13 @@ class HealthViewModel extends Notifier<HealthViewState> {
ref.syncDeviceSettings(device, updatedSettings);
unawaited(
_tracking.legacyDeviceHealthHeartRateFrequencyChanged(frequency),
ref
.read(sfTrackingProvider)
.legacyDeviceHealthHeartRateFrequencyChanged(frequency),
);
return true;
} catch (e) {
} catch (_) {
if (!ref.mounted) return false;
state = state.copyWith(errorEvent: HealthErrorEvent.heartRateFrequency);
return false;
@@ -346,21 +330,21 @@ class HealthViewModel extends Notifier<HealthViewState> {
try {
state = state.copyWith(isMeasuring: true, errorEvent: null);
final request = SendCommandRequestModel(
device: device.identificator,
command: DeviceCommand.requestHeartRate,
);
await _commandsRepository.send(request: request);
await ref.read(commandsRepositoryProvider).send(
request: SendCommandRequestModel(
device: device.identificator,
command: DeviceCommand.requestHeartRate,
),
);
if (!ref.mounted) return;
unawaited(_tracking.legacyDeviceHealthMeasurementStarted());
unawaited(
ref.read(sfTrackingProvider).legacyDeviceHealthMeasurementStarted(),
);
ref
.read(_measureEndTimeProvider.notifier)
.set(
DateTime.now().add(
const Duration(seconds: _measureDurationSeconds),
),
ref.read(measureEndTimeProvider.notifier).set(
DateTime.now()
.add(const Duration(seconds: _measureDurationSeconds)),
);
state = state.copyWith(
@@ -370,7 +354,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
);
_startCountdownTimer();
} catch (e) {
} catch (_) {
if (!ref.mounted) return;
state = state.copyWith(
isMeasuring: false,
@@ -390,7 +374,7 @@ class HealthViewModel extends Notifier<HealthViewState> {
if (remaining <= 0) {
timer.cancel();
_measureTimer = null;
ref.read(_measureEndTimeProvider.notifier).set(null);
ref.read(measureEndTimeProvider.notifier).set(null);
state = state.copyWith(
isMeasuringCountdown: false,
measureRemainingSeconds: 0,
@@ -401,4 +385,9 @@ class HealthViewModel extends Notifier<HealthViewState> {
}
});
}
void clearError() {
if (state.errorEvent == null) return;
state = state.copyWith(errorEvent: null);
}
}

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'health_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(HealthController)
const healthControllerProvider = HealthControllerProvider._();
final class HealthControllerProvider
extends $NotifierProvider<HealthController, HealthState> {
const HealthControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'healthControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$healthControllerHash();
@$internal
@override
HealthController create() => HealthController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(HealthState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<HealthState>(value),
);
}
}
String _$healthControllerHash() => r'601f8ead6d6b580f0fd731c967566a31a97ab603';
abstract class _$HealthController extends $Notifier<HealthState> {
HealthState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<HealthState, HealthState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<HealthState, HealthState>,
HealthState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,10 +1,9 @@
import 'package:device_management/src/core/presentation/time_range.dart';
import 'package:device_management/src/features/health/domain/entities/heartbeat_entity.dart';
import 'package:device_management/src/features/health/domain/entities/oxygen_entity.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../../core/presentation/time_range.dart';
import '../../domain/entities/heartbeat_entity.dart';
import '../../domain/entities/oxygen_entity.dart';
part 'health_view_state.freezed.dart';
part 'health_state.freezed.dart';
enum HealthErrorEvent { loadData, loadMore, measure, heartRateFrequency }
@@ -18,8 +17,8 @@ abstract class HealthStats with _$HealthStats {
}
@freezed
abstract class HealthViewState with _$HealthViewState {
const factory HealthViewState({
abstract class HealthState with _$HealthState {
const factory HealthState({
@Default([]) List<HeartbeatEntity> latestHeartbeats,
@Default([]) List<OxygenEntity> latestOxygens,
@Default([]) List<HeartbeatEntity> chartHeartbeats,
@@ -31,13 +30,13 @@ abstract class HealthViewState with _$HealthViewState {
@Default(HealthStats()) HealthStats heartRateStats,
@Default(HealthStats()) HealthStats oxygenStats,
@Default(TimeRange.today) TimeRange timeRange,
@Default(null) DateTime? customStart,
@Default(null) DateTime? customEnd,
DateTime? customStart,
DateTime? customEnd,
@Default(true) bool isLoading,
@Default(false) bool isLoadingMore,
@Default(false) bool isMeasuring,
@Default(false) bool isMeasuringCountdown,
@Default(0) int measureRemainingSeconds,
HealthErrorEvent? errorEvent,
}) = _HealthViewState;
}) = _HealthState;
}

View File

@@ -3,7 +3,7 @@
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'health_view_state.dart';
part of 'health_state.dart';
// **************************************************************************
// FreezedGenerator
@@ -275,20 +275,20 @@ as int,
}
/// @nodoc
mixin _$HealthViewState {
mixin _$HealthState {
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
/// Create a copy of HealthState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$HealthViewStateCopyWith<HealthViewState> get copyWith => _$HealthViewStateCopyWithImpl<HealthViewState>(this as HealthViewState, _$identity);
$HealthStateCopyWith<HealthState> get copyWith => _$HealthStateCopyWithImpl<HealthState>(this as HealthState, _$identity);
@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.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));
return identical(this, other) || (other.runtimeType == runtimeType&&other is HealthState&&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));
}
@@ -297,15 +297,15 @@ int get hashCode => Object.hashAll([runtimeType,const DeepCollectionEquality().h
@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, isMeasuring: $isMeasuring, isMeasuringCountdown: $isMeasuringCountdown, measureRemainingSeconds: $measureRemainingSeconds, errorEvent: $errorEvent)';
return 'HealthState(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)';
}
}
/// @nodoc
abstract mixin class $HealthViewStateCopyWith<$Res> {
factory $HealthViewStateCopyWith(HealthViewState value, $Res Function(HealthViewState) _then) = _$HealthViewStateCopyWithImpl;
abstract mixin class $HealthStateCopyWith<$Res> {
factory $HealthStateCopyWith(HealthState value, $Res Function(HealthState) _then) = _$HealthStateCopyWithImpl;
@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, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent
@@ -316,14 +316,14 @@ $HealthStatsCopyWith<$Res> get heartRateStats;$HealthStatsCopyWith<$Res> get oxy
}
/// @nodoc
class _$HealthViewStateCopyWithImpl<$Res>
implements $HealthViewStateCopyWith<$Res> {
_$HealthViewStateCopyWithImpl(this._self, this._then);
class _$HealthStateCopyWithImpl<$Res>
implements $HealthStateCopyWith<$Res> {
_$HealthStateCopyWithImpl(this._self, this._then);
final HealthViewState _self;
final $Res Function(HealthViewState) _then;
final HealthState _self;
final $Res Function(HealthState) _then;
/// Create a copy of HealthViewState
/// Create a copy of HealthState
/// 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? isMeasuring = null,Object? isMeasuringCountdown = null,Object? measureRemainingSeconds = null,Object? errorEvent = freezed,}) {
return _then(_self.copyWith(
@@ -349,7 +349,7 @@ as int,errorEvent: freezed == errorEvent ? _self.errorEvent : errorEvent // igno
as HealthErrorEvent?,
));
}
/// Create a copy of HealthViewState
/// Create a copy of HealthState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@@ -358,7 +358,7 @@ $HealthStatsCopyWith<$Res> get heartRateStats {
return $HealthStatsCopyWith<$Res>(_self.heartRateStats, (value) {
return _then(_self.copyWith(heartRateStats: value));
});
}/// Create a copy of HealthViewState
}/// Create a copy of HealthState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@@ -371,8 +371,8 @@ $HealthStatsCopyWith<$Res> get oxygenStats {
}
/// Adds pattern-matching-related methods to [HealthViewState].
extension HealthViewStatePatterns on HealthViewState {
/// Adds pattern-matching-related methods to [HealthState].
extension HealthStatePatterns on HealthState {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
@@ -385,10 +385,10 @@ extension HealthViewStatePatterns on HealthViewState {
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _HealthViewState value)? $default,{required TResult orElse(),}){
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _HealthState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _HealthViewState() when $default != null:
case _HealthState() when $default != null:
return $default(_that);case _:
return orElse();
@@ -407,10 +407,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _HealthViewState value) $default,){
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _HealthState value) $default,){
final _that = this;
switch (_that) {
case _HealthViewState():
case _HealthState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
@@ -428,10 +428,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _HealthViewState value)? $default,){
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _HealthState value)? $default,){
final _that = this;
switch (_that) {
case _HealthViewState() when $default != null:
case _HealthState() when $default != null:
return $default(_that);case _:
return null;
@@ -451,7 +451,7 @@ 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, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _HealthViewState() when $default != null:
case _HealthState() 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.isMeasuring,_that.isMeasuringCountdown,_that.measureRemainingSeconds,_that.errorEvent);case _:
return orElse();
@@ -472,7 +472,7 @@ 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, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent) $default,) {final _that = this;
switch (_that) {
case _HealthViewState():
case _HealthState():
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');
@@ -492,7 +492,7 @@ 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, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent)? $default,) {final _that = this;
switch (_that) {
case _HealthViewState() when $default != null:
case _HealthState() 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.isMeasuring,_that.isMeasuringCountdown,_that.measureRemainingSeconds,_that.errorEvent);case _:
return null;
@@ -504,8 +504,8 @@ return $default(_that.latestHeartbeats,_that.latestOxygens,_that.chartHeartbeats
/// @nodoc
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.isMeasuring = false, this.isMeasuringCountdown = false, this.measureRemainingSeconds = 0, this.errorEvent}): _latestHeartbeats = latestHeartbeats,_latestOxygens = latestOxygens,_chartHeartbeats = chartHeartbeats,_chartOxygens = chartOxygens,_historyHeartbeats = historyHeartbeats,_historyOxygens = historyOxygens;
class _HealthState implements HealthState {
const _HealthState({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, this.customEnd, 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;
@@ -555,8 +555,8 @@ class _HealthViewState implements HealthViewState {
@override@JsonKey() final HealthStats heartRateStats;
@override@JsonKey() final HealthStats oxygenStats;
@override@JsonKey() final TimeRange timeRange;
@override@JsonKey() final DateTime? customStart;
@override@JsonKey() final DateTime? customEnd;
@override final DateTime? customStart;
@override final DateTime? customEnd;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isLoadingMore;
@override@JsonKey() final bool isMeasuring;
@@ -564,17 +564,17 @@ class _HealthViewState implements HealthViewState {
@override@JsonKey() final int measureRemainingSeconds;
@override final HealthErrorEvent? errorEvent;
/// Create a copy of HealthViewState
/// Create a copy of HealthState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$HealthViewStateCopyWith<_HealthViewState> get copyWith => __$HealthViewStateCopyWithImpl<_HealthViewState>(this, _$identity);
_$HealthStateCopyWith<_HealthState> get copyWith => __$HealthStateCopyWithImpl<_HealthState>(this, _$identity);
@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.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));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _HealthState&&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));
}
@@ -583,15 +583,15 @@ int get hashCode => Object.hashAll([runtimeType,const DeepCollectionEquality().h
@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, isMeasuring: $isMeasuring, isMeasuringCountdown: $isMeasuringCountdown, measureRemainingSeconds: $measureRemainingSeconds, errorEvent: $errorEvent)';
return 'HealthState(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)';
}
}
/// @nodoc
abstract mixin class _$HealthViewStateCopyWith<$Res> implements $HealthViewStateCopyWith<$Res> {
factory _$HealthViewStateCopyWith(_HealthViewState value, $Res Function(_HealthViewState) _then) = __$HealthViewStateCopyWithImpl;
abstract mixin class _$HealthStateCopyWith<$Res> implements $HealthStateCopyWith<$Res> {
factory _$HealthStateCopyWith(_HealthState value, $Res Function(_HealthState) _then) = __$HealthStateCopyWithImpl;
@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, bool isMeasuring, bool isMeasuringCountdown, int measureRemainingSeconds, HealthErrorEvent? errorEvent
@@ -602,17 +602,17 @@ $Res call({
}
/// @nodoc
class __$HealthViewStateCopyWithImpl<$Res>
implements _$HealthViewStateCopyWith<$Res> {
__$HealthViewStateCopyWithImpl(this._self, this._then);
class __$HealthStateCopyWithImpl<$Res>
implements _$HealthStateCopyWith<$Res> {
__$HealthStateCopyWithImpl(this._self, this._then);
final _HealthViewState _self;
final $Res Function(_HealthViewState) _then;
final _HealthState _self;
final $Res Function(_HealthState) _then;
/// Create a copy of HealthViewState
/// Create a copy of HealthState
/// 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? isMeasuring = null,Object? isMeasuringCountdown = null,Object? measureRemainingSeconds = null,Object? errorEvent = freezed,}) {
return _then(_HealthViewState(
return _then(_HealthState(
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
as List<OxygenEntity>,chartHeartbeats: null == chartHeartbeats ? _self._chartHeartbeats : chartHeartbeats // ignore: cast_nullable_to_non_nullable
@@ -636,7 +636,7 @@ as HealthErrorEvent?,
));
}
/// Create a copy of HealthViewState
/// Create a copy of HealthState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
@@ -645,7 +645,7 @@ $HealthStatsCopyWith<$Res> get heartRateStats {
return $HealthStatsCopyWith<$Res>(_self.heartRateStats, (value) {
return _then(_self.copyWith(heartRateStats: value));
});
}/// Create a copy of HealthViewState
}/// Create a copy of HealthState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')

View File

@@ -0,0 +1,11 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'measure_end_time_provider.g.dart';
@Riverpod(keepAlive: true)
class MeasureEndTime extends _$MeasureEndTime {
@override
DateTime? build() => null;
void set(DateTime? value) => state = value;
}

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'measure_end_time_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(MeasureEndTime)
const measureEndTimeProvider = MeasureEndTimeProvider._();
final class MeasureEndTimeProvider
extends $NotifierProvider<MeasureEndTime, DateTime?> {
const MeasureEndTimeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'measureEndTimeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$measureEndTimeHash();
@$internal
@override
MeasureEndTime create() => MeasureEndTime();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(DateTime? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<DateTime?>(value),
);
}
}
String _$measureEndTimeHash() => r'7226ccb70bfa78afe3a8285fb5fbdc17809e3a77';
abstract class _$MeasureEndTime extends $Notifier<DateTime?> {
DateTime? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<DateTime?, DateTime?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<DateTime?, DateTime?>,
DateTime?,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -5,7 +5,7 @@ import 'package:utils/utils.dart';
import '../../domain/entities/heartbeat_entity.dart';
import '../health_colors.dart';
import '../state/health_view_model.dart';
import '../providers/health_controller.dart';
import 'health_chart_helpers.dart';
import 'health_empty_state.dart';
import 'health_history_section.dart';
@@ -27,7 +27,9 @@ class BloodPressureTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(healthViewModelProvider.notifier);
final vm = ref.read(healthControllerProvider.notifier);
final timeRange =
ref.watch(healthControllerProvider.select((s) => s.timeRange));
final chartWithPressure = chartData
.where((h) => h.highBloodPressure != null && h.lowBloodPressure != null)
@@ -46,16 +48,19 @@ class BloodPressureTab extends ConsumerWidget {
),
children: [
HealthLineChart(
spots: toFlSpots(
points: toPoints(
chartWithPressure,
(h) => h.occurredAt,
(h) => h.highBloodPressure!.toDouble(),
),
lineColor: Theme.of(context).colorScheme.error,
secondarySpots: toFlSpots(
secondaryPoints: toPoints(
chartWithPressure,
(h) => h.occurredAt,
(h) => h.lowBloodPressure!.toDouble(),
),
secondaryLineColor: const Color(0xFFFF9800),
timeRange: timeRange,
),
HealthHistorySection<HeartbeatEntity>(
items: historyWithPressure,

View File

@@ -1,9 +1,49 @@
import 'package:fl_chart/fl_chart.dart';
List<FlSpot> toFlSpots<T>(List<T> items, double Function(T) getValue) {
class HealthPoint {
const HealthPoint({required this.timestampMs, required this.value});
final int timestampMs;
final double value;
}
List<HealthPoint> toPoints<T>(
List<T> items,
int Function(T) getTimestamp,
double Function(T) getValue,
) {
return items
.asMap()
.entries
.map((e) => FlSpot(e.key.toDouble(), getValue(e.value)))
.map(
(e) => HealthPoint(
timestampMs: getTimestamp(e),
value: getValue(e),
),
)
.toList();
}
/// Splits points into contiguous segments, breaking whenever the gap between
/// consecutive points exceeds [maxGap]. Keeps the line honest when the device
/// stopped measuring for a while.
List<List<FlSpot>> toSegments(
List<HealthPoint> points, {
required Duration maxGap,
}) {
if (points.isEmpty) return const [];
final maxGapMs = maxGap.inMilliseconds;
final segments = <List<FlSpot>>[];
var current = <FlSpot>[];
int? previousMs;
for (final p in points) {
final x = p.timestampMs.toDouble();
if (previousMs != null && p.timestampMs - previousMs > maxGapMs) {
if (current.isNotEmpty) segments.add(current);
current = <FlSpot>[];
}
current.add(FlSpot(x, p.value));
previousMs = p.timestampMs;
}
if (current.isNotEmpty) segments.add(current);
return segments;
}

View File

@@ -1,25 +1,33 @@
import 'package:device_management/src/core/presentation/time_range.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:utils/utils.dart';
class HealthLineChart extends StatelessWidget {
final List<FlSpot> spots;
final Color lineColor;
import 'health_chart_helpers.dart';
final List<FlSpot>? secondarySpots;
class HealthLineChart extends StatelessWidget {
final List<HealthPoint> points;
final Color lineColor;
final List<HealthPoint>? secondaryPoints;
final Color? secondaryLineColor;
final TimeRange timeRange;
const HealthLineChart({
super.key,
required this.spots,
required this.points,
required this.lineColor,
this.secondarySpots,
required this.timeRange,
this.secondaryPoints,
this.secondaryLineColor,
});
Duration get _maxGap => timeRange == TimeRange.today
? const Duration(hours: 2)
: const Duration(days: 2);
@override
Widget build(BuildContext context) {
if (spots.isEmpty) {
if (points.isEmpty) {
return SizedBox(
height: SizeUtils.getByScreen(small: 180, big: 160),
child: Center(
@@ -27,7 +35,9 @@ class HealthLineChart extends StatelessWidget {
'--',
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 16, big: 14),
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.3),
),
),
@@ -35,12 +45,20 @@ class HealthLineChart extends StatelessWidget {
);
}
final primarySegments = toSegments(points, maxGap: _maxGap);
final secondarySegments = secondaryPoints == null
? const <List<FlSpot>>[]
: toSegments(secondaryPoints!, maxGap: _maxGap);
final lines = <LineChartBarData>[
_buildLine(spots, lineColor),
if (secondarySpots != null && secondarySpots!.isNotEmpty)
_buildLine(secondarySpots!, secondaryLineColor ?? lineColor),
for (final seg in primarySegments) _buildLine(seg, lineColor),
for (final seg in secondarySegments)
_buildLine(seg, secondaryLineColor ?? lineColor),
];
final (minX, maxX) = _rangeX(points, secondaryPoints);
final xInterval = maxX > minX ? (maxX - minX) / 4 : 1.0;
return Padding(
padding: EdgeInsets.symmetric(
horizontal: SizeUtils.getByScreen(small: 16, big: 14),
@@ -49,13 +67,17 @@ class HealthLineChart extends StatelessWidget {
height: SizeUtils.getByScreen(small: 180, big: 160),
child: LineChart(
LineChartData(
minX: minX,
maxX: maxX,
lineBarsData: lines,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
horizontalInterval: _computeInterval(spots),
horizontalInterval: _verticalInterval(),
getDrawingHorizontalLine: (value) => FlLine(
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.08),
strokeWidth: 1,
),
@@ -69,14 +91,38 @@ class HealthLineChart extends StatelessWidget {
value.toInt().toString(),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
color: Theme.of(context).colorScheme.onSurface
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.4),
),
),
),
),
bottomTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: xInterval,
reservedSize: SizeUtils.getByScreen(small: 22, big: 20),
getTitlesWidget: (value, meta) {
if (value == meta.min || value == meta.max) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
_formatX(value),
style: TextStyle(
fontSize: SizeUtils.getByScreen(small: 10, big: 9),
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.4),
),
),
);
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
@@ -93,9 +139,9 @@ class HealthLineChart extends StatelessWidget {
getTooltipItems: (spots) => spots
.map(
(spot) => LineTooltipItem(
spot.y.toInt().toString(),
'${spot.y.toInt()}\n${_formatX(spot.x)}',
TextStyle(
fontSize: SizeUtils.getByScreen(small: 12, big: 11),
fontSize: SizeUtils.getByScreen(small: 11, big: 10),
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
@@ -110,11 +156,43 @@ class HealthLineChart extends StatelessWidget {
);
}
(double, double) _rangeX(
List<HealthPoint> primary,
List<HealthPoint>? secondary,
) {
final all = [...primary, ...?secondary];
final timestamps = all.map((p) => p.timestampMs);
final minMs = timestamps.reduce((a, b) => a < b ? a : b);
final maxMs = timestamps.reduce((a, b) => a > b ? a : b);
if (minMs == maxMs) {
return ((minMs - 60000).toDouble(), (maxMs + 60000).toDouble());
}
return (minMs.toDouble(), maxMs.toDouble());
}
double _verticalInterval() {
final all = [...points, ...?secondaryPoints];
if (all.length < 2) return 1;
final values = all.map((p) => p.value);
final range = values.reduce((a, b) => a > b ? a : b) -
values.reduce((a, b) => a < b ? a : b);
if (range <= 0) return 1;
return (range / 4).ceilToDouble();
}
String _formatX(double x) {
final date = DateTime.fromMillisecondsSinceEpoch(x.toInt());
String two(int v) => v.toString().padLeft(2, '0');
if (timeRange == TimeRange.today) {
return '${two(date.hour)}:${two(date.minute)}';
}
return '${two(date.day)}/${two(date.month)}';
}
LineChartBarData _buildLine(List<FlSpot> data, Color color) {
return LineChartBarData(
spots: data,
isCurved: true,
preventCurveOverShooting: true,
isCurved: false,
color: color,
barWidth: 2,
dotData: FlDotData(
@@ -128,14 +206,4 @@ class HealthLineChart extends StatelessWidget {
),
);
}
double _computeInterval(List<FlSpot> data) {
if (data.length < 2) return 1;
final yValues = data.map((s) => s.y);
final range =
yValues.reduce((a, b) => a > b ? a : b) -
yValues.reduce((a, b) => a < b ? a : b);
if (range <= 0) return 1;
return (range / 4).ceilToDouble();
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:sf_localizations/sf_localizations.dart';
import 'package:utils/utils.dart';
import '../state/health_view_state.dart';
import '../providers/health_state.dart';
class HealthStatsRow extends StatelessWidget {
final HealthStats stats;

View File

@@ -5,8 +5,8 @@ import 'package:utils/utils.dart';
import '../../domain/entities/heartbeat_entity.dart';
import '../health_colors.dart';
import '../state/health_view_model.dart';
import '../state/health_view_state.dart';
import '../providers/health_controller.dart';
import '../providers/health_state.dart';
import 'health_chart_helpers.dart';
import 'health_empty_state.dart';
import 'health_history_section.dart';
@@ -31,7 +31,9 @@ class HeartRateTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(healthViewModelProvider.notifier);
final vm = ref.read(healthControllerProvider.notifier);
final timeRange =
ref.watch(healthControllerProvider.select((s) => s.timeRange));
if (chartData.isEmpty && historyData.isEmpty) {
return HealthEmptyState(icon: Icons.favorite_rounded);
@@ -43,8 +45,13 @@ class HeartRateTab extends ConsumerWidget {
),
children: [
HealthLineChart(
spots: toFlSpots(chartData, (h) => h.heartbeats.toDouble()),
points: toPoints(
chartData,
(h) => h.occurredAt,
(h) => h.heartbeats.toDouble(),
),
lineColor: Theme.of(context).colorScheme.error,
timeRange: timeRange,
),
HealthStatsRow(
stats: stats,

View File

@@ -5,8 +5,8 @@ import 'package:utils/utils.dart';
import '../../domain/entities/oxygen_entity.dart';
import '../health_colors.dart';
import '../state/health_view_model.dart';
import '../state/health_view_state.dart';
import '../providers/health_controller.dart';
import '../providers/health_state.dart';
import 'health_chart_helpers.dart';
import 'health_empty_state.dart';
import 'health_history_section.dart';
@@ -31,7 +31,9 @@ class OxygenTab extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(healthViewModelProvider.notifier);
final vm = ref.read(healthControllerProvider.notifier);
final timeRange =
ref.watch(healthControllerProvider.select((s) => s.timeRange));
if (chartData.isEmpty && historyData.isEmpty) {
return HealthEmptyState(icon: Icons.air_rounded);
@@ -43,8 +45,13 @@ class OxygenTab extends ConsumerWidget {
),
children: [
HealthLineChart(
spots: toFlSpots(chartData, (o) => o.oxygen.toDouble()),
points: toPoints(
chartData,
(o) => o.occurredAt,
(o) => o.oxygen.toDouble(),
),
lineColor: const Color(0xFF4CAF50),
timeRange: timeRange,
),
HealthStatsRow(
stats: stats,