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,
|
||||
activeTrackColor: theme.getColorFor(ThemeCode.legacyPrimary),
|
||||
onChanged: (value) async {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
final success = await ref
|
||||
.read(activityMeterViewModelProvider.notifier)
|
||||
.togglePedometer(enabled: value);
|
||||
|
||||
@@ -97,8 +97,8 @@ class BackgroundImageScreen extends ConsumerWidget {
|
||||
child: IconButton(
|
||||
onPressed: state.isSaving
|
||||
? null
|
||||
: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.uploadPhoto();
|
||||
},
|
||||
icon: Icon(
|
||||
@@ -119,16 +119,16 @@ class BackgroundImageScreen extends ConsumerWidget {
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: state.photos.isEmpty
|
||||
? _EmptyState(
|
||||
onUpload: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onUpload: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.uploadPhoto();
|
||||
},
|
||||
primaryColor: primaryColor,
|
||||
)
|
||||
: _PhotoGrid(
|
||||
state: state,
|
||||
onPhotoTap: (id) {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPhotoTap: (id) async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.setAsBackground(id);
|
||||
},
|
||||
primaryColor: primaryColor,
|
||||
|
||||
@@ -58,8 +58,9 @@ class ContactsScreen extends ConsumerWidget {
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onTap: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
if (state.contacts.length >= state.maxContacts) {
|
||||
showTopSnackbar(
|
||||
context,
|
||||
|
||||
@@ -155,8 +155,8 @@ class DoNotDisturbScreen extends ConsumerWidget {
|
||||
child: isSaving
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.save();
|
||||
},
|
||||
text: context.translate(I18n.doNotDisturbSave),
|
||||
|
||||
@@ -110,7 +110,7 @@ class _HealthScreenState extends ConsumerState<HealthScreen>
|
||||
options: device.capabilities!.heartbeats!.options,
|
||||
theme: theme,
|
||||
onChanged: (frequency) async {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
final success = await vm.updateHeartRateFrequency(
|
||||
frequency: frequency,
|
||||
);
|
||||
@@ -199,8 +199,8 @@ class _SaveSection extends ConsumerWidget {
|
||||
child: PrimaryButton(
|
||||
onPressed: isMeasuring
|
||||
? null
|
||||
: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.measure();
|
||||
},
|
||||
text: isMeasuring ? '...' : context.translate(I18n.measure),
|
||||
|
||||
@@ -90,8 +90,8 @@ class LocateDeviceDialog extends ConsumerWidget {
|
||||
SizedBox(width: SizeUtils.getByScreen(small: 8, big: 16)),
|
||||
Expanded(
|
||||
child: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.locateDevice();
|
||||
},
|
||||
text: context.translate(I18n.accept),
|
||||
|
||||
@@ -152,8 +152,8 @@ class _TakePictureSection extends ConsumerWidget {
|
||||
big: EdgeInsets.symmetric(vertical: 10, horizontal: 25),
|
||||
),
|
||||
child: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.takePicture();
|
||||
},
|
||||
text: context.translate(I18n.takePicture),
|
||||
|
||||
@@ -33,8 +33,9 @@ class RemoteConnectionScreen extends ConsumerWidget {
|
||||
children: [
|
||||
if (cameraEnabled) ...[
|
||||
_SectionButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
@@ -50,8 +51,9 @@ class RemoteConnectionScreen extends ConsumerWidget {
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 16, big: 15)),
|
||||
],
|
||||
_SectionButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(child: SpyCallDialog()),
|
||||
|
||||
@@ -77,13 +77,13 @@ class SpyCallDialog extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 12, big: 10)),
|
||||
_PhoneSection(onSubmit: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
_PhoneSection(onSubmit: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.call();
|
||||
}),
|
||||
SizedBox(height: SizeUtils.getByScreen(small: 28, big: 27)),
|
||||
_CallSection(onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
_CallSection(onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.call();
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -104,8 +104,9 @@ class _ScheduledActivitiesScreenState
|
||||
shape: const CircleBorder(),
|
||||
child: InkWell(
|
||||
customBorder: const CircleBorder(),
|
||||
onTap: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onTap: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showActivityFormSheet(context, weekDay: _selectedWeekDay);
|
||||
},
|
||||
child: SizedBox(
|
||||
|
||||
@@ -97,9 +97,10 @@ class _ActivityFormSheetState extends ConsumerState<ActivityFormSheet> {
|
||||
});
|
||||
}
|
||||
|
||||
void _submit() {
|
||||
Future<void> _submit() async {
|
||||
if (!_isFormValid) return;
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!mounted) return;
|
||||
|
||||
final name = _nameController.text.trim();
|
||||
final period = _buildPeriod();
|
||||
|
||||
@@ -188,8 +188,9 @@ class _ActivityTimelineCard extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showActivityFormSheet(context, activity: activity);
|
||||
},
|
||||
icon: Icon(
|
||||
@@ -201,8 +202,9 @@ class _ActivityTimelineCard extends ConsumerWidget {
|
||||
padding: EdgeInsets.all(SizeUtils.getByScreen(small: 8, big: 6)),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => Dialog(
|
||||
|
||||
@@ -81,8 +81,8 @@ class VolumeControlScreen extends ConsumerWidget {
|
||||
child: state.isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.submit();
|
||||
},
|
||||
text: context.translate(I18n.volumeSend),
|
||||
|
||||
@@ -75,7 +75,7 @@ class _FrequentPlaceSheetState extends ConsumerState<_FrequentPlaceSheet> {
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_canSave) return;
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
|
||||
final vm = ref.read(locationViewModelProvider.notifier);
|
||||
final bool success;
|
||||
|
||||
@@ -91,7 +91,7 @@ class _GeofenceSheetState extends ConsumerState<_GeofenceSheet> {
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (!_canSave) return;
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
|
||||
final vm = ref.read(locationViewModelProvider.notifier);
|
||||
final description = _descriptionController.text.trim();
|
||||
|
||||
@@ -237,7 +237,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
}
|
||||
|
||||
Future<void> _updateFrequency(int frequency) async {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
final success = await ref
|
||||
.read(locationViewModelProvider.notifier)
|
||||
.updateLocationFrequency(frequency: frequency);
|
||||
@@ -264,8 +264,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
}
|
||||
}
|
||||
|
||||
void _confirmPlacement() {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
Future<void> _confirmPlacement() async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!mounted) return;
|
||||
final center = _mapController.camera.center;
|
||||
final mapState = ref.read(locationMapViewModelProvider);
|
||||
|
||||
@@ -289,8 +290,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
}
|
||||
}
|
||||
|
||||
void _confirmRadius() {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
Future<void> _confirmRadius() async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!mounted) return;
|
||||
final mapState = ref.read(locationMapViewModelProvider);
|
||||
final point = mapState.previewPoint!;
|
||||
final radius = mapState.previewRadius;
|
||||
@@ -414,8 +416,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
}
|
||||
}
|
||||
|
||||
void _onEditFrequentPlace(FrequentPlaceEntity fp) {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
Future<void> _onEditFrequentPlace(FrequentPlaceEntity fp) async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!mounted) return;
|
||||
_vm.clearSelectedFrequentPlace();
|
||||
showNameInputSheet(
|
||||
context,
|
||||
@@ -673,12 +676,13 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
onAddGeofence: () => _vm.startPlacing(PlacingMode.geofence),
|
||||
onAddFrequentPlace: () => _vm.startPlacing(PlacingMode.frequentPlace),
|
||||
onShareTap: _shareLocation,
|
||||
onRefreshTap: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onRefreshTap: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
unawaited(
|
||||
ref.read(sfTrackingProvider).legacyLocationMapRefreshTapped(),
|
||||
);
|
||||
widget.onRefreshPosition();
|
||||
if (!mounted) return;
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.locationMapRefreshRequested),
|
||||
@@ -686,9 +690,9 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
);
|
||||
},
|
||||
onCenterTap: _centerOnDevice,
|
||||
onToggleFollow: () {
|
||||
onToggleFollow: () async {
|
||||
final willActivate = !mapState.isFollowing;
|
||||
if (willActivate && !guardDeviceCommand(context, ref)) return;
|
||||
if (willActivate && !await guardDeviceCommand(context, ref)) return;
|
||||
_vm.toggleFollowing();
|
||||
unawaited(
|
||||
ref
|
||||
@@ -698,6 +702,7 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
if (willActivate && widget.selectedPosition != null) {
|
||||
_centerOnDevice();
|
||||
}
|
||||
if (!mounted) return;
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(
|
||||
@@ -721,12 +726,12 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
child: GeofenceInfoCard(
|
||||
geofence: mapState.selectedGeofence!,
|
||||
onClose: _vm.clearSelectedGeofence,
|
||||
onEdit: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onEdit: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
_vm.startEditingGeofence(mapState.selectedGeofence!);
|
||||
},
|
||||
onDelete: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onDelete: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
final id = mapState.selectedGeofence!.id;
|
||||
_vm.clearSelectedGeofence();
|
||||
ref
|
||||
@@ -742,8 +747,8 @@ class _LocationMapState extends ConsumerState<LocationMap>
|
||||
frequentPlace: mapState.selectedFrequentPlace!,
|
||||
onClose: _vm.clearSelectedFrequentPlace,
|
||||
onEdit: () => _onEditFrequentPlace(mapState.selectedFrequentPlace!),
|
||||
onDelete: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onDelete: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
final id = mapState.selectedFrequentPlace!.id;
|
||||
_vm.clearSelectedFrequentPlace();
|
||||
ref
|
||||
|
||||
@@ -92,8 +92,9 @@ class AlarmScreen extends ConsumerWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
_openForm(context, vm);
|
||||
},
|
||||
icon: Icon(
|
||||
@@ -126,8 +127,8 @@ class AlarmScreen extends ConsumerWidget {
|
||||
child: isSaving
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.save();
|
||||
},
|
||||
text: context.translate(I18n.alarmSave),
|
||||
|
||||
@@ -81,8 +81,8 @@ class AlertsScreen extends ConsumerWidget {
|
||||
child: PrimaryButton(
|
||||
onPressed: state.isSaving
|
||||
? null
|
||||
: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.save();
|
||||
},
|
||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||
|
||||
@@ -99,8 +99,8 @@ class BatteryScreen extends ConsumerWidget {
|
||||
activeTrackColor: primaryColor,
|
||||
onChanged: state.isSaving
|
||||
? null
|
||||
: (value) {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
: (value) async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.toggleNightMode(value);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -84,8 +84,9 @@ class BlockPhoneScreen extends ConsumerWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showAddContactSheet(context);
|
||||
},
|
||||
icon: Icon(
|
||||
@@ -216,13 +217,14 @@ class _ContactList extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(
|
||||
Future<void> _confirmDelete(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
int index,
|
||||
String name,
|
||||
) {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
) async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
final theme = ref.read(themePortProvider);
|
||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
||||
|
||||
|
||||
@@ -78,8 +78,8 @@ class DisableFunctionsScreen extends ConsumerWidget {
|
||||
child: PrimaryButton(
|
||||
onPressed: state.isSaving
|
||||
? null
|
||||
: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.save();
|
||||
},
|
||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||
|
||||
@@ -110,8 +110,8 @@ class _SaveSection extends ConsumerWidget {
|
||||
child: isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.submit();
|
||||
},
|
||||
text: context.translate(I18n.save),
|
||||
|
||||
@@ -51,8 +51,9 @@ class _OptionsSection extends ConsumerWidget {
|
||||
title: context.translate(I18n.remoteTurnOff),
|
||||
subtitle: context.translate(I18n.remoteTurnOffMessage),
|
||||
icon: Icons.settings_power_outlined,
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
@@ -95,8 +96,9 @@ class _OptionsSection extends ConsumerWidget {
|
||||
title: context.translate(I18n.remoteRestart),
|
||||
subtitle: context.translate(I18n.remoteRestartMessage),
|
||||
icon: Icons.refresh_outlined,
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
@@ -117,8 +119,9 @@ class _OptionsSection extends ConsumerWidget {
|
||||
title: context.translate(I18n.remoteFactoryReset),
|
||||
subtitle: context.translate(I18n.remoteFactoryResetMessage),
|
||||
icon: Icons.restart_alt_outlined,
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => Dialog(
|
||||
|
||||
@@ -177,8 +177,8 @@ class _SaveSection extends ConsumerWidget {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: PrimaryButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.submit();
|
||||
},
|
||||
text: context.translate(I18n.save),
|
||||
|
||||
@@ -96,8 +96,8 @@ class TimezoneScreen extends ConsumerWidget {
|
||||
child: PrimaryButton(
|
||||
onPressed: state.isSaving
|
||||
? null
|
||||
: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.save();
|
||||
},
|
||||
text: state.isSaving ? '...' : context.translate(I18n.save),
|
||||
|
||||
@@ -94,8 +94,8 @@ class WifiSettingsScreen extends ConsumerWidget {
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
onPressed: () async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
vm.scanNetworks();
|
||||
},
|
||||
icon: Icon(
|
||||
@@ -275,8 +275,13 @@ class _Body extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, WidgetRef ref, dynamic network) {
|
||||
if (!guardDeviceCommand(context, ref)) return;
|
||||
Future<void> _confirmDelete(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
dynamic network,
|
||||
) async {
|
||||
if (!await guardDeviceCommand(context, ref)) return;
|
||||
if (!context.mounted) return;
|
||||
final theme = ref.read(themePortProvider);
|
||||
final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary);
|
||||
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import 'dart:async';
|
||||
|
||||
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';
|
||||
|
||||
bool guardDeviceCommand(BuildContext context, WidgetRef ref) {
|
||||
final device = ref.read(selectedDeviceProvider).value;
|
||||
const _deviceConnectionTtl = Duration(seconds: 30);
|
||||
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 (!context.mounted) return false;
|
||||
showTopSnackbar(
|
||||
context,
|
||||
message: context.translate(I18n.errorDeviceDisconnected),
|
||||
|
||||
@@ -691,6 +691,7 @@
|
||||
"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",
|
||||
"checkingDeviceConnection": "Verbindung des Geräts wird überprüft...",
|
||||
"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",
|
||||
|
||||
@@ -859,6 +859,7 @@
|
||||
"errorContactsMax": "Maximum contacts reached for this device",
|
||||
"errorPositions": "Could not load positions",
|
||||
"errorDeviceDisconnected": "The watch is disconnected and cannot receive commands",
|
||||
"checkingDeviceConnection": "Checking if the device is connected...",
|
||||
"errorSosContactsMax": "Maximum SOS contacts reached for this device",
|
||||
"customBackground": "Custom background image",
|
||||
"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",
|
||||
"errorPositions": "No se pudieron cargar las posiciones",
|
||||
"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",
|
||||
"customBackground": "Fondo de pantalla personalizado",
|
||||
"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",
|
||||
"errorPositions": "Impossible de charger les positions",
|
||||
"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",
|
||||
"customBackground": "Image de fond personnalisée",
|
||||
"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",
|
||||
"errorPositions": "Impossibile caricare le posizioni",
|
||||
"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",
|
||||
"customBackground": "Immagine di sfondo personalizzata",
|
||||
"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",
|
||||
"errorPositions": "Não foi possível carregar as posições",
|
||||
"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",
|
||||
"customBackground": "Imagem de fundo personalizada",
|
||||
"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 errorPositions = 'errorPositions';
|
||||
static const String errorDeviceDisconnected = 'errorDeviceDisconnected';
|
||||
static const String checkingDeviceConnection = 'checkingDeviceConnection';
|
||||
static const String errorSosContactsMax = 'errorSosContactsMax';
|
||||
static const String errorContactsMin = 'errorContactsMin';
|
||||
static const String errorDisableFunctions = 'errorDisableFunctions';
|
||||
|
||||
@@ -2,9 +2,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:sf_shared/sf_shared.dart';
|
||||
|
||||
class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
|
||||
DateTime? _lastFetchedAt;
|
||||
|
||||
@override
|
||||
Future<List<DeviceEntity>> build() {
|
||||
return ref.read(sharedDevicesRepositoryProvider).getDevices();
|
||||
Future<List<DeviceEntity>> build() async {
|
||||
final devices = await ref.read(sharedDevicesRepositoryProvider).getDevices();
|
||||
_lastFetchedAt = DateTime.now();
|
||||
return devices;
|
||||
}
|
||||
|
||||
void removeDevice(String deviceId) {
|
||||
@@ -30,6 +34,21 @@ class LegacyDevices extends AsyncNotifier<List<DeviceEntity>> {
|
||||
state = await AsyncValue.guard(
|
||||
() => 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