From 72d44b81dfa79ffc2cae4819aca62316999a6782 Mon Sep 17 00:00:00 2001 From: JulianAlcala Date: Fri, 17 Apr 2026 09:44:29 +0200 Subject: [PATCH] feat(legacy): add pull-to-refresh for control_panel and location Introduce a shared RefreshableErrorState widget that wraps the retry hint in a RefreshIndicator with an explicit 'pull down to retry' caption, so users can recover from load failures without navigating away. Wire it into the location screen's error fallbacks and make the control_panel body pull-to-refresh at any time, invalidating the device list so the dashboard picks up fresh data. --- .../presentation/control_panel_screen.dart | 54 ++++++++------ .../presentation/location_screen.dart | 28 +++---- .../legacy_shared/lib/legacy_shared.dart | 1 + .../src/widgets/refreshable_error_state.dart | 73 +++++++++++++++++++ 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 + 11 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 modules/legacy/packages/legacy_shared/lib/src/widgets/refreshable_error_state.dart diff --git a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart index babb76d4..a691fcb4 100644 --- a/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart +++ b/modules/legacy/modules/control_panel/lib/src/features/control_panel/presentation/control_panel_screen.dart @@ -41,14 +41,12 @@ class ControlPanelScreen extends ConsumerWidget { body: asyncState.when( skipLoadingOnReload: true, loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Text( - formatErrorMessage(error), - textAlign: TextAlign.center, - ), - ), + error: (error, _) => RefreshableErrorState( + message: formatErrorMessage(error), + onRefresh: () async { + ref.invalidate(controlPanelViewModelProvider); + await ref.read(controlPanelViewModelProvider.future); + }, ), data: (state) => SafeArea( child: Container( @@ -62,22 +60,30 @@ class ControlPanelScreen extends ConsumerWidget { ), SizedBox(height: SizeUtils.getByScreen(small: 12, big: 14)), Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _MenuSection(navigationContract: navigationContract), - SizedBox( - height: SizeUtils.getByScreen(small: 16, big: 22), - ), - _MapSection( - state: state, - navigationContract: navigationContract, - ), - SizedBox( - height: SizeUtils.getByScreen(small: 14, big: 13), - ), - ], + child: RefreshIndicator( + color: theme.getColorFor(ThemeCode.legacyPrimary), + onRefresh: () async { + ref.invalidate(legacyDevicesProvider); + await ref.read(controlPanelViewModelProvider.future); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _MenuSection(navigationContract: navigationContract), + SizedBox( + height: SizeUtils.getByScreen(small: 16, big: 22), + ), + _MapSection( + state: state, + navigationContract: navigationContract, + ), + SizedBox( + height: SizeUtils.getByScreen(small: 14, big: 13), + ), + ], + ), ), ), ), diff --git a/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart b/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart index e77f2daa..7290445f 100644 --- a/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart +++ b/modules/legacy/modules/location/lib/src/features/location/presentation/location_screen.dart @@ -92,26 +92,21 @@ class LocationScreen extends ConsumerWidget { body: asyncControlPanelState.when( skipLoadingOnReload: true, loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Text( - context.translate(I18n.errorGeneric), - textAlign: TextAlign.center, - ), - ), + error: (error, _) => RefreshableErrorState( + onRefresh: () async { + ref.invalidate(controlPanelViewModelProvider); + ref.invalidate(locationViewModelProvider); + await ref.read(controlPanelViewModelProvider.future); + }, ), data: (controlPanelState) => asyncLocationState.when( skipLoadingOnReload: true, loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center( - child: Padding( - padding: const EdgeInsets.all(24), - child: Text( - context.translate(I18n.errorGeneric), - textAlign: TextAlign.center, - ), - ), + error: (error, _) => RefreshableErrorState( + onRefresh: () async { + ref.invalidate(locationViewModelProvider); + await ref.read(locationViewModelProvider.future); + }, ), data: (locationState) => LocationMap( selectedPosition: controlPanelState.selectedPosition, @@ -139,3 +134,4 @@ class LocationScreen extends ConsumerWidget { ); } } + diff --git a/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart b/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart index a0489544..f598b02f 100644 --- a/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart +++ b/modules/legacy/packages/legacy_shared/lib/legacy_shared.dart @@ -2,6 +2,7 @@ export 'src/providers/map_style_provider.dart'; export 'src/widgets/layouts/page_layout.dart'; export 'src/components/section_button.dart'; export 'src/widgets/pulsing_location_marker.dart'; +export 'src/widgets/refreshable_error_state.dart'; export 'src/widgets/week_day_chips.dart'; export 'src/components/menu_button.dart'; export 'src/data/models/device_response_model.dart'; diff --git a/modules/legacy/packages/legacy_shared/lib/src/widgets/refreshable_error_state.dart b/modules/legacy/packages/legacy_shared/lib/src/widgets/refreshable_error_state.dart new file mode 100644 index 00000000..6743af65 --- /dev/null +++ b/modules/legacy/packages/legacy_shared/lib/src/widgets/refreshable_error_state.dart @@ -0,0 +1,73 @@ +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'; + +class RefreshableErrorState extends ConsumerWidget { + const RefreshableErrorState({ + super.key, + required this.onRefresh, + this.message, + }); + + final Future Function() onRefresh; + final String? message; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = ref.watch(themePortProvider); + final primaryColor = theme.getColorFor(ThemeCode.legacyPrimary); + final textColor = theme.getColorFor(ThemeCode.textPrimary); + final errorMessage = message ?? context.translate(I18n.errorGeneric); + + return RefreshIndicator( + onRefresh: onRefresh, + color: primaryColor, + child: LayoutBuilder( + builder: (context, constraints) => SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: constraints.maxHeight), + child: Padding( + padding: const EdgeInsets.all(24), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.cloud_off_outlined, + size: 56, + color: textColor.withValues(alpha: 0.4), + ), + const SizedBox(height: 16), + Text( + errorMessage, + textAlign: TextAlign.center, + style: TextStyle(fontSize: 15, color: textColor), + ), + const SizedBox(height: 24), + Icon( + Icons.arrow_downward_rounded, + size: 20, + color: primaryColor, + ), + const SizedBox(height: 6), + Text( + context.translate(I18n.pullDownToRetry), + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: primaryColor, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/packages/sf_localizations/assets/l10n/de.json b/packages/sf_localizations/assets/l10n/de.json index 273cc6de..82e9709a 100644 --- a/packages/sf_localizations/assets/l10n/de.json +++ b/packages/sf_localizations/assets/l10n/de.json @@ -668,6 +668,7 @@ "frequentPlaceUpdated": "Häufiger Ort aktualisiert", "frequentPlaceDeleted": "Häufiger Ort gelöscht", "errorGeneric": "Etwas ist schiefgelaufen. Bitte versuchen Sie es erneut.", + "pullDownToRetry": "Zum Wiederholen nach unten ziehen", "errorGeofenceCreate": "Die Sicherheitszone konnte nicht erstellt werden", "errorGeofenceUpdate": "Die Sicherheitszone konnte nicht aktualisiert werden", "errorGeofenceDelete": "Die Sicherheitszone konnte nicht gelöscht werden", diff --git a/packages/sf_localizations/assets/l10n/en.json b/packages/sf_localizations/assets/l10n/en.json index 2edaee69..463466e7 100755 --- a/packages/sf_localizations/assets/l10n/en.json +++ b/packages/sf_localizations/assets/l10n/en.json @@ -836,6 +836,7 @@ "frequentPlaceUpdated": "Frequent place updated", "frequentPlaceDeleted": "Frequent place deleted", "errorGeneric": "Something went wrong. Please try again.", + "pullDownToRetry": "Pull down to retry", "errorGeofenceCreate": "Could not create the safety zone", "errorGeofenceUpdate": "Could not update the safety zone", "errorGeofenceDelete": "Could not delete the safety zone", diff --git a/packages/sf_localizations/assets/l10n/es.json b/packages/sf_localizations/assets/l10n/es.json index 17c7faa6..88cb13d3 100644 --- a/packages/sf_localizations/assets/l10n/es.json +++ b/packages/sf_localizations/assets/l10n/es.json @@ -837,6 +837,7 @@ "frequentPlaceUpdated": "Lugar frecuente actualizado", "frequentPlaceDeleted": "Lugar frecuente eliminado", "errorGeneric": "Algo salió mal. Inténtalo de nuevo.", + "pullDownToRetry": "Desliza hacia abajo para reintentar", "errorGeofenceCreate": "No se pudo crear la zona de seguridad", "errorGeofenceUpdate": "No se pudo actualizar la zona de seguridad", "errorGeofenceDelete": "No se pudo eliminar la zona de seguridad", diff --git a/packages/sf_localizations/assets/l10n/fr.json b/packages/sf_localizations/assets/l10n/fr.json index 5d923365..ad4eb1b1 100644 --- a/packages/sf_localizations/assets/l10n/fr.json +++ b/packages/sf_localizations/assets/l10n/fr.json @@ -668,6 +668,7 @@ "frequentPlaceUpdated": "Lieu fréquent mis à jour", "frequentPlaceDeleted": "Lieu fréquent supprimé", "errorGeneric": "Une erreur est survenue. Veuillez réessayer.", + "pullDownToRetry": "Tirez vers le bas pour réessayer", "errorGeofenceCreate": "Impossible de créer la zone de sécurité", "errorGeofenceUpdate": "Impossible de mettre à jour la zone de sécurité", "errorGeofenceDelete": "Impossible de supprimer la zone de sécurité", diff --git a/packages/sf_localizations/assets/l10n/it.json b/packages/sf_localizations/assets/l10n/it.json index 3a25d000..c2d90125 100644 --- a/packages/sf_localizations/assets/l10n/it.json +++ b/packages/sf_localizations/assets/l10n/it.json @@ -668,6 +668,7 @@ "frequentPlaceUpdated": "Luogo frequente aggiornato", "frequentPlaceDeleted": "Luogo frequente eliminato", "errorGeneric": "Qualcosa è andato storto. Riprova.", + "pullDownToRetry": "Trascina verso il basso per riprovare", "errorGeofenceCreate": "Impossibile creare la zona di sicurezza", "errorGeofenceUpdate": "Impossibile aggiornare la zona di sicurezza", "errorGeofenceDelete": "Impossibile eliminare la zona di sicurezza", diff --git a/packages/sf_localizations/assets/l10n/pt.json b/packages/sf_localizations/assets/l10n/pt.json index 29e52ba9..1a5b4eb8 100644 --- a/packages/sf_localizations/assets/l10n/pt.json +++ b/packages/sf_localizations/assets/l10n/pt.json @@ -668,6 +668,7 @@ "frequentPlaceUpdated": "Local frequente atualizado", "frequentPlaceDeleted": "Local frequente eliminado", "errorGeneric": "Algo correu mal. Tente novamente.", + "pullDownToRetry": "Deslize para baixo para tentar novamente", "errorGeofenceCreate": "Não foi possível criar a zona de segurança", "errorGeofenceUpdate": "Não foi possível atualizar a zona de segurança", "errorGeofenceDelete": "Não foi possível eliminar a zona de segurança", diff --git a/packages/sf_localizations/lib/src/generated/i18n.dart b/packages/sf_localizations/lib/src/generated/i18n.dart index bc39f250..1f552bee 100755 --- a/packages/sf_localizations/lib/src/generated/i18n.dart +++ b/packages/sf_localizations/lib/src/generated/i18n.dart @@ -413,6 +413,7 @@ class I18n { static const String errorFrequentPlaceDelete = 'errorFrequentPlaceDelete'; static const String errorFrequentPlaceUpdate = 'errorFrequentPlaceUpdate'; static const String errorGeneric = 'errorGeneric'; + static const String pullDownToRetry = 'pullDownToRetry'; static const String errorGeofenceCreate = 'errorGeofenceCreate'; static const String errorGeofenceDelete = 'errorGeofenceDelete'; static const String errorGeofenceUpdate = 'errorGeofenceUpdate';