Compare commits

..

1 Commits

Author SHA1 Message Date
94c042d403 manual health measurement command 2026-03-20 15:35:12 +01:00
15 changed files with 134 additions and 73 deletions

View File

@@ -26,7 +26,7 @@ late final GoRouter appRouter;
void configureAppRouter() {
appRouter = GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: AppRoutes.controlPanel,
initialLocation: AppRoutes.splash,
debugLogDiagnostics: true,
routes: [
GoRoute(

View File

@@ -130,6 +130,31 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
),
],
),
footer: _SaveSection()
);
}
}
class _SaveSection extends ConsumerWidget{
const _SaveSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.read(themePortProvider);
final vm = ref.read(healthViewModelProvider.notifier);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10),
child:
PrimaryButton(
onPressed: () async {
await vm.measure();
},
text: context.translate(I18n.measure),
color: theme.getColorFor(ThemeCode.legacyPrimary)
)
);
}
}

View File

@@ -14,12 +14,14 @@ final healthViewModelProvider =
class HealthViewModel extends Notifier<HealthViewState> {
late final HealthRepository _repository;
late final CommandsRepository _commandsRepository;
static const int _historyPageSize = 20;
@override
HealthViewState build() {
_repository = ref.read(healthRepositoryProvider);
_commandsRepository = ref.read(commandsRepositoryProvider);
_init();
return const HealthViewState();
}
@@ -243,4 +245,32 @@ class HealthViewModel extends Notifier<HealthViewState> {
final msg = e.toString();
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
}
Future<void> measure() async {
try {
state = state.copyWith(
isLoading: true,
);
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
final request = SendCommandRequestModel(
device: device.identificator,
command: DeviceCommand.requestHeartRate,
);
await _commandsRepository.send(request: request);
state = state.copyWith(
isLoading: false,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
errorMessage: e.toString(),
);
}
}
}

View File

@@ -0,0 +1,3 @@
abstract class SetSoundUseCase {
Future<void> setSound({required String deviceId});
}

View File

@@ -0,0 +1,16 @@
import 'package:settings/src/core/domain/repositories/settings_repository.dart';
import 'set_sound_use_case.dart';
class SetSoundUseCaseImpl implements SetSoundUseCase {
SetSoundUseCaseImpl(this._repository);
final SettingsRepository _repository;
@override
Future<void> setSound({required String deviceId}) async {
return;
// return _repository.setSound(deviceId: deviceId);
}
}

View File

@@ -0,0 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:settings/src/core/providers/settings_repository_provider.dart';
import '../../domain/set_sound_use_case.dart';
import '../../domain/set_sound_use_case_impl.dart';
final setSoundUseCaseProvider = Provider.autoDispose<SetSoundUseCase>((ref) {
final settingsRepository = ref.read(settingsRepositoryProvider);
return SetSoundUseCaseImpl(settingsRepository);
});

View File

