From ddc5086b3b6e2db03f4c99cd34456700c1ed807f Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Thu, 16 Apr 2026 23:48:07 +0200 Subject: [PATCH] feat(legacy): block device commands when watch is disconnected --- .../widgets/pedometer_toggle.dart | 1 + .../presentation/background_image_screen.dart | 21 ++++++++++++++++--- .../presentation/contacts_screen.dart | 1 + .../presentation/do_not_disturb_screen.dart | 5 ++++- .../health/presentation/health_screen.dart | 8 ++++++- .../widgets/locate_device_dialog.dart | 7 ++++++- .../presentation/remote_camera_screen.dart | 5 ++++- .../remote_connection_screen.dart | 2 ++ .../presentation/widgets/spy_call_dialog.dart | 11 ++++++++-- .../scheduled_activities_screen.dart | 6 ++++-- .../widgets/activity_form_sheet.dart | 1 + .../presentation/widgets/day_timeline.dart | 11 +++++++--- .../presentation/volume_control_screen.dart | 5 ++++- .../widgets/create_frequent_place_sheet.dart | 2 ++ .../widgets/create_geofence_sheet.dart | 2 ++ .../presentation/widgets/location_map.dart | 14 ++++++++++++- .../alarm/presentation/alarm_screen.dart | 11 ++++++++-- .../alerts/presentation/alerts_screen.dart | 7 ++++++- .../battery/presentation/battery_screen.dart | 5 ++++- .../presentation/block_phone_screen.dart | 7 ++++++- .../disable_functions_screen.dart | 7 ++++++- .../presentation/language_screen.dart | 5 ++++- .../remote_management_screen.dart | 3 +++ .../sound/presentation/sound_screen.dart | 1 + .../presentation/timezone_screen.dart | 7 ++++++- .../presentation/wifi_settings_screen.dart | 7 ++++++- .../legacy_shared/lib/legacy_shared.dart | 1 + .../lib/src/utils/device_command_guard.dart | 20 ++++++++++++++++++ packages/sf_localizations/assets/l10n/de.json | 1 + packages/sf_localizations/assets/l10n/en.json | 1 + packages/sf_localizations/assets/l10n/es.json | 1 + packages/sf_localizations/assets/l10n/fr.json | 1 + packages/sf_localizations/assets/l10n/it.json | 1 + packages/sf_localizations/assets/l10n/pt.json | 1 + .../lib/src/generated/i18n.dart | 1 + .../src/domain/entities/device_entity.dart | 4 ++++ 36 files changed, 169 insertions(+), 25 deletions(-) create mode 100644 modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart diff --git a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart index 4fc5a8e0..eb9bd9d4 100644 --- a/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart +++ b/modules/legacy/modules/device_management/lib/src/features/activity_meter/presentation/widgets/pedometer_toggle.dart @@ -38,6 +38,7 @@ class PedometerToggle extends ConsumerWidget { value: enabled, activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary), onChanged: (value) async { + if (!guardDeviceCommand(context, ref)) return; final success = await ref .read(activityMeterViewModelProvider.notifier) .togglePedometer(enabled: value); diff --git a/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart b/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart index d3da2a4e..e6992313 100644 --- a/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/background_image/presentation/background_image_screen.dart @@ -1,6 +1,7 @@ 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:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -94,7 +95,12 @@ class BackgroundImageScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: state.isSaving ? null : vm.uploadPhoto, + onPressed: state.isSaving + ? null + : () { + if (!guardDeviceCommand(context, ref)) return; + vm.uploadPhoto(); + }, icon: Icon( Icons.add_photo_alternate_outlined, color: Colors.white, @@ -112,10 +118,19 @@ class BackgroundImageScreen extends ConsumerWidget { : state.isSaving ? const Center(child: CircularProgressIndicator()) : state.photos.isEmpty - ? _EmptyState(onUpload: vm.uploadPhoto, primaryColor: primaryColor) + ? _EmptyState( + onUpload: () { + if (!guardDeviceCommand(context, ref)) return; + vm.uploadPhoto(); + }, + primaryColor: primaryColor, + ) : _PhotoGrid( state: state, - onPhotoTap: vm.setAsBackground, + onPhotoTap: (id) { + if (!guardDeviceCommand(context, ref)) return; + vm.setAsBackground(id); + }, primaryColor: primaryColor, ), ), diff --git a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart index d148bf7a..38c1883e 100644 --- a/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/contacts/presentation/contacts_screen.dart @@ -59,6 +59,7 @@ class ContactsScreen extends ConsumerWidget { child: InkWell( customBorder: const CircleBorder(), onTap: () { + if (!guardDeviceCommand(context, ref)) return; if (state.contacts.length >= state.maxContacts) { showTopSnackbar( context, diff --git a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart index d275a9dd..2d18f065 100644 --- a/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/do_not_disturb/presentation/do_not_disturb_screen.dart @@ -155,7 +155,10 @@ class DoNotDisturbScreen extends ConsumerWidget { child: isSaving ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: vm.save, + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.save(); + }, text: context.translate(I18n.doNotDisturbSave), color: primaryColor, ), diff --git a/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart b/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart index f41d63c3..5a425577 100644 --- a/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/health/presentation/health_screen.dart @@ -109,6 +109,7 @@ class _HealthScreenState extends ConsumerState options: device.capabilities!.heartbeats!.options, theme: theme, onChanged: (frequency) async { + if (!guardDeviceCommand(context, ref)) return; final success = await vm.updateHeartRateFrequency( frequency: frequency, ); @@ -195,7 +196,12 @@ class _SaveSection extends ConsumerWidget { return Padding( padding: EdgeInsets.symmetric(horizontal: 24, vertical: 10), child: PrimaryButton( - onPressed: isMeasuring ? null : () => vm.measure(), + onPressed: isMeasuring + ? null + : () { + if (!guardDeviceCommand(context, ref)) return; + vm.measure(); + }, text: isMeasuring ? '...' : context.translate(I18n.measure), color: theme.getColorFor(ThemeCode.legacyPrimary), ), diff --git a/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart index 088d1688..7c323183 100644 --- a/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart +++ b/modules/legacy/modules/device_management/lib/src/features/locate_device/presentation/widgets/locate_device_dialog.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; +import 'package:legacy_shared/legacy_shared.dart'; + import '../state/locate_device_view_model.dart'; class LocateDeviceDialog extends ConsumerWidget { @@ -88,7 +90,10 @@ class LocateDeviceDialog extends ConsumerWidget { SizedBox(width: SizeUtils.getByScreen(small: 8, big: 16)), Expanded( child: PrimaryButton( - onPressed: vm.locateDevice, + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.locateDevice(); + }, text: context.translate(I18n.accept), color: theme.getColorFor(ThemeCode.legacyPrimary), height: SizeUtils.getByScreen(small: 38, big: 36), diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart index c6415258..6d4ff8a6 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_camera_screen.dart @@ -152,7 +152,10 @@ class _TakePictureSection extends ConsumerWidget { big: EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), child: PrimaryButton( - onPressed: vm.takePicture, + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.takePicture(); + }, text: context.translate(I18n.takePicture), color: theme.getColorFor(ThemeCode.legacyPrimary), height: SizeUtils.getByScreen(small: 36, big: 35), diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart index 0ed4dac8..a7e6fbe0 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/remote_connection_screen.dart @@ -33,6 +33,7 @@ class RemoteConnectionScreen extends ConsumerWidget { if (cameraEnabled) ...[ _SectionButton( onPressed: () { + if (!guardDeviceCommand(context, ref)) return; Navigator.push( context, MaterialPageRoute( @@ -49,6 +50,7 @@ class RemoteConnectionScreen extends ConsumerWidget { ], _SectionButton( onPressed: () { + if (!guardDeviceCommand(context, ref)) return; showDialog( context: context, builder: (context) => Dialog(child: SpyCallDialog()), diff --git a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart index f264cbe2..021af859 100644 --- a/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart +++ b/modules/legacy/modules/device_management/lib/src/features/remote_connection/presentation/widgets/spy_call_dialog.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_model.dart'; import 'package:device_management/src/features/remote_connection/presentation/state/remote_connection_view_state.dart'; +import 'package:legacy_shared/legacy_shared.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -76,9 +77,15 @@ class SpyCallDialog extends ConsumerWidget { ), ), SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)), - _PhoneSection(onSubmit: vm.call), + _PhoneSection(onSubmit: () { + if (!guardDeviceCommand(context, ref)) return; + vm.call(); + }), SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)), - _CallSection(onPressed: vm.call), + _CallSection(onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.call(); + }), ], ), ); diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart index 2ee88090..43f0cec6 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/scheduled_activities_screen.dart @@ -104,8 +104,10 @@ class _ScheduledActivitiesScreenState shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), - onTap: () => - showActivityFormSheet(context, weekDay: _selectedWeekDay), + onTap: () { + if (!guardDeviceCommand(context, ref)) return; + showActivityFormSheet(context, weekDay: _selectedWeekDay); + }, child: SizedBox( width: SizeUtils.getByScreen(small: 48, big: 46), height: SizeUtils.getByScreen(small: 48, big: 46), diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart index 6bfac262..d6304e53 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/activity_form_sheet.dart @@ -99,6 +99,7 @@ class _ActivityFormSheetState extends ConsumerState { void _submit() { if (!_isFormValid) return; + if (!guardDeviceCommand(context, ref)) return; final name = _nameController.text.trim(); final period = _buildPeriod(); diff --git a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart index 1bda7393..1a42737b 100644 --- a/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart +++ b/modules/legacy/modules/device_management/lib/src/features/scheduled_activities/presentation/widgets/day_timeline.dart @@ -1,6 +1,7 @@ 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:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -155,14 +156,14 @@ class _TimelineDotAndLine extends StatelessWidget { } } -class _ActivityTimelineCard extends StatelessWidget { +class _ActivityTimelineCard extends ConsumerWidget { final ScheduledActivityEntity activity; final ThemePort theme; const _ActivityTimelineCard({required this.activity, required this.theme}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Container( margin: EdgeInsets.only(bottom: SizeUtils.getByScreen(small: 10, big: 8)), padding: SizeUtils.getByScreen( @@ -187,7 +188,10 @@ class _ActivityTimelineCard extends StatelessWidget { ), ), IconButton( - onPressed: () => showActivityFormSheet(context, activity: activity), + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + showActivityFormSheet(context, activity: activity); + }, icon: Icon( Icons.edit_outlined, color: theme.getColorFor(ThemeCode.legacyPrimary), @@ -198,6 +202,7 @@ class _ActivityTimelineCard extends StatelessWidget { ), IconButton( onPressed: () { + if (!guardDeviceCommand(context, ref)) return; showDialog( context: context, builder: (_) => Dialog( diff --git a/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart b/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart index fee78b7e..8cb821ce 100644 --- a/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart +++ b/modules/legacy/modules/device_management/lib/src/features/volume_control/presentation/volume_control_screen.dart @@ -81,7 +81,10 @@ class VolumeControlScreen extends ConsumerWidget { child: state.isLoading ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: vm.submit, + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.submit(); + }, text: context.translate(I18n.volumeSend), color: primaryColor, ), diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart index 27a12f86..3775eeff 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_frequent_place_sheet.dart @@ -2,6 +2,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; +import 'package:legacy_shared/legacy_shared.dart'; import 'package:location/src/core/domain/entities/frequent_place_entity.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -74,6 +75,7 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> { Future _submit() async { if (!_canSave) return; + if (!guardDeviceCommand(context, ref)) return; final vm = ref.read(locationViewModelProvider.notifier); final bool success; diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart index 1263328a..fad7c9c4 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/create_geofence_sheet.dart @@ -2,6 +2,7 @@ import 'package:design_system/design_system.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:latlong2/latlong.dart'; +import 'package:legacy_shared/legacy_shared.dart'; import 'package:location/src/core/domain/entities/geofence_entity.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -90,6 +91,7 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> { Future _submit() async { if (!_canSave) return; + if (!guardDeviceCommand(context, ref)) return; final vm = ref.read(locationViewModelProvider.notifier); final description = _descriptionController.text.trim(); diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart index 9fe34469..e3005488 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/widgets/location_map.dart @@ -91,6 +91,7 @@ class _LocationMapState extends ConsumerState _followTimer?.cancel(); final frequency = widget.selectedDevice?.settings.frequency ?? 60; _followTimer = Timer.periodic(Duration(seconds: frequency), (_) { + if (ref.read(selectedDeviceProvider).value?.isDisconnected ?? true) return; widget.onRefreshPosition(); }); } @@ -236,6 +237,7 @@ class _LocationMapState extends ConsumerState } Future _updateFrequency(int frequency) async { + if (!guardDeviceCommand(context, ref)) return; final success = await ref .read(locationViewModelProvider.notifier) .updateLocationFrequency(frequency: frequency); @@ -263,6 +265,7 @@ class _LocationMapState extends ConsumerState } void _confirmPlacement() { + if (!guardDeviceCommand(context, ref)) return; final center = _mapController.camera.center; final mapState = ref.read(locationMapViewModelProvider); @@ -287,6 +290,7 @@ class _LocationMapState extends ConsumerState } void _confirmRadius() { + if (!guardDeviceCommand(context, ref)) return; final mapState = ref.read(locationMapViewModelProvider); final point = mapState.previewPoint!; final radius = mapState.previewRadius; @@ -411,6 +415,7 @@ class _LocationMapState extends ConsumerState } void _onEditFrequentPlace(FrequentPlaceEntity fp) { + if (!guardDeviceCommand(context, ref)) return; _vm.clearSelectedFrequentPlace(); showNameInputSheet( context, @@ -669,6 +674,7 @@ class _LocationMapState extends ConsumerState onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace), onShareTap: _shareLocation, onRefreshTap: () { + if (!guardDeviceCommand(context, ref)) return; unawaited( ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(), ); @@ -682,6 +688,7 @@ class _LocationMapState extends ConsumerState onCenterTap: _centerOnDevice, onToggleFollow: () { final willActivate = !mapState.isFollowing; + if (willActivate && !guardDeviceCommand(context, ref)) return; _vm.toggleFollowing(); unawaited( ref @@ -714,8 +721,12 @@ class _LocationMapState extends ConsumerState child: GeofenceInfoCard( geofence: mapState.selectedGeofence!, onClose: _vm.clearSelectedGeofence, - onEdit: () => _vm.startEditingGeofence(mapState.selectedGeofence!), + onEdit: () { + if (!guardDeviceCommand(context, ref)) return; + _vm.startEditingGeofence(mapState.selectedGeofence!); + }, onDelete: () { + if (!guardDeviceCommand(context, ref)) return; final id = mapState.selectedGeofence!.id; _vm.clearSelectedGeofence(); ref @@ -732,6 +743,7 @@ class _LocationMapState extends ConsumerState onClose: _vm.clearSelectedFrequentPlace, onEdit: () => _onEditFrequentPlace(mapState.selectedFrequentPlace!), onDelete: () { + if (!guardDeviceCommand(context, ref)) return; final id = mapState.selectedFrequentPlace!.id; _vm.clearSelectedFrequentPlace(); ref diff --git a/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart b/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart index 0160544d..ad308039 100644 --- a/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/alarm/presentation/alarm_screen.dart @@ -1,6 +1,7 @@ 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:navigation/navigation.dart'; import 'package:settings/src/features/alarm/domain/entities/alarm_entity.dart'; import 'package:settings/src/features/alarm/presentation/state/alarm_view_model.dart'; @@ -91,7 +92,10 @@ class AlarmScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () => _openForm(context, vm), + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + _openForm(context, vm); + }, icon: Icon( Icons.add, color: Colors.white, @@ -122,7 +126,10 @@ class AlarmScreen extends ConsumerWidget { child: isSaving ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: vm.save, + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.save(); + }, text: context.translate(I18n.alarmSave), color: primaryColor, ), diff --git a/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart b/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart index 3d2dd1fb..c0152096 100644 --- a/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/alerts/presentation/alerts_screen.dart @@ -79,7 +79,12 @@ class AlertsScreen extends ConsumerWidget { footer: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), child: PrimaryButton( - onPressed: state.isSaving ? null : () => vm.save(), + onPressed: state.isSaving + ? null + : () { + if (!guardDeviceCommand(context, ref)) return; + vm.save(); + }, text: state.isSaving ? '...' : context.translate(I18n.save), color: theme.getColorFor(ThemeCode.legacyPrimary), ), diff --git a/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart b/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart index 03e7d068..74350cb7 100644 --- a/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/battery/presentation/battery_screen.dart @@ -99,7 +99,10 @@ class BatteryScreen extends ConsumerWidget { activeTrackColor: primaryColor, onChanged: state.isSaving ? null - : (value) => vm.toggleNightMode(value), + : (value) { + if (!guardDeviceCommand(context, ref)) return; + vm.toggleNightMode(value); + }, ), ], ), diff --git a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart index d968e1ca..df7d33f3 100644 --- a/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/block_phone/presentation/block_phone_screen.dart @@ -1,6 +1,7 @@ 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:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -83,7 +84,10 @@ class BlockPhoneScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () => showAddContactSheet(context), + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + showAddContactSheet(context); + }, icon: Icon( Icons.add, color: Colors.white, @@ -218,6 +222,7 @@ class _ContactList extends ConsumerWidget { int index, String name, ) { + if (!guardDeviceCommand(context, ref)) return; final theme = ref.read(themePortProvider); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); diff --git a/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart b/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart index 9e2b3d05..85205550 100644 --- a/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/disable_functions/presentation/disable_functions_screen.dart @@ -76,7 +76,12 @@ class DisableFunctionsScreen extends ConsumerWidget { footer: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), child: PrimaryButton( - onPressed: state.isSaving ? null : () => vm.save(), + onPressed: state.isSaving + ? null + : () { + if (!guardDeviceCommand(context, ref)) return; + vm.save(); + }, text: state.isSaving ? '...' : context.translate(I18n.save), color: primaryColor, ), diff --git a/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart b/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart index f905c0c7..0c2bcb44 100644 --- a/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/language/presentation/language_screen.dart @@ -110,7 +110,10 @@ class _SaveSection extends ConsumerWidget { child: isLoading ? const Center(child: CircularProgressIndicator()) : PrimaryButton( - onPressed: vm.submit, + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + vm.submit(); + }, text: context.translate(I18n.save), color: theme.getColorFor(ThemeCode.legacyPrimary), ), diff --git a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart index 3ccb6cc6..cb403b26 100644 --- a/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/remote_management/presentation/remote_management_screen.dart @@ -52,6 +52,7 @@ class _OptionsSection extends ConsumerWidget { subtitle: context.translate(I18n.remoteTurnOffMessage), icon: Icons.settings_power_outlined, onPressed: () { + if (!guardDeviceCommand(context, ref)) return; showDialog( context: context, builder: (context) => Dialog( @@ -95,6 +96,7 @@ class _OptionsSection extends ConsumerWidget { subtitle: context.translate(I18n.remoteRestartMessage), icon: Icons.refresh_outlined, onPressed: () { + if (!guardDeviceCommand(context, ref)) return; showDialog( context: context, builder: (context) => Dialog( @@ -116,6 +118,7 @@ class _OptionsSection extends ConsumerWidget { subtitle: context.translate(I18n.remoteFactoryResetMessage), icon: Icons.restart_alt_outlined, onPressed: () { + if (!guardDeviceCommand(context, ref)) return; showDialog( context: context, builder: (context) => Dialog( diff --git a/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart b/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart index 47d1756d..7f642990 100644 --- a/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/sound/presentation/sound_screen.dart @@ -178,6 +178,7 @@ class _SaveSection extends ConsumerWidget { padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), child: PrimaryButton( onPressed: () { + if (!guardDeviceCommand(context, ref)) return; vm.submit(); }, text: context.translate(I18n.save), diff --git a/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart b/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart index e6431f8a..d55b7296 100644 --- a/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/timezone/presentation/timezone_screen.dart @@ -94,7 +94,12 @@ class TimezoneScreen extends ConsumerWidget { footer: Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 10), child: PrimaryButton( - onPressed: state.isSaving ? null : () => vm.save(), + onPressed: state.isSaving + ? null + : () { + if (!guardDeviceCommand(context, ref)) return; + vm.save(); + }, text: state.isSaving ? '...' : context.translate(I18n.save), color: primaryColor, ), diff --git a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart index 8c7d7e8b..9f4eab50 100644 --- a/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart +++ b/modules/legacy/modules/settings/lib/src/features/wifi_settings/presentation/wifi_settings_screen.dart @@ -1,6 +1,7 @@ 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:navigation/navigation.dart'; import 'package:sf_localizations/sf_localizations.dart'; import 'package:utils/utils.dart'; @@ -83,7 +84,10 @@ class WifiSettingsScreen extends ConsumerWidget { shape: BoxShape.circle, ), child: IconButton( - onPressed: () => showAddWifiNetworkSheet(context), + onPressed: () { + if (!guardDeviceCommand(context, ref)) return; + showAddWifiNetworkSheet(context); + }, icon: Icon( Icons.add, color: Colors.white, @@ -212,6 +216,7 @@ class _NetworkList extends ConsumerWidget { } void _confirmDelete(BuildContext context, WidgetRef ref, dynamic network) { + if (!guardDeviceCommand(context, ref)) return; final theme = ref.read(themePortProvider); final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); diff --git a/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart b/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart index 30f16fbf..cb5e3020 100644 --- a/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart +++ b/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart @@ -17,3 +17,4 @@ export 'src/providers/devices_repository_provider.dart'; export 'src/providers/legacy_devices_provider.dart'; export 'src/data/datasources/device_settings_update_datasource.dart'; export 'src/providers/device_settings_update_provider.dart'; +export 'src/utils/device_command_guard.dart'; diff --git a/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart b/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart new file mode 100644 index 00000000..5ec78a02 --- /dev/null +++ b/modules/legacy/packages/legacy_shared/lib/src/utils/device_command_guard.dart @@ -0,0 +1,20 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sf_localizations/sf_localizations.dart'; +import 'package:sf_shared/sf_shared.dart'; + +import '../providers/selected_device_provider.dart'; + +bool guardDeviceCommand(BuildContext context, WidgetRef ref) { + final device = ref.read(selectedDeviceProvider).value; + if (device == null || device.isDisconnected) { + showTopSnackbar( + context, + message: context.translate(I18n.errorDeviceDisconnected), + type: MessageType.error, + ); + return false; + } + return true; +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index f6541f30..22c9e9c0 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -674,6 +674,7 @@ "errorContactsMin": "Das Gerät muss mindestens einen Kontakt haben", "errorContactsMax": "Maximale Kontaktanzahl für dieses Gerät erreicht", "errorPositions": "Positionen konnten nicht geladen werden", + "errorDeviceDisconnected": "Die Uhr ist getrennt und kann keine Befehle empfangen", "errorSosContactsMax": "Maximale SOS-Kontaktanzahl für dieses Gerät erreicht", "customBackground": "Benutzerdefiniertes Hintergrundbild", "backgroundImageDescription": "Legen Sie ein Foto als benutzerdefinierten Bildschirmschoner für das Gerät fest", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index d319c246..8ae66092 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -826,6 +826,7 @@ "errorContactsMin": "The device must have at least one contact", "errorContactsMax": "Maximum contacts reached for this device", "errorPositions": "Could not load positions", + "errorDeviceDisconnected": "The watch is disconnected and cannot receive commands", "errorSosContactsMax": "Maximum SOS contacts reached for this device", "customBackground": "Custom background image", "backgroundImageDescription": "Set a photo as a custom screensaver for the device", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index b03578ba..56939076 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -827,6 +827,7 @@ "errorContactsMin": "El dispositivo debe tener al menos un contacto", "errorContactsMax": "Se ha alcanzado el máximo de contactos para este dispositivo", "errorPositions": "No se pudieron cargar las posiciones", + "errorDeviceDisconnected": "El reloj está desconectado y no puede recibir comandos", "errorSosContactsMax": "Se ha alcanzado el máximo de contactos SOS para este dispositivo", "customBackground": "Fondo de pantalla personalizado", "backgroundImageDescription": "Configura una foto como protector de pantalla exclusivo para el dispositivo", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index edaa2d2d..86f4d02f 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -674,6 +674,7 @@ "errorContactsMin": "L'appareil doit avoir au moins un contact", "errorContactsMax": "Nombre maximum de contacts atteint pour cet appareil", "errorPositions": "Impossible de charger les positions", + "errorDeviceDisconnected": "La montre est déconnectée et ne peut pas recevoir de commandes", "errorSosContactsMax": "Nombre maximum de contacts SOS atteint pour cet appareil", "customBackground": "Image de fond personnalisée", "backgroundImageDescription": "Définissez une photo comme écran de veille personnalisé pour l'appareil", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index e43f5cd1..bf97c6cc 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -674,6 +674,7 @@ "errorContactsMin": "Il dispositivo deve avere almeno un contatto", "errorContactsMax": "Numero massimo di contatti raggiunto per questo dispositivo", "errorPositions": "Impossibile caricare le posizioni", + "errorDeviceDisconnected": "L'orologio è disconnesso e non può ricevere comandi", "errorSosContactsMax": "Numero massimo di contatti SOS raggiunto per questo dispositivo", "customBackground": "Immagine di sfondo personalizzata", "backgroundImageDescription": "Imposta una foto come screensaver personalizzato per il dispositivo", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 0d3407ed..3f6b7f97 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -674,6 +674,7 @@ "errorContactsMin": "O dispositivo deve ter pelo menos um contacto", "errorContactsMax": "Número máximo de contactos atingido para este dispositivo", "errorPositions": "Não foi possível carregar as posições", + "errorDeviceDisconnected": "O relógio está desconectado e não pode receber comandos", "errorSosContactsMax": "Número máximo de contactos SOS atingido para este dispositivo", "customBackground": "Imagem de fundo personalizada", "backgroundImageDescription": "Defina uma foto como protetor de ecrã personalizado para o dispositivo", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index 94896c8d..39b8aa54 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -378,6 +378,7 @@ class I18n { static const String errorCall = 'errorCall'; static const String errorContactsMax = 'errorContactsMax'; static const String errorPositions = 'errorPositions'; + static const String errorDeviceDisconnected = 'errorDeviceDisconnected'; static const String errorSosContactsMax = 'errorSosContactsMax'; static const String errorContactsMin = 'errorContactsMin'; static const String errorDisableFunctions = 'errorDisableFunctions'; diff --git a/packages/sf_shared/lib/src/domain/entities/device_entity.dart b/packages/sf_shared/lib/src/domain/entities/device_entity.dart index 4a91aaa8..b4cf00af 100644 --- a/packages/sf_shared/lib/src/domain/entities/device_entity.dart +++ b/packages/sf_shared/lib/src/domain/entities/device_entity.dart @@ -37,3 +37,7 @@ abstract class DeviceEntity with _$DeviceEntity { String? updatedAt, }) = _DeviceEntity; } + +extension DeviceEntityFlags on DeviceEntity { + bool get isDisconnected => flags['isDisconnect'] == true; +}