refactor(legacy): make guardDeviceCommand async with stale TTL refetch
Convert the shared command guard to an async check that refetches /devices when the cached state is older than 30s, so the isDisconnect flag reflects reality before a command runs. A TopSnackBar explains the check only if the fetch takes longer than 400ms, avoiding noise on fast responses. Update all 44 call sites to await the guard.
This commit is contained in:
@@ -39,7 +39,7 @@ class PedometerToggle extends ConsumerWidget {
|
|||||||
value: enabled,
|
value: enabled,
|
||||||
activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary),
|
activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
final success = await ref
|
final success = await ref
|
||||||
.read(activityMeterViewModelProvider.notifier)
|
.read(activityMeterViewModelProvider.notifier)
|
||||||
.togglePedometer(enabled: value);
|
.togglePedometer(enabled: value);
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ class BackgroundImageScreen extends ConsumerWidget {
|
|||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: state.isSaving
|
onPressed: state.isSaving
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.uploadPhoto();
|
vm.uploadPhoto();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -119,16 +119,16 @@ class BackgroundImageScreen extends ConsumerWidget {
|
|||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: state.photos.isEmpty
|
: state.photos.isEmpty
|
||||||
? _EmptyState(
|
? _EmptyState(
|
||||||
onUpload: () {
|
onUpload: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.uploadPhoto();
|
vm.uploadPhoto();
|
||||||
},
|
},
|
||||||
primaryColor: primaryColor,
|
primaryColor: primaryColor,
|
||||||
)
|
)
|
||||||
: _PhotoGrid(
|
: _PhotoGrid(
|
||||||
state: state,
|
state: state,
|
||||||
onPhotoTap: (id) {
|
onPhotoTap: (id) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.setAsBackground(id);
|
vm.setAsBackground(id);
|
||||||
},
|
},
|
||||||
primaryColor: primaryColor,
|
primaryColor: primaryColor,
|
||||||
|
|||||||
@@ -58,8 +58,9 @@ class ContactsScreen extends ConsumerWidget {
|
|||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
if (state.contacts.length >= state.maxContacts) {
|
if (state.contacts.length >= state.maxContacts) {
|
||||||
showTopSnackbar(
|
showTopSnackbar(
|
||||||
context,
|
context,
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ class DoNotDisturbScreen extends ConsumerWidget {
|
|||||||
child: isSaving
|
child: isSaving
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: PrimaryButton(
|
: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.save();
|
vm.save();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.doNotDisturbSave),
|
text: context.translate(I18n.doNotDisturbSave),
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
|
|||||||
options: device.capabilities!.heartbeats!.options,
|
options: device.capabilities!.heartbeats!.options,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
onChanged: (frequency) async {
|
onChanged: (frequency) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
final success = await vm.updateHeartRateFrequency(
|
final success = await vm.updateHeartRateFrequency(
|
||||||
frequency: frequency,
|
frequency: frequency,
|
||||||
);
|
);
|
||||||
@@ -199,8 +199,8 @@ class _SaveSection extends ConsumerWidget {
|
|||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: isMeasuring
|
onPressed: isMeasuring
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.measure();
|
vm.measure();
|
||||||
},
|
},
|
||||||
text: isMeasuring ? '...' : context.translate(I18n.measure),
|
text: isMeasuring ? '...' : context.translate(I18n.measure),
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ class LocateDeviceDialog extends ConsumerWidget {
|
|||||||
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 16)),
|
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 16)),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.locateDevice();
|
vm.locateDevice();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.accept),
|
text: context.translate(I18n.accept),
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ class _TakePictureSection extends ConsumerWidget {
|
|||||||
big: EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
big: EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||||
),
|
),
|
||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.takePicture();
|
vm.takePicture();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.takePicture),
|
text: context.translate(I18n.takePicture),
|
||||||
|
|||||||
@@ -33,8 +33,9 @@ class RemoteConnectionScreen extends ConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
if (cameraEnabled) ...[
|
if (cameraEnabled) ...[
|
||||||
_SectionButton(
|
_SectionButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@@ -50,8 +51,9 @@ class RemoteConnectionScreen extends ConsumerWidget {
|
|||||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 15)),
|
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 15)),
|
||||||
],
|
],
|
||||||
_SectionButton(
|
_SectionButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Dialog(child: SpyCallDialog()),
|
builder: (context) => Dialog(child: SpyCallDialog()),
|
||||||
|
|||||||
@@ -77,13 +77,13 @@ class SpyCallDialog extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),
|
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),
|
||||||
_PhoneSection(onSubmit: () {
|
_PhoneSection(onSubmit: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.call();
|
vm.call();
|
||||||
}),
|
}),
|
||||||
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
|
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
|
||||||
_CallSection(onPressed: () {
|
_CallSection(onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.call();
|
vm.call();
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -104,8 +104,9 @@ class _ScheduledActivitiesScreenState
|
|||||||
shape: const CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
customBorder: const CircleBorder(),
|
customBorder: const CircleBorder(),
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showActivityFormSheet(context, weekDay: _selectedWeekDay);
|
showActivityFormSheet(context, weekDay: _selectedWeekDay);
|
||||||
},
|
},
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
|
|||||||
@@ -97,9 +97,10 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _submit() {
|
Future<void> _submit() async {
|
||||||
if (!_isFormValid) return;
|
if (!_isFormValid) return;
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
final name = _nameController.text.trim();
|
final name = _nameController.text.trim();
|
||||||
final period = _buildPeriod();
|
final period = _buildPeriod();
|
||||||
|
|||||||
@@ -188,8 +188,9 @@ class _ActivityTimelineCard extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showActivityFormSheet(context, activity: activity);
|
showActivityFormSheet(context, activity: activity);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -201,8 +202,9 @@ class _ActivityTimelineCard extends ConsumerWidget {
|
|||||||
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 6)),
|
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 6)),
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => Dialog(
|
builder: (_) => Dialog(
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ class VolumeControlScreen extends ConsumerWidget {
|
|||||||
child: state.isLoading
|
child: state.isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: PrimaryButton(
|
: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.submit();
|
vm.submit();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.volumeSend),
|
text: context.translate(I18n.volumeSend),
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> {
|
|||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_canSave) return;
|
if (!_canSave) return;
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
|
||||||
final vm = ref.read(locationViewModelProvider.notifier);
|
final vm = ref.read(locationViewModelProvider.notifier);
|
||||||
final bool success;
|
final bool success;
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> {
|
|||||||
|
|
||||||
Future<void> _submit() async {
|
Future<void> _submit() async {
|
||||||
if (!_canSave) return;
|
if (!_canSave) return;
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
|
||||||
final vm = ref.read(locationViewModelProvider.notifier);
|
final vm = ref.read(locationViewModelProvider.notifier);
|
||||||
final description = _descriptionController.text.trim();
|
final description = _descriptionController.text.trim();
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateFrequency(int frequency) async {
|
Future<void> _updateFrequency(int frequency) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
final success = await ref
|
final success = await ref
|
||||||
.read(locationViewModelProvider.notifier)
|
.read(locationViewModelProvider.notifier)
|
||||||
.updateLocationFrequency(frequency: frequency);
|
.updateLocationFrequency(frequency: frequency);
|
||||||
@@ -264,8 +264,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmPlacement() {
|
Future<void> _confirmPlacement() async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!mounted) return;
|
||||||
final center = _mapController.camera.center;
|
final center = _mapController.camera.center;
|
||||||
final mapState = ref.read(locationMapViewModelProvider);
|
final mapState = ref.read(locationMapViewModelProvider);
|
||||||
|
|
||||||
@@ -289,8 +290,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmRadius() {
|
Future<void> _confirmRadius() async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!mounted) return;
|
||||||
final mapState = ref.read(locationMapViewModelProvider);
|
final mapState = ref.read(locationMapViewModelProvider);
|
||||||
final point = mapState.previewPoint!;
|
final point = mapState.previewPoint!;
|
||||||
final radius = mapState.previewRadius;
|
final radius = mapState.previewRadius;
|
||||||
@@ -414,8 +416,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEditFrequentPlace(FrequentPlaceEntity fp) {
|
Future<void> _onEditFrequentPlace(FrequentPlaceEntity fp) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!mounted) return;
|
||||||
_vm.clearSelectedFrequentPlace();
|
_vm.clearSelectedFrequentPlace();
|
||||||
showNameInputSheet(
|
showNameInputSheet(
|
||||||
context,
|
context,
|
||||||
@@ -673,12 +676,13 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
|
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
|
||||||
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
|
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
|
||||||
onShareTap: _shareLocation,
|
onShareTap: _shareLocation,
|
||||||
onRefreshTap: () {
|
onRefreshTap: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
unawaited(
|
unawaited(
|
||||||
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
|
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
|
||||||
);
|
);
|
||||||
widget.onRefreshPosition();
|
widget.onRefreshPosition();
|
||||||
|
if (!mounted) return;
|
||||||
showTopSnackbar(
|
showTopSnackbar(
|
||||||
context,
|
context,
|
||||||
message: context.translate(I18n.locationMapRefreshRequested),
|
message: context.translate(I18n.locationMapRefreshRequested),
|
||||||
@@ -686,9 +690,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
onCenterTap: _centerOnDevice,
|
onCenterTap: _centerOnDevice,
|
||||||
onToggleFollow: () {
|
onToggleFollow: () async {
|
||||||
final willActivate = !mapState.isFollowing;
|
final willActivate = !mapState.isFollowing;
|
||||||
if (willActivate && !guardDeviceCommand(context, ref)) return;
|
if (willActivate && !await guardDeviceCommand(context, ref)) return;
|
||||||
_vm.toggleFollowing();
|
_vm.toggleFollowing();
|
||||||
unawaited(
|
unawaited(
|
||||||
ref
|
ref
|
||||||
@@ -698,6 +702,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
if (willActivate && widget.selectedPosition != null) {
|
if (willActivate && widget.selectedPosition != null) {
|
||||||
_centerOnDevice();
|
_centerOnDevice();
|
||||||
}
|
}
|
||||||
|
if (!mounted) return;
|
||||||
showTopSnackbar(
|
showTopSnackbar(
|
||||||
context,
|
context,
|
||||||
message: context.translate(
|
message: context.translate(
|
||||||
@@ -721,12 +726,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
child: GeofenceInfoCard(
|
child: GeofenceInfoCard(
|
||||||
geofence: mapState.selectedGeofence!,
|
geofence: mapState.selectedGeofence!,
|
||||||
onClose: _vm.clearSelectedGeofence,
|
onClose: _vm.clearSelectedGeofence,
|
||||||
onEdit: () {
|
onEdit: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
_vm.startEditingGeofence(mapState.selectedGeofence!);
|
_vm.startEditingGeofence(mapState.selectedGeofence!);
|
||||||
},
|
},
|
||||||
onDelete: () {
|
onDelete: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
final id = mapState.selectedGeofence!.id;
|
final id = mapState.selectedGeofence!.id;
|
||||||
_vm.clearSelectedGeofence();
|
_vm.clearSelectedGeofence();
|
||||||
ref
|
ref
|
||||||
@@ -742,8 +747,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
|||||||
frequentPlace: mapState.selectedFrequentPlace!,
|
frequentPlace: mapState.selectedFrequentPlace!,
|
||||||
onClose: _vm.clearSelectedFrequentPlace,
|
onClose: _vm.clearSelectedFrequentPlace,
|
||||||
onEdit: () => _onEditFrequentPlace(mapState.selectedFrequentPlace!),
|
onEdit: () => _onEditFrequentPlace(mapState.selectedFrequentPlace!),
|
||||||
onDelete: () {
|
onDelete: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
final id = mapState.selectedFrequentPlace!.id;
|
final id = mapState.selectedFrequentPlace!.id;
|
||||||
_vm.clearSelectedFrequentPlace();
|
_vm.clearSelectedFrequentPlace();
|
||||||
ref
|
ref
|
||||||
|
|||||||
@@ -92,8 +92,9 @@ class AlarmScreen extends ConsumerWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
_openForm(context, vm);
|
_openForm(context, vm);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -126,8 +127,8 @@ class AlarmScreen extends ConsumerWidget {
|
|||||||
child: isSaving
|
child: isSaving
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: PrimaryButton(
|
: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.save();
|
vm.save();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.alarmSave),
|
text: context.translate(I18n.alarmSave),
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ class AlertsScreen extends ConsumerWidget {
|
|||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: state.isSaving
|
onPressed: state.isSaving
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.save();
|
vm.save();
|
||||||
},
|
},
|
||||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||||
|
|||||||
@@ -99,8 +99,8 @@ class BatteryScreen extends ConsumerWidget {
|
|||||||
activeTrackColor: primaryColor,
|
activeTrackColor: primaryColor,
|
||||||
onChanged: state.isSaving
|
onChanged: state.isSaving
|
||||||
? null
|
? null
|
||||||
: (value) {
|
: (value) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.toggleNightMode(value);
|
vm.toggleNightMode(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -84,8 +84,9 @@ class BlockPhoneScreen extends ConsumerWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showAddContactSheet(context);
|
showAddContactSheet(context);
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -216,13 +217,14 @@ class _ContactList extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmDelete(
|
Future<void> _confirmDelete(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
int index,
|
int index,
|
||||||
String name,
|
String name,
|
||||||
) {
|
) async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
final theme = ref.read(themePortProvider);
|
final theme = ref.read(themePortProvider);
|
||||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
||||||
|
|
||||||
|
|||||||
@@ -78,8 +78,8 @@ class DisableFunctionsScreen extends ConsumerWidget {
|
|||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: state.isSaving
|
onPressed: state.isSaving
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.save();
|
vm.save();
|
||||||
},
|
},
|
||||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||||
|
|||||||
@@ -110,8 +110,8 @@ class _SaveSection extends ConsumerWidget {
|
|||||||
child: isLoading
|
child: isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: PrimaryButton(
|
: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.submit();
|
vm.submit();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.save),
|
text: context.translate(I18n.save),
|
||||||
|
|||||||
@@ -51,8 +51,9 @@ class _OptionsSection extends ConsumerWidget {
|
|||||||
title: context.translate(I18n.remoteTurnOff),
|
title: context.translate(I18n.remoteTurnOff),
|
||||||
subtitle: context.translate(I18n.remoteTurnOffMessage),
|
subtitle: context.translate(I18n.remoteTurnOffMessage),
|
||||||
icon: Icons.settings_power_outlined,
|
icon: Icons.settings_power_outlined,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Dialog(
|
builder: (context) => Dialog(
|
||||||
@@ -95,8 +96,9 @@ class _OptionsSection extends ConsumerWidget {
|
|||||||
title: context.translate(I18n.remoteRestart),
|
title: context.translate(I18n.remoteRestart),
|
||||||
subtitle: context.translate(I18n.remoteRestartMessage),
|
subtitle: context.translate(I18n.remoteRestartMessage),
|
||||||
icon: Icons.refresh_outlined,
|
icon: Icons.refresh_outlined,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Dialog(
|
builder: (context) => Dialog(
|
||||||
@@ -117,8 +119,9 @@ class _OptionsSection extends ConsumerWidget {
|
|||||||
title: context.translate(I18n.remoteFactoryReset),
|
title: context.translate(I18n.remoteFactoryReset),
|
||||||
subtitle: context.translate(I18n.remoteFactoryResetMessage),
|
subtitle: context.translate(I18n.remoteFactoryResetMessage),
|
||||||
icon: Icons.restart_alt_outlined,
|
icon: Icons.restart_alt_outlined,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => Dialog(
|
builder: (context) => Dialog(
|
||||||
|
|||||||
@@ -177,8 +177,8 @@ class _SaveSection extends ConsumerWidget {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.submit();
|
vm.submit();
|
||||||
},
|
},
|
||||||
text: context.translate(I18n.save),
|
text: context.translate(I18n.save),
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class TimezoneScreen extends ConsumerWidget {
|
|||||||
child: PrimaryButton(
|
child: PrimaryButton(
|
||||||
onPressed: state.isSaving
|
onPressed: state.isSaving
|
||||||
? null
|
? null
|
||||||
: () {
|
: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.save();
|
vm.save();
|
||||||
},
|
},
|
||||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||||
|
|||||||
@@ -94,8 +94,8 @@ class WifiSettingsScreen extends ConsumerWidget {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
vm.scanNetworks();
|
vm.scanNetworks();
|
||||||
},
|
},
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
@@ -275,8 +275,13 @@ class _Body extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _confirmDelete(BuildContext context, WidgetRef ref, dynamic network) {
|
Future<void> _confirmDelete(
|
||||||
if (!guardDeviceCommand(context, ref)) return;
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
dynamic network,
|
||||||
|
) async {
|
||||||
|
if (!await guardDeviceCommand(context, ref)) return;
|
||||||
|
if (!context.mounted) return;
|
||||||
final theme = ref.read(themePortProvider);
|
final theme = ref.read(themePortProvider);
|
||||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,36 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:sf_localizations/sf_localizations.dart';
|
import 'package:sf_localizations/sf_localizations.dart';
|
||||||
import 'package:sf_shared/sf_shared.dart';
|
import 'package:sf_shared/sf_shared.dart';
|
||||||
|
|
||||||
bool guardDeviceCommand(BuildContext context, WidgetRef ref) {
|
const _deviceConnectionTtl = Duration(seconds: 30);
|
||||||
final device = ref.read(selectedDeviceProvider).value;
|
const _snackbarDelay = Duration(milliseconds: 400);
|
||||||
|
|
||||||
|
Future<bool> guardDeviceCommand(BuildContext context, WidgetRef ref) async {
|
||||||
|
final notifier = ref.read(legacyDevicesProvider.notifier);
|
||||||
|
|
||||||
|
if (notifier.isStale(_deviceConnectionTtl)) {
|
||||||
|
final fetchFuture = notifier.refreshIfStale(_deviceConnectionTtl);
|
||||||
|
final delayedSnackbar = Timer(_snackbarDelay, () {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
showTopSnackbar(
|
||||||
|
context,
|
||||||
|
message: context.translate(I18n.checkingDeviceConnection),
|
||||||
|
type: MessageType.info,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await fetchFuture;
|
||||||
|
delayedSnackbar.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.mounted) return false;
|
||||||
|
|
||||||
|
final device = await ref.read(selectedDeviceProvider.future);
|
||||||
if (device == null || device.isDisconnected) {
|
if (device == null || device.isDisconnected) {
|
||||||
|
if (!context.mounted) return false;
|
||||||
showTopSnackbar(
|
showTopSnackbar(
|
||||||
context,
|
context,
|
||||||
message: context.translate(I18n.errorDeviceDisconnected),
|
message: context.translate(I18n.errorDeviceDisconnected),
|
||||||
|
|||||||
@@ -691,6 +691,7 @@
|
|||||||
"errorContactsMax": "Maximale Kontaktanzahl für dieses Gerät erreicht",
|
"errorContactsMax": "Maximale Kontaktanzahl für dieses Gerät erreicht",
|
||||||
"errorPositions": "Positionen konnten nicht geladen werden",
|
"errorPositions": "Positionen konnten nicht geladen werden",
|
||||||
"errorDeviceDisconnected": "Die Uhr ist getrennt und kann keine Befehle empfangen",
|
"errorDeviceDisconnected": "Die Uhr ist getrennt und kann keine Befehle empfangen",
|
||||||
|
"checkingDeviceConnection": "Verbindung des Geräts wird überprüft...",
|
||||||
"errorSosContactsMax": "Maximale SOS-Kontaktanzahl für dieses Gerät erreicht",
|
"errorSosContactsMax": "Maximale SOS-Kontaktanzahl für dieses Gerät erreicht",
|
||||||
"customBackground": "Benutzerdefiniertes Hintergrundbild",
|
"customBackground": "Benutzerdefiniertes Hintergrundbild",
|
||||||
"backgroundImageDescription": "Legen Sie ein Foto als benutzerdefinierten Bildschirmschoner für das Gerät fest",
|
"backgroundImageDescription": "Legen Sie ein Foto als benutzerdefinierten Bildschirmschoner für das Gerät fest",
|
||||||
|
|||||||
@@ -859,6 +859,7 @@
|
|||||||
"errorContactsMax": "Maximum contacts reached for this device",
|
"errorContactsMax": "Maximum contacts reached for this device",
|
||||||
"errorPositions": "Could not load positions",
|
"errorPositions": "Could not load positions",
|
||||||
"errorDeviceDisconnected": "The watch is disconnected and cannot receive commands",
|
"errorDeviceDisconnected": "The watch is disconnected and cannot receive commands",
|
||||||
|
"checkingDeviceConnection": "Checking if the device is connected...",
|
||||||
"errorSosContactsMax": "Maximum SOS contacts reached for this device",
|
"errorSosContactsMax": "Maximum SOS contacts reached for this device",
|
||||||
"customBackground": "Custom background image",
|
"customBackground": "Custom background image",
|
||||||
"backgroundImageDescription": "Set a photo as a custom screensaver for the device",
|
"backgroundImageDescription": "Set a photo as a custom screensaver for the device",
|
||||||
|
|||||||
@@ -860,6 +860,7 @@
|
|||||||
"errorContactsMax": "Se ha alcanzado el máximo de contactos para este dispositivo",
|
"errorContactsMax": "Se ha alcanzado el máximo de contactos para este dispositivo",
|
||||||
"errorPositions": "No se pudieron cargar las posiciones",
|
"errorPositions": "No se pudieron cargar las posiciones",
|
||||||
"errorDeviceDisconnected": "El reloj está desconectado y no puede recibir comandos",
|
"errorDeviceDisconnected": "El reloj está desconectado y no puede recibir comandos",
|
||||||
|
"checkingDeviceConnection": "Revisando si el dispositivo está conectado...",
|
||||||
"errorSosContactsMax": "Se ha alcanzado el máximo de contactos SOS para este dispositivo",
|
"errorSosContactsMax": "Se ha alcanzado el máximo de contactos SOS para este dispositivo",
|
||||||
"customBackground": "Fondo de pantalla personalizado",
|
"customBackground": "Fondo de pantalla personalizado",
|
||||||
"backgroundImageDescription": "Configura una foto como protector de pantalla exclusivo para el dispositivo",
|
"backgroundImageDescription": "Configura una foto como protector de pantalla exclusivo para el dispositivo",
|
||||||
|
|||||||
@@ -691,6 +691,7 @@
|
|||||||
"errorContactsMax": "Nombre maximum de contacts atteint pour cet appareil",
|
"errorContactsMax": "Nombre maximum de contacts atteint pour cet appareil",
|
||||||
"errorPositions": "Impossible de charger les positions",
|
"errorPositions": "Impossible de charger les positions",
|
||||||
"errorDeviceDisconnected": "La montre est déconnectée et ne peut pas recevoir de commandes",
|
"errorDeviceDisconnected": "La montre est déconnectée et ne peut pas recevoir de commandes",
|
||||||
|
"checkingDeviceConnection": "Vérification de la connexion de l'appareil...",
|
||||||
"errorSosContactsMax": "Nombre maximum de contacts SOS atteint pour cet appareil",
|
"errorSosContactsMax": "Nombre maximum de contacts SOS atteint pour cet appareil",
|
||||||
"customBackground": "Image de fond personnalisée",
|
"customBackground": "Image de fond personnalisée",
|
||||||
"backgroundImageDescription": "Définissez une photo comme écran de veille personnalisé pour l'appareil",
|
"backgroundImageDescription": "Définissez une photo comme écran de veille personnalisé pour l'appareil",
|
||||||
|
|||||||
@@ -691,6 +691,7 @@
|
|||||||
"errorContactsMax": "Numero massimo di contatti raggiunto per questo dispositivo",
|
"errorContactsMax": "Numero massimo di contatti raggiunto per questo dispositivo",
|
||||||
"errorPositions": "Impossibile caricare le posizioni",
|
"errorPositions": "Impossibile caricare le posizioni",
|
||||||
"errorDeviceDisconnected": "L'orologio è disconnesso e non può ricevere comandi",
|
"errorDeviceDisconnected": "L'orologio è disconnesso e non può ricevere comandi",
|
||||||
|
"checkingDeviceConnection": "Verifica della connessione del dispositivo...",
|
||||||
"errorSosContactsMax": "Numero massimo di contatti SOS raggiunto per questo dispositivo",
|
"errorSosContactsMax": "Numero massimo di contatti SOS raggiunto per questo dispositivo",
|
||||||
"customBackground": "Immagine di sfondo personalizzata",
|
"customBackground": "Immagine di sfondo personalizzata",
|
||||||
"backgroundImageDescription": "Imposta una foto come screensaver personalizzato per il dispositivo",
|
"backgroundImageDescription": "Imposta una foto come screensaver personalizzato per il dispositivo",
|
||||||
|
|||||||
@@ -691,6 +691,7 @@
|
|||||||
"errorContactsMax": "Número máximo de contactos atingido para este dispositivo",
|
"errorContactsMax": "Número máximo de contactos atingido para este dispositivo",
|
||||||
"errorPositions": "Não foi possível carregar as posições",
|
"errorPositions": "Não foi possível carregar as posições",
|
||||||
"errorDeviceDisconnected": "O relógio está desconectado e não pode receber comandos",
|
"errorDeviceDisconnected": "O relógio está desconectado e não pode receber comandos",
|
||||||
|
"checkingDeviceConnection": "A verificar se o dispositivo está conectado...",
|
||||||
"errorSosContactsMax": "Número máximo de contactos SOS atingido para este dispositivo",
|
"errorSosContactsMax": "Número máximo de contactos SOS atingido para este dispositivo",
|
||||||
"customBackground": "Imagem de fundo personalizada",
|
"customBackground": "Imagem de fundo personalizada",
|
||||||
"backgroundImageDescription": "Defina uma foto como protetor de ecrã personalizado para o dispositivo",
|
"backgroundImageDescription": "Defina uma foto como protetor de ecrã personalizado para o dispositivo",
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ class I18n {
|
|||||||
static const String errorContactsMax = 'errorContactsMax';
|
static const String errorContactsMax = 'errorContactsMax';
|
||||||
static const String errorPositions = 'errorPositions';
|
static const String errorPositions = 'errorPositions';
|
||||||
static const String errorDeviceDisconnected = 'errorDeviceDisconnected';
|
static const String errorDeviceDisconnected = 'errorDeviceDisconnected';
|
||||||
|
static const String checkingDeviceConnection = 'checkingDeviceConnection';
|
||||||
static const String errorSosContactsMax = 'errorSosContactsMax';
|
static const String errorSosContactsMax = 'errorSosContactsMax';
|
||||||
static const String errorContactsMin = 'errorContactsMin';
|
static const String errorContactsMin = 'errorContactsMin';
|
||||||
static const String errorDisableFunctions = 'errorDisableFunctions';
|
static const String errorDisableFunctions = 'errorDisableFunctions';
|
||||||
|
|||||||
@@ -2,9 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:sf_shared/sf_shared.dart';
|
import 'package:sf_shared/sf_shared.dart';
|
||||||
|
|
||||||
class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
|
class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
|
||||||
|
DateTime? _lastFetchedAt;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<DeviceEntity>> build() {
|
Future<List<DeviceEntity>> build() async {
|
||||||
return ref.read(sharedDevicesRepositoryProvider).getDevices();
|
final devices = await ref.read(sharedDevicesRepositoryProvider).getDevices();
|
||||||
|
_lastFetchedAt = DateTime.now();
|
||||||
|
return devices;
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeDevice(String deviceId) {
|
void removeDevice(String deviceId) {
|
||||||
@@ -30,6 +34,21 @@ class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
|
|||||||
state = await AsyncValue.guard(
|
state = await AsyncValue.guard(
|
||||||
() => ref.read(sharedDevicesRepositoryProvider).getDevices(),
|
() => ref.read(sharedDevicesRepositoryProvider).getDevices(),
|
||||||
);
|
);
|
||||||
|
_lastFetchedAt = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isStale(Duration ttl) {
|
||||||
|
final last = _lastFetchedAt;
|
||||||
|
return last == null || DateTime.now().difference(last) >= ttl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshIfStale(Duration ttl) async {
|
||||||
|
if (!isStale(ttl)) return;
|
||||||
|
final result = await AsyncValue.guard(
|
||||||
|
() => ref.read(sharedDevicesRepositoryProvider).getDevices(),
|
||||||
|
);
|
||||||
|
state = result;
|
||||||
|
_lastFetchedAt = DateTime.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user