@@ -20,26 +20,6 @@ class SoundScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = ref.read(themePortProvider);
ref.listen(soundViewModelProvider.select((s) => s.errorMessage), (
_,
errorMessage,
) {
if (errorMessage.isNotEmpty) {
showTopSnackbar(
context,
message: errorMessage,
type: MessageType.error,
);
}
});
ref.listen(soundViewModelProvider.select((s) => s.isComplete), (
_,
isComplete,
) {
if (isComplete) Navigator.pop(context);
});
return LegacyPageLayout(
theme: theme,
title: context.translate(I18n.sound),
@@ -81,15 +61,15 @@ class _OptionsSection extends ConsumerWidget {
_SectionButton(
title: context.translate(I18n.soundAndVibration),
icon: Icons.volume_up_outlined,
active: soundOption == 'VIBRATION_AND_RINGING',
onPressed: () {vm.setSoundOption('VIBRATION_AND_RINGING');},
active: soundOption == 'SOUND_AND_VIBRATION',
onPressed: () {vm.setSoundOption('SOUND_AND_VIBRATION');},
),
SizedBox(height: 12),
_SectionButton(
title: context.translate(I18n.soundOnly),
icon: Icons.volume_up_outlined,
active: soundOption == 'RINGING',
onPressed: () {vm.setSoundOption('RINGING');},
active: soundOption == 'SOUND',
onPressed: () {vm.setSoundOption('SOUND');},
),
SizedBox(height: 12),
_SectionButton(
@@ -102,8 +82,8 @@ class _OptionsSection extends ConsumerWidget {
_SectionButton(
title: context.translate(I18n.silent),
icon: Icons.volume_mute_outlined,
active: soundOption == 'SILENCE',
onPressed: () {vm.setSoundOption('SILENCE');},
active: soundOption == 'SILENT',
onPressed: () {vm.setSoundOption('SILENT');},
),
]
),
@@ -172,7 +152,7 @@ class _SaveSection extends ConsumerWidget {
return Padding(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: PrimaryButton(
onPressed: () {vm.submit();},
onPressed: vm.submit,
text: context.translate(I18n.save),
color: theme.getColorFor(ThemeCode.legacyPrimary)
),

View File

@@ -1,6 +1,9 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:legacy_shared/legacy_shared.dart';
import 'package:sf_shared/sf_shared.dart';
import '../../domain/set_sound_use_case.dart';
import '../providers/set_sound_use_case_provider.dart';
import 'sound_view_state.dart';
final soundViewModelProvider =
@@ -9,11 +12,11 @@ NotifierProvider.autoDispose<SoundViewModel, SoundViewState>(
);
class SoundViewModel extends Notifier<SoundViewState> {
late final CommandsRepository _commandsRepository;
late final SetSoundUseCase _setSoundUseCase;
@override
SoundViewState build() {
_commandsRepository = ref.read(commandsRepositoryProvider);
_setSoundUseCase = ref.read(setSoundUseCaseProvider);
Future.microtask(() => load());
@@ -22,12 +25,16 @@ class SoundViewModel extends Notifier<SoundViewState> {
Future<void> load() async {
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
setDevice(device!);
state = state.copyWith(
soundOption: 'SOUND_AND_VIBRATION',
isLoading: false,
);
}
void setDevice(DeviceEntity device) {
state = state.copyWith(
deviceId: device.identificator,
soundOption: device.settings['soundMode'] ?? 'VIBRATION',
isLoading: false,
);
}
@@ -43,20 +50,8 @@ class SoundViewModel extends Notifier<SoundViewState> {
try {
state = state.copyWith(
isLoading: true,
isComplete: false,
);
final request = SendCommandRequestModel(
device: state.deviceId,
command: DeviceCommand.setSoundMode,
data: {'soundMode': state.soundOption}
);
await _commandsRepository.send(request: request);
state = state.copyWith(
isLoading: false,
isComplete: true,
);
_setSoundUseCase.setSound(deviceId: state.deviceId);
} catch (e) {
state = state.copyWith(
isLoading: false,

View File

@@ -9,7 +9,6 @@ abstract class SoundViewState with _$SoundViewState {
@Default('') String deviceId,
String? soundOption,
@Default(true) bool isLoading,
@Default(false) bool isComplete,
@Default('') String errorMessage,
}) = _SoundViewState;
}

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;
String get deviceId; String? get soundOption; bool get isLoading; 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.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.soundOption, soundOption) || other.soundOption == soundOption)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(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,deviceId,soundOption,isLoading,errorMessage);
@override
String toString() {
return 'SoundViewState(deviceId: $deviceId, soundOption: $soundOption, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)';
return 'SoundViewState(deviceId: $deviceId, soundOption: $soundOption, isLoading: $isLoading, errorMessage: $errorMessage)';
}
@@ -45,7 +45,7 @@ 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
String deviceId, String? soundOption, bool isLoading, String errorMessage
});
@@ -62,12 +62,11 @@ 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? deviceId = null,Object? soundOption = freezed,Object? isLoading = 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
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,
));
@@ -154,10 +153,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( String deviceId, String? soundOption, bool isLoading, 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.deviceId,_that.soundOption,_that.isLoading,_that.errorMessage);case _:
return orElse();
}
@@ -175,10 +174,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( String deviceId, String? soundOption, bool isLoading, 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.deviceId,_that.soundOption,_that.isLoading,_that.errorMessage);case _:
throw StateError('Unexpected subclass');
}
@@ -195,10 +194,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( String deviceId, String? soundOption, bool isLoading, 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.deviceId,_that.soundOption,_that.isLoading,_that.errorMessage);case _:
return null;
}
@@ -210,13 +209,12 @@ 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.deviceId = '', this.soundOption, this.isLoading = true, this.errorMessage = ''});
@override@JsonKey() final String deviceId;
@override final String? soundOption;
@override@JsonKey() final bool isLoading;
@override@JsonKey() final bool isComplete;
@override@JsonKey() final String errorMessage;
/// Create a copy of SoundViewState
@@ -229,16 +227,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.deviceId, deviceId) || other.deviceId == deviceId)&&(identical(other.soundOption, soundOption) || other.soundOption == soundOption)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(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,deviceId,soundOption,isLoading,errorMessage);
@override
String toString() {
return 'SoundViewState(deviceId: $deviceId, soundOption: $soundOption, isLoading: $isLoading, isComplete: $isComplete, errorMessage: $errorMessage)';
return 'SoundViewState(deviceId: $deviceId, soundOption: $soundOption, isLoading: $isLoading, errorMessage: $errorMessage)';
}
@@ -249,7 +247,7 @@ 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
String deviceId, String? soundOption, bool isLoading, String errorMessage
});
@@ -266,12 +264,11 @@ 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? deviceId = null,Object? soundOption = freezed,Object? isLoading = 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
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,
));

