feat: merge health feature and add measure command

- Add REQUEST_HEART_RATE command with measure button in health screen
  - Add ref.mounted checks and fix early return in measure()
  - Remove unused SET_LANGUAGE from DeviceCommand enum
This commit is contained in:
2026-03-22 05:15:22 +01:00
11 changed files with 74 additions and 17 deletions

View File

@@ -56,14 +56,14 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
final state = ref.watch(healthViewModelProvider);
final vm = ref.read(healthViewModelProvider.notifier);
ref.listen(
healthViewModelProvider.select((s) => s.errorMessage),
(previous, next) {
if (next.isNotEmpty) {
showTopSnackbar(context, message: next, type: MessageType.error);
}
},
);
ref.listen(healthViewModelProvider.select((s) => s.errorMessage), (
previous,
next,
) {
if (next.isNotEmpty) {
showTopSnackbar(context, message: next, type: MessageType.error);
}
});
return LegacyPageLayout(
theme: theme,
@@ -130,6 +130,29 @@ 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();
}
@@ -247,4 +249,29 @@ class HealthViewModel extends Notifier<HealthViewState> {
final msg = e.toString();
return msg.startsWith('Exception: ') ? msg.substring(11) : msg;
}
Future<void> measure() async {
final device = ref.read(selectedDeviceProvider);
if (device == null) return;
try {
state = state.copyWith(isLoading: true);
final request = SendCommandRequestModel(
device: device.identificator,
command: DeviceCommand.requestHeartRate,
);
await _commandsRepository.send(request: request);
if (!ref.mounted) return;
state = state.copyWith(isLoading: false);
} catch (e) {
if (!ref.mounted) return;
state = state.copyWith(
isLoading: false,
errorMessage: _formatError(e),
);
}
}
}

View File

@@ -12,12 +12,12 @@ enum DeviceCommand {
findDevice,
@JsonValue('REQUEST_PHOTO')
requestPhoto,
@JsonValue('REQUEST_HEART_RATE')
requestHeartRate,
@JsonValue('RESTART')
restart,
@JsonValue('REWARDS')
rewards,
@JsonValue('SET_LANGUAGE')
setLanguage,
@JsonValue('SHUTDOWN')
shutdown,
@JsonValue('SET_SOUND_MODE')

View File

@@ -27,9 +27,9 @@ const _$DeviceCommandEnumMap = {
DeviceCommand.factory: 'FACTORY',
DeviceCommand.findDevice: 'FIND_DEVICE',
DeviceCommand.requestPhoto: 'REQUEST_PHOTO',
DeviceCommand.requestHeartRate: 'REQUEST_HEART_RATE',
DeviceCommand.restart: 'RESTART',
DeviceCommand.rewards: 'REWARDS',
DeviceCommand.setLanguage: 'SET_LANGUAGE',
DeviceCommand.shutdown: 'SHUTDOWN',
DeviceCommand.setSoundMode: 'SET_SOUND_MODE',
};

View File

@@ -625,5 +625,6 @@
"volumeHint": "Sie können den Schieberegler ziehen, um die Gerätelautstärke anzupassen. Die App speichert nur die zuletzt erfolgreich eingestellte Lautstärke. Die tatsächliche Lautstärke hängt vom Gerät ab.",
"volumeSend": "Senden",
"photoTaken": "Foto erfolgreich aufgenommen",
"noPhotosAvailable": "Keine Fotos verfügbar"
"noPhotosAvailable": "Keine Fotos verfügbar",
"measure": "Messen"
}

View File

@@ -757,5 +757,6 @@
"syncClockMessage": "Synchronize the device clock with the current time",
"locationWifiNetworksOptional": "WiFi networks (optional)",
"photoTaken": "Photo taken successfully",
"noPhotosAvailable": "No photos available"
"noPhotosAvailable": "No photos available",
"measure": "Measure"
}

View File

@@ -755,5 +755,6 @@
"syncClockMessage": "Sincroniza el reloj del dispositivo con la hora actual",
"locationWifiNetworksOptional": "Redes WiFi (opcional)",
"photoTaken": "Foto tomada exitosamente",
"noPhotosAvailable": "No hay fotos disponibles"
"noPhotosAvailable": "No hay fotos disponibles",
"measure": "Medir"
}

View File

@@ -625,5 +625,6 @@
"volumeHint": "Vous pouvez faire glisser le curseur pour régler le volume de l'appareil. L'application ne sauvegarde que le dernier niveau de volume ajusté avec succès. Le volume réel dépendra de l'appareil.",
"volumeSend": "Envoyer",
"photoTaken": "Photo prise avec succès",
"noPhotosAvailable": "Aucune photo disponible"
"noPhotosAvailable": "Aucune photo disponible",
"measure": "Mesurer"
}

View File

@@ -625,5 +625,6 @@
"volumeHint": "Puoi trascinare il cursore per regolare il volume del dispositivo. L'app salva solo l'ultimo livello di volume regolato con successo. Il volume effettivo dipenderà dal dispositivo.",
"volumeSend": "Invia",
"photoTaken": "Foto scattata con successo",
"noPhotosAvailable": "Nessuna foto disponibile"
"noPhotosAvailable": "Nessuna foto disponibile",
"measure": "Misurare"
}

View File

@@ -625,5 +625,6 @@
"volumeHint": "Você pode arrastar o controle deslizante para ajustar o volume do dispositivo. O aplicativo salva apenas o nível de volume ajustado com sucesso mais recentemente. O volume real dependerá do dispositivo.",
"volumeSend": "Enviar",
"photoTaken": "Foto tirada com sucesso",
"noPhotosAvailable": "Nenhuma foto disponível"
"noPhotosAvailable": "Nenhuma foto disponível",
"measure": "Medir"
}

View File

@@ -762,4 +762,5 @@ class I18n {
static const String photoTaken = 'photoTaken';
static const String noPhotosAvailable = 'noPhotosAvailable';
static const String yesterday = 'yesterday';
static const String measure = 'measure';
}