View File

@@ -8,14 +8,16 @@ enum DeviceCommand {
factory,
@JsonValue('FIND_DEVICE')
findDevice,
@JsonValue('REQUEST_HEART_RATE')
requestHeartRate,
@JsonValue('RESTART')
restart,
@JsonValue('REWARDS')
rewards,
@JsonValue('SHUTDOWN')
shutdown,
@JsonValue('SET_SOUND_MODE')
setSoundMode,
@JsonValue('SOUND')
sound,
}
@freezed

View File

@@ -25,8 +25,9 @@ Map<String, dynamic> _$SendCommandRequestModelToJson(
const _$DeviceCommandEnumMap = {
DeviceCommand.factory: 'FACTORY',
DeviceCommand.findDevice: 'FIND_DEVICE',
DeviceCommand.requestHeartRate: 'REQUEST_HEART_RATE',
DeviceCommand.restart: 'RESTART',
DeviceCommand.rewards: 'REWARDS',
DeviceCommand.shutdown: 'SHUTDOWN',
DeviceCommand.setSoundMode: 'SET_SOUND_MODE',
DeviceCommand.sound: 'SOUND',
};

View File

@@ -744,5 +744,6 @@
"vibrationOnly": "Vibration only",
"silent": "Silent",
"syncClockMessage": "Synchronize the device clock with the current time",
"locationWifiNetworksOptional": "WiFi networks (optional)"
"locationWifiNetworksOptional": "WiFi networks (optional)",
"measure": "Measure"
}

View File

@@ -742,5 +742,6 @@
"vibrationOnly": "Solo vibración",
"silent": "Silencio",
"syncClockMessage": "Sincroniza el reloj del dispositivo con la hora actual",
"locationWifiNetworksOptional": "Redes WiFi (opcional)"
"locationWifiNetworksOptional": "Redes WiFi (opcional)",
"measure": "Medir"
}

View File

@@ -749,4 +749,5 @@ class I18n {
static const String wifiSsid = 'wifiSsid';
static const String wifiSsidHint = 'wifiSsidHint';
static const String yesterday = 'yesterday';
static const String measure = 'measure';
}