From 2ecd1c5b4e2b22cec6752e743e8ccb853b7d0a0a Mon Sep 17 00:00:00 2001 From: Mathias Beaulieu-Duncan Date: Wed, 26 Nov 2025 17:41:37 -0500 Subject: [PATCH] checkpoint --- DEVELOPMENT.md | 276 ++++++++++++++++++ .../collapsible_routes_sidebar.dart | 6 +- lib/components/delivery_list_item.dart | 4 +- lib/components/route_list_item.dart | 4 +- lib/l10n/app_en.arb | 27 +- lib/l10n/app_fr.arb | 77 +++-- lib/l10n/app_localizations.dart | 114 ++++++++ lib/l10n/app_localizations_en.dart | 59 ++++ lib/l10n/app_localizations_fr.dart | 109 +++++-- lib/main.dart | 76 +++-- lib/pages/deliveries_page.dart | 54 ++-- lib/pages/login_page.dart | 25 +- lib/pages/routes_page.dart | 32 +- lib/pages/settings_page.dart | 61 ++-- lib/providers/providers.dart | 22 +- 15 files changed, 794 insertions(+), 152 deletions(-) create mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..4ae355f --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,276 @@ +# Development Guide - Plan B Logistics Flutter App + +This guide covers building and running the Plan B Logistics Flutter app on Android devices (KM10) with local backend communication. + +## Prerequisites + +- Flutter SDK installed and configured +- Android SDK installed (with platform-tools for adb) +- Android device (KM10) connected via USB with USB debugging enabled +- Backend API running on localhost (Mac) + +## Device Setup + +### 1. Verify Device Connection + +Check that your Android device is connected and recognized: + +```bash +flutter devices +``` + +You should see output similar to: +``` +KM10 (mobile) • 24117ad4 • android-arm64 • Android 13 (API 33) +``` + +Alternatively, use adb directly: +```bash +/Users/mathias/Library/Android/sdk/platform-tools/adb devices -l +``` + +### 2. Configure ADB Reverse Proxy + +The app needs to communicate with your local backend API running on `localhost:7182`. Since the Android device cannot access your Mac's localhost directly, you need to set up a reverse proxy using adb. + +#### Get Device Serial Number + +First, identify your device's serial number: +```bash +/Users/mathias/Library/Android/sdk/platform-tools/adb devices +``` + +Example output: +``` +24117ad4 device +``` + +#### Set Up Reverse Proxy + +Forward the device's localhost:7182 to your Mac's localhost:7182: + +```bash +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182 +``` + +Replace `24117ad4` with your actual device serial number. + +#### Verify Reverse Proxy + +Check that the reverse proxy is active: +```bash +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list +``` + +Expected output: +``` +tcp:7182 tcp:7182 +``` + +#### Test Backend Connectivity (Optional) + +From the device shell, test if the backend is accessible: +```bash +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell "curl -k https://localhost:7182/api/query/simpleDeliveryRouteQueryItems -X POST -H 'Content-Type: application/json' -d '{}' -m 5 2>&1 | head -10" +``` + +## Building and Running + +### 1. Install Dependencies + +```bash +flutter pub get +``` + +### 2. Run on Connected Device + +Run the app on the KM10 device in debug mode: + +```bash +flutter run -d KM10 +``` + +Or using the device serial number: +```bash +flutter run -d 24117ad4 +``` + +### 3. Hot Reload and Restart + +While the app is running: +- **Hot Reload** (r): Reload changed code without restarting +- **Hot Restart** (R): Restart the entire app +- **Quit** (q): Stop the app + +### 4. Build Release APK + +To build a release APK: + +```bash +flutter build apk --release +``` + +The APK will be located at: +``` +build/app/outputs/flutter-apk/app-release.apk +``` + +## Development Workflow + +### Full Development Session + +```bash +# 1. Check device connection +flutter devices + +# 2. Set up ADB reverse proxy (do this once per device connection) +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182 + +# 3. Verify reverse proxy +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list + +# 4. Run the app +flutter run -d KM10 +``` + +### If Backend Connection Fails + +If the app cannot connect to the backend API: + +1. Verify backend is running on Mac: + ```bash + curl https://localhost:7182/api/query/simpleDeliveryRouteQueryItems \ + -X POST \ + -H 'Content-Type: application/json' \ + -d '{}' + ``` + +2. Check ADB reverse proxy is active: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --list + ``` + +3. Re-establish reverse proxy if needed: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse --remove-all + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 reverse tcp:7182 tcp:7182 + ``` + +4. Check device logs for errors: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -d -t 100 + ``` + +## API Configuration + +The app is configured to use the following API endpoints: + +- **Query Base URL**: `https://localhost:7182/api/query` +- **Command Base URL**: `https://localhost:7182/api/command` + +These are configured in `lib/api/openapi_config.dart`. + +## Common Commands Reference + +### Device Management + +```bash +# List all connected devices +flutter devices + +# List devices with adb +/Users/mathias/Library/Android/sdk/platform-tools/adb devices -l + +# Get device info +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell getprop +``` + +### ADB Reverse Proxy + +```bash +# Set up reverse proxy for port 7182 +/Users/mathias/Library/Android/sdk/platform-tools/adb -s reverse tcp:7182 tcp:7182 + +# List all reverse proxies +/Users/mathias/Library/Android/sdk/platform-tools/adb -s reverse --list + +# Remove specific reverse proxy +/Users/mathias/Library/Android/sdk/platform-tools/adb -s reverse --remove tcp:7182 + +# Remove all reverse proxies +/Users/mathias/Library/Android/sdk/platform-tools/adb -s reverse --remove-all +``` + +### Logging + +```bash +# View Flutter logs (last 100 lines) +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -d -t 100 + +# View Flutter logs in real-time +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -s flutter:I -v time + +# View all logs (last 300 lines) +/Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -d -t 300 +``` + +### Build Commands + +```bash +# Debug build (default) +flutter build apk --debug + +# Release build +flutter build apk --release + +# Profile build (for performance testing) +flutter build apk --profile + +# Install APK directly +flutter install -d KM10 +``` + +## Troubleshooting + +### Device Not Found + +If `flutter devices` doesn't show your device: + +1. Check USB debugging is enabled on the device +2. Check device is authorized (check device screen for prompt) +3. Restart adb server: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb kill-server + /Users/mathias/Library/Android/sdk/platform-tools/adb start-server + ``` + +### App Crashes on Startup + +1. Check logs: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 logcat -d | grep -i error + ``` + +2. Clear app data: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 shell pm clear com.goutezplanb.planb_logistic + ``` + +3. Uninstall and reinstall: + ```bash + /Users/mathias/Library/Android/sdk/platform-tools/adb -s 24117ad4 uninstall com.goutezplanb.planb_logistic + flutter run -d KM10 + ``` + +### Backend Connection Issues + +1. Verify reverse proxy is active +2. Check backend API is running +3. Check SSL certificate issues (app uses `https://localhost:7182`) +4. Review network logs in the Flutter output + +## Additional Resources + +- Flutter Documentation: https://docs.flutter.dev +- Android Debug Bridge (adb): https://developer.android.com/studio/command-line/adb +- Project-specific guidelines: See CLAUDE.md for code standards and architecture diff --git a/lib/components/collapsible_routes_sidebar.dart b/lib/components/collapsible_routes_sidebar.dart index 815f574..85c6ae0 100644 --- a/lib/components/collapsible_routes_sidebar.dart +++ b/lib/components/collapsible_routes_sidebar.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../l10n/app_localizations.dart'; import '../models/delivery_route.dart'; import '../theme/spacing_system.dart'; import '../theme/size_system.dart'; @@ -72,6 +73,7 @@ class _CollapsibleRoutesSidebarState extends ConsumerState Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final statusColor = _getStatusColor(widget.delivery); + final l10n = AppLocalizations.of(context)!; // Collapsed view: Show only the badge if (widget.isCollapsed) { @@ -296,7 +298,7 @@ class _DeliveryListItemState extends State Text( widget.delivery.deliveryAddress ?.formattedAddress ?? - 'No address', + l10n.noAddress, style: Theme.of(context) .textTheme .bodyMedium diff --git a/lib/components/route_list_item.dart b/lib/components/route_list_item.dart index f52f0d7..cbc597b 100644 --- a/lib/components/route_list_item.dart +++ b/lib/components/route_list_item.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../models/delivery_route.dart'; import '../theme/animation_system.dart'; import '../theme/color_system.dart'; +import '../l10n/app_localizations.dart'; class RouteListItem extends StatefulWidget { final DeliveryRoute route; @@ -79,6 +80,7 @@ class _RouteListItemState extends State Widget build(BuildContext context) { final isDark = Theme.of(context).brightness == Brightness.dark; final statusColor = _getStatusColor(widget.route); + final l10n = AppLocalizations.of(context)!; // Collapsed view: Show only the badge if (widget.isCollapsed) { @@ -229,7 +231,7 @@ class _RouteListItemState extends State const SizedBox(height: 4), // Route details Text( - '${widget.route.deliveredCount}/${widget.route.deliveriesCount} deliveries', + l10n.routeDeliveries(widget.route.deliveredCount, widget.route.deliveriesCount), style: Theme.of(context) .textTheme .bodyMedium diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 3e231c7..d63426d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -84,5 +84,30 @@ "requestPermission": "Request Permission", "navigationArrived": "You have arrived at the destination", "navigatingTo": "Navigating to", - "initializingNavigation": "Initializing navigation..." + "initializingNavigation": "Initializing navigation...", + "noProfileInfo": "No profile information", + "preferences": "Preferences", + "systemLanguage": "System", + "theme": "Theme", + "themeLight": "Light", + "themeDark": "Dark", + "themeSystem": "Auto", + "builtWithFlutter": "Built with Flutter", + "noAddress": "No address", + "routeDeliveries": "{delivered}/{total} deliveries", + "@routeDeliveries": { + "placeholders": { + "delivered": {"type": "int"}, + "total": {"type": "int"} + } + }, + "username": "Username", + "usernameHint": "Enter your username", + "usernameRequired": "Username is required", + "password": "Password", + "passwordHint": "Enter your password", + "passwordRequired": "Password is required", + "loginButton": "Login", + "navigate": "Navigate", + "upload": "Upload" } diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index bac88b7..2bab17f 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -1,18 +1,18 @@ { "@@locale": "fr", "appTitle": "Plan B Logistique", - "appDescription": "Systme de Gestion des Livraisons", + "appDescription": "Système de Gestion des Livraisons", "loginWithKeycloak": "Connexion avec Keycloak", - "deliveryRoutes": "Itinraires de Livraison", - "routes": "Itinraires", + "deliveryRoutes": "Itinéraires de Livraison", + "routes": "Itinéraires", "deliveries": "Livraisons", - "settings": "Paramtres", + "settings": "Paramètres", "profile": "Profil", - "logout": "Dconnexion", - "completed": "Livr", + "logout": "Déconnexion", + "completed": "Livré", "pending": "En attente", - "todo": "livrer", - "delivered": "Livr", + "todo": "À livrer", + "delivered": "Livré", "newCustomer": "Nouveau Client", "items": "{count} articles", "@items": { @@ -29,29 +29,29 @@ "call": "Appeler", "map": "Carte", "more": "Plus", - "markAsCompleted": "Marquer comme livr", - "markAsUncompleted": "Marquer comme livrer", - "uploadPhoto": "Tlcharger une photo", - "viewDetails": "Voir les dtails", - "deliverySuccessful": "Livraison marque comme complte", - "deliveryFailed": "chec du marquage de la livraison", + "markAsCompleted": "Marquer comme livré", + "markAsUncompleted": "Marquer comme à livrer", + "uploadPhoto": "Télécharger une photo", + "viewDetails": "Voir les détails", + "deliverySuccessful": "Livraison marquée comme complète", + "deliveryFailed": "Échec du marquage de la livraison", "noDeliveries": "Aucune livraison", - "noRoutes": "Aucun itinraire disponible", + "noRoutes": "Aucun itinéraire disponible", "error": "Erreur: {message}", "@error": { "placeholders": { "message": {"type": "String"} } }, - "retry": "Ressayer", + "retry": "Réessayer", "authenticationRequired": "Authentification requise", "phoneCall": "Appeler le client", "navigateToAddress": "Afficher sur la carte", "language": "Langue", "english": "English", - "french": "Franais", + "french": "Français", "appVersion": "Version de l'application", - "about": " propos", + "about": "À propos", "fullName": "{firstName} {lastName}", "@fullName": { "placeholders": { @@ -59,7 +59,7 @@ "lastName": {"type": "String"} } }, - "completedDeliveries": "{completed}/{total} livrs", + "completedDeliveries": "{completed}/{total} livrés", "@completedDeliveries": { "placeholders": { "completed": {"type": "int"}, @@ -69,20 +69,45 @@ "navigationTcTitle": "Service de Navigation", "navigationTcDescription": "Cette application utilise Google Navigation pour fournir une navigation virage par virage pour les livraisons.", "navigationTcAttribution": "Attribution: Services de cartes et de navigation fournis par Google Maps.", - "navigationTcTerms": "En acceptant, vous acceptez les conditions d'utilisation et la politique de confidentialit de Google pour les services de navigation.", + "navigationTcTerms": "En acceptant, vous acceptez les conditions d'utilisation et la politique de confidentialité de Google pour les services de navigation.", "accept": "Accepter", "decline": "Refuser", "locationPermissionRequired": "Permission de localisation", - "locationPermissionMessage": "Cette application ncessite la permission de localisation pour naviguer vers les livraisons.", - "locationPermissionDenied": "Permission de localisation refuse. La navigation ne peut pas continuer.", + "locationPermissionMessage": "Cette application nécessite la permission de localisation pour naviguer vers les livraisons.", + "locationPermissionDenied": "Permission de localisation refusée. La navigation ne peut pas continuer.", "permissionPermanentlyDenied": "Permission requise", - "openSettingsMessage": "La permission de localisation est dfinitivement refuse. Veuillez l'activer dans les paramtres de l'application.", - "openSettings": "Ouvrir les paramtres", + "openSettingsMessage": "La permission de localisation est définitivement refusée. Veuillez l'activer dans les paramètres de l'application.", + "openSettings": "Ouvrir les paramètres", "cancel": "Annuler", "ok": "OK", "errorTitle": "Erreur", "requestPermission": "Demander la permission", - "navigationArrived": "Vous tes arriv la destination", + "navigationArrived": "Vous êtes arrivé à la destination", "navigatingTo": "Navigation vers", - "initializingNavigation": "Initialisation de la navigation..." + "initializingNavigation": "Initialisation de la navigation...", + "noProfileInfo": "Aucune information de profil", + "preferences": "Préférences", + "systemLanguage": "Système", + "theme": "Thème", + "themeLight": "Clair", + "themeDark": "Sombre", + "themeSystem": "Auto", + "builtWithFlutter": "Développé avec Flutter", + "noAddress": "Aucune adresse", + "routeDeliveries": "{delivered}/{total} livraisons", + "@routeDeliveries": { + "placeholders": { + "delivered": {"type": "int"}, + "total": {"type": "int"} + } + }, + "username": "Nom d'utilisateur", + "usernameHint": "Entrez votre nom d'utilisateur", + "usernameRequired": "Le nom d'utilisateur est requis", + "password": "Mot de passe", + "passwordHint": "Entrez votre mot de passe", + "passwordRequired": "Le mot de passe est requis", + "loginButton": "Connexion", + "navigate": "Naviguer", + "upload": "Téléverser" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index e24a372..8c9de9c 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -445,6 +445,120 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Initializing navigation...'** String get initializingNavigation; + + /// No description provided for @noProfileInfo. + /// + /// In en, this message translates to: + /// **'No profile information'** + String get noProfileInfo; + + /// No description provided for @preferences. + /// + /// In en, this message translates to: + /// **'Preferences'** + String get preferences; + + /// No description provided for @systemLanguage. + /// + /// In en, this message translates to: + /// **'System'** + String get systemLanguage; + + /// No description provided for @theme. + /// + /// In en, this message translates to: + /// **'Theme'** + String get theme; + + /// No description provided for @themeLight. + /// + /// In en, this message translates to: + /// **'Light'** + String get themeLight; + + /// No description provided for @themeDark. + /// + /// In en, this message translates to: + /// **'Dark'** + String get themeDark; + + /// No description provided for @themeSystem. + /// + /// In en, this message translates to: + /// **'Auto'** + String get themeSystem; + + /// No description provided for @builtWithFlutter. + /// + /// In en, this message translates to: + /// **'Built with Flutter'** + String get builtWithFlutter; + + /// No description provided for @noAddress. + /// + /// In en, this message translates to: + /// **'No address'** + String get noAddress; + + /// No description provided for @routeDeliveries. + /// + /// In en, this message translates to: + /// **'{delivered}/{total} deliveries'** + String routeDeliveries(int delivered, int total); + + /// No description provided for @username. + /// + /// In en, this message translates to: + /// **'Username'** + String get username; + + /// No description provided for @usernameHint. + /// + /// In en, this message translates to: + /// **'Enter your username'** + String get usernameHint; + + /// No description provided for @usernameRequired. + /// + /// In en, this message translates to: + /// **'Username is required'** + String get usernameRequired; + + /// No description provided for @password. + /// + /// In en, this message translates to: + /// **'Password'** + String get password; + + /// No description provided for @passwordHint. + /// + /// In en, this message translates to: + /// **'Enter your password'** + String get passwordHint; + + /// No description provided for @passwordRequired. + /// + /// In en, this message translates to: + /// **'Password is required'** + String get passwordRequired; + + /// No description provided for @loginButton. + /// + /// In en, this message translates to: + /// **'Login'** + String get loginButton; + + /// No description provided for @navigate. + /// + /// In en, this message translates to: + /// **'Navigate'** + String get navigate; + + /// No description provided for @upload. + /// + /// In en, this message translates to: + /// **'Upload'** + String get upload; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index d55e6b8..0399f33 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -197,4 +197,63 @@ class AppLocalizationsEn extends AppLocalizations { @override String get initializingNavigation => 'Initializing navigation...'; + + @override + String get noProfileInfo => 'No profile information'; + + @override + String get preferences => 'Preferences'; + + @override + String get systemLanguage => 'System'; + + @override + String get theme => 'Theme'; + + @override + String get themeLight => 'Light'; + + @override + String get themeDark => 'Dark'; + + @override + String get themeSystem => 'Auto'; + + @override + String get builtWithFlutter => 'Built with Flutter'; + + @override + String get noAddress => 'No address'; + + @override + String routeDeliveries(int delivered, int total) { + return '$delivered/$total deliveries'; + } + + @override + String get username => 'Username'; + + @override + String get usernameHint => 'Enter your username'; + + @override + String get usernameRequired => 'Username is required'; + + @override + String get password => 'Password'; + + @override + String get passwordHint => 'Enter your password'; + + @override + String get passwordRequired => 'Password is required'; + + @override + String get loginButton => 'Login'; + + @override + String get navigate => 'Navigate'; + + @override + String get upload => 'Upload'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index c0e9790..6ebea89 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -12,40 +12,40 @@ class AppLocalizationsFr extends AppLocalizations { String get appTitle => 'Plan B Logistique'; @override - String get appDescription => 'Systme de Gestion des Livraisons'; + String get appDescription => 'Système de Gestion des Livraisons'; @override String get loginWithKeycloak => 'Connexion avec Keycloak'; @override - String get deliveryRoutes => 'Itinraires de Livraison'; + String get deliveryRoutes => 'Itinéraires de Livraison'; @override - String get routes => 'Itinraires'; + String get routes => 'Itinéraires'; @override String get deliveries => 'Livraisons'; @override - String get settings => 'Paramtres'; + String get settings => 'Paramètres'; @override String get profile => 'Profil'; @override - String get logout => 'Dconnexion'; + String get logout => 'Déconnexion'; @override - String get completed => 'Livr'; + String get completed => 'Livré'; @override String get pending => 'En attente'; @override - String get todo => 'livrer'; + String get todo => 'À livrer'; @override - String get delivered => 'Livr'; + String get delivered => 'Livré'; @override String get newCustomer => 'Nouveau Client'; @@ -70,28 +70,28 @@ class AppLocalizationsFr extends AppLocalizations { String get more => 'Plus'; @override - String get markAsCompleted => 'Marquer comme livr'; + String get markAsCompleted => 'Marquer comme livré'; @override - String get markAsUncompleted => 'Marquer comme livrer'; + String get markAsUncompleted => 'Marquer comme à livrer'; @override - String get uploadPhoto => 'Tlcharger une photo'; + String get uploadPhoto => 'Télécharger une photo'; @override - String get viewDetails => 'Voir les dtails'; + String get viewDetails => 'Voir les détails'; @override - String get deliverySuccessful => 'Livraison marque comme complte'; + String get deliverySuccessful => 'Livraison marquée comme complète'; @override - String get deliveryFailed => 'chec du marquage de la livraison'; + String get deliveryFailed => 'Échec du marquage de la livraison'; @override String get noDeliveries => 'Aucune livraison'; @override - String get noRoutes => 'Aucun itinraire disponible'; + String get noRoutes => 'Aucun itinéraire disponible'; @override String error(String message) { @@ -99,7 +99,7 @@ class AppLocalizationsFr extends AppLocalizations { } @override - String get retry => 'Ressayer'; + String get retry => 'Réessayer'; @override String get authenticationRequired => 'Authentification requise'; @@ -117,13 +117,13 @@ class AppLocalizationsFr extends AppLocalizations { String get english => 'English'; @override - String get french => 'Franais'; + String get french => 'Français'; @override String get appVersion => 'Version de l\'application'; @override - String get about => ' propos'; + String get about => 'À propos'; @override String fullName(String firstName, String lastName) { @@ -132,7 +132,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String completedDeliveries(int completed, int total) { - return '$completed/$total livrs'; + return '$completed/$total livrés'; } @override @@ -148,7 +148,7 @@ class AppLocalizationsFr extends AppLocalizations { @override String get navigationTcTerms => - 'En acceptant, vous acceptez les conditions d\'utilisation et la politique de confidentialit de Google pour les services de navigation.'; + 'En acceptant, vous acceptez les conditions d\'utilisation et la politique de confidentialité de Google pour les services de navigation.'; @override String get accept => 'Accepter'; @@ -161,21 +161,21 @@ class AppLocalizationsFr extends AppLocalizations { @override String get locationPermissionMessage => - 'Cette application ncessite la permission de localisation pour naviguer vers les livraisons.'; + 'Cette application nécessite la permission de localisation pour naviguer vers les livraisons.'; @override String get locationPermissionDenied => - 'Permission de localisation refuse. La navigation ne peut pas continuer.'; + 'Permission de localisation refusée. La navigation ne peut pas continuer.'; @override String get permissionPermanentlyDenied => 'Permission requise'; @override String get openSettingsMessage => - 'La permission de localisation est dfinitivement refuse. Veuillez l\'activer dans les paramtres de l\'application.'; + 'La permission de localisation est définitivement refusée. Veuillez l\'activer dans les paramètres de l\'application.'; @override - String get openSettings => 'Ouvrir les paramtres'; + String get openSettings => 'Ouvrir les paramètres'; @override String get cancel => 'Annuler'; @@ -190,11 +190,70 @@ class AppLocalizationsFr extends AppLocalizations { String get requestPermission => 'Demander la permission'; @override - String get navigationArrived => 'Vous tes arriv la destination'; + String get navigationArrived => 'Vous êtes arrivé à la destination'; @override String get navigatingTo => 'Navigation vers'; @override String get initializingNavigation => 'Initialisation de la navigation...'; + + @override + String get noProfileInfo => 'Aucune information de profil'; + + @override + String get preferences => 'Préférences'; + + @override + String get systemLanguage => 'Système'; + + @override + String get theme => 'Thème'; + + @override + String get themeLight => 'Clair'; + + @override + String get themeDark => 'Sombre'; + + @override + String get themeSystem => 'Auto'; + + @override + String get builtWithFlutter => 'Développé avec Flutter'; + + @override + String get noAddress => 'Aucune adresse'; + + @override + String routeDeliveries(int delivered, int total) { + return '$delivered/$total livraisons'; + } + + @override + String get username => 'Nom d\'utilisateur'; + + @override + String get usernameHint => 'Entrez votre nom d\'utilisateur'; + + @override + String get usernameRequired => 'Le nom d\'utilisateur est requis'; + + @override + String get password => 'Mot de passe'; + + @override + String get passwordHint => 'Entrez votre mot de passe'; + + @override + String get passwordRequired => 'Le mot de passe est requis'; + + @override + String get loginButton => 'Connexion'; + + @override + String get navigate => 'Naviguer'; + + @override + String get upload => 'Téléverser'; } diff --git a/lib/main.dart b/lib/main.dart index 84f64dc..4687be0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'l10n/app_localizations.dart'; import 'theme.dart'; import 'providers/providers.dart'; import 'pages/login_page.dart'; @@ -27,25 +28,66 @@ class PlanBLogisticApp extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final language = ref.watch(languageProvider); + final languageAsync = ref.watch(languageProvider); final themeMode = ref.watch(themeModeProvider); - return MaterialApp( - title: 'Plan B Logistics', - theme: MaterialTheme(const TextTheme()).light(), - darkTheme: MaterialTheme(const TextTheme()).dark(), - themeMode: themeMode, - locale: Locale(language), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en', 'CA'), - Locale('fr', 'CA'), - ], - home: const AppHome(), + return languageAsync.when( + data: (language) { + Locale? locale; + if (language == 'en') { + locale = const Locale('en'); + } else if (language == 'fr') { + locale = const Locale('fr'); + } + // If language is 'system', locale remains null and will use device locale + + return MaterialApp( + title: 'Plan B Logistics', + theme: MaterialTheme(const TextTheme()).light(), + darkTheme: MaterialTheme(const TextTheme()).dark(), + themeMode: themeMode, + locale: locale, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('fr'), + ], + home: const AppHome(), + ); + }, + loading: () => MaterialApp( + title: 'Plan B Logistics', + theme: MaterialTheme(const TextTheme()).light(), + darkTheme: MaterialTheme(const TextTheme()).dark(), + themeMode: themeMode, + home: const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ), + ), + error: (_, __) => MaterialApp( + title: 'Plan B Logistics', + theme: MaterialTheme(const TextTheme()).light(), + darkTheme: MaterialTheme(const TextTheme()).dark(), + themeMode: themeMode, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('fr'), + ], + home: const AppHome(), + ), ); } } diff --git a/lib/pages/deliveries_page.dart b/lib/pages/deliveries_page.dart index c27673b..dcbb7a7 100644 --- a/lib/pages/deliveries_page.dart +++ b/lib/pages/deliveries_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../l10n/app_localizations.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http/http.dart' as http; @@ -85,6 +86,7 @@ class _DeliveriesPageState extends ConsumerState { final deliveriesData = ref.watch(deliveriesProvider(widget.routeFragmentId)); final tokenAsync = ref.watch(authTokenProvider); final token = tokenAsync.hasValue ? tokenAsync.value : null; + final l10n = AppLocalizations.of(context)!; // When embedded in sidebar, show only the delivery list with back button // This is a responsive sidebar that collapses like routes @@ -183,7 +185,7 @@ class _DeliveriesPageState extends ConsumerState { child: CircularProgressIndicator(), ), error: (error, stackTrace) => Center( - child: Text('Error: $error'), + child: Text(l10n.error(error.toString())), ), ); } @@ -277,7 +279,7 @@ class _DeliveriesPageState extends ConsumerState { child: CircularProgressIndicator(), ), error: (error, stackTrace) => Center( - child: Text('Error: $error'), + child: Text(l10n.error(error.toString())), ), ), ), @@ -291,7 +293,8 @@ class _DeliveriesPageState extends ConsumerState { String? token, ) async { if (token == null) { - ToastHelper.showError(context, 'Authentication required'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.authenticationRequired); return; } @@ -312,12 +315,14 @@ class _DeliveriesPageState extends ConsumerState { ); result.when( success: (_) { + final l10n = AppLocalizations.of(context)!; // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); - ToastHelper.showSuccess(context, 'Delivery marked as completed'); + ToastHelper.showSuccess(context, l10n.deliverySuccessful); }, onError: (error) { - ToastHelper.showError(context, 'Error: ${error.message}'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.error(error.message)); }, ); break; @@ -329,12 +334,14 @@ class _DeliveriesPageState extends ConsumerState { ); result.when( success: (_) { + final l10n = AppLocalizations.of(context)!; // ignore: unused_result ref.refresh(deliveriesProvider(widget.routeFragmentId)); ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); }, onError: (error) { - ToastHelper.showError(context, 'Error: ${error.message}'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.error(error.message)); }, ); break; @@ -368,7 +375,8 @@ class _DeliveriesPageState extends ConsumerState { String? token, ) async { if (token == null) { - ToastHelper.showError(context, 'Authentication required'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.authenticationRequired); return; } @@ -416,11 +424,11 @@ class _DeliveriesPageState extends ConsumerState { actions: [ TextButton( onPressed: () => Navigator.of(dialogContext).pop(false), - child: const Text('Cancel'), + child: Text(AppLocalizations.of(context)!.cancel), ), ElevatedButton( onPressed: () => Navigator.of(dialogContext).pop(true), - child: const Text('Upload'), + child: Text(AppLocalizations.of(context)!.upload), ), ], ); @@ -511,9 +519,10 @@ class UnifiedDeliveryListView extends StatelessWidget { @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; if (deliveries.isEmpty) { - return const Center( - child: Text('No deliveries'), + return Center( + child: Text(l10n.noDeliveries), ); } @@ -599,12 +608,12 @@ class DeliveryCard extends StatelessWidget { ), if (delivery.delivered) Chip( - label: const Text('Delivered'), + label: Text(AppLocalizations.of(context)!.delivered), backgroundColor: Theme.of(context).colorScheme.primaryContainer, ) else if (order?.isNewCustomer ?? false) Chip( - label: const Text('New Customer'), + label: Text(AppLocalizations.of(context)!.newCustomer), backgroundColor: const Color(0xFFFFFBEB), ), ], @@ -624,11 +633,11 @@ class DeliveryCard extends StatelessWidget { children: [ if (order.totalItems != null) Text( - '${order.totalItems} items', + AppLocalizations.of(context)!.items(order.totalItems!), style: Theme.of(context).textTheme.bodySmall, ), Text( - '${order.totalAmount} MAD', + AppLocalizations.of(context)!.moneyCurrency(order.totalAmount), style: Theme.of(context).textTheme.titleSmall?.copyWith( color: Theme.of(context).colorScheme.primary, ), @@ -644,7 +653,7 @@ class DeliveryCard extends StatelessWidget { OutlinedButton.icon( onPressed: () => onAction(delivery, 'call'), icon: const Icon(Icons.phone), - label: const Text('Call'), + label: Text(AppLocalizations.of(context)!.call), ), if (delivery.deliveryAddress != null) OutlinedButton.icon( @@ -653,12 +662,12 @@ class DeliveryCard extends StatelessWidget { onAction(delivery, 'map'); }, icon: const Icon(Icons.map), - label: const Text('Navigate'), + label: Text(AppLocalizations.of(context)!.navigate), ), OutlinedButton.icon( onPressed: () => _showDeliveryActions(context), icon: const Icon(Icons.more_vert), - label: const Text('More'), + label: Text(AppLocalizations.of(context)!.more), ), ], ), @@ -670,6 +679,7 @@ class DeliveryCard extends StatelessWidget { } void _showDeliveryActions(BuildContext context) { + final l10n = AppLocalizations.of(context)!; showModalBottomSheet( context: context, builder: (context) => SafeArea( @@ -679,7 +689,7 @@ class DeliveryCard extends StatelessWidget { if (!delivery.delivered) ListTile( leading: const Icon(Icons.check_circle), - title: const Text('Mark as Completed'), + title: Text(l10n.markAsCompleted), onTap: () { Navigator.pop(context); onAction(delivery, 'complete'); @@ -688,7 +698,7 @@ class DeliveryCard extends StatelessWidget { else ListTile( leading: const Icon(Icons.undo), - title: const Text('Mark as Uncompleted'), + title: Text(l10n.markAsUncompleted), onTap: () { Navigator.pop(context); onAction(delivery, 'uncomplete'); @@ -696,7 +706,7 @@ class DeliveryCard extends StatelessWidget { ), ListTile( leading: const Icon(Icons.camera_alt), - title: const Text('Upload Photo'), + title: Text(l10n.uploadPhoto), onTap: () { Navigator.pop(context); // TODO: Implement photo upload @@ -704,7 +714,7 @@ class DeliveryCard extends StatelessWidget { ), ListTile( leading: const Icon(Icons.description), - title: const Text('View Details'), + title: Text(l10n.viewDetails), onTap: () { Navigator.pop(context); // TODO: Navigate to delivery details diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 3a5117d..579f7bf 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../l10n/app_localizations.dart'; import '../providers/providers.dart'; import '../utils/toast_helper.dart'; @@ -78,7 +79,7 @@ class _LoginPageState extends ConsumerState { ), const SizedBox(height: 24), Text( - 'Plan B Logistics', + AppLocalizations.of(context)!.appTitle, textAlign: TextAlign.center, style: Theme.of(context).textTheme.displayMedium?.copyWith( color: Theme.of(context).colorScheme.primary, @@ -87,7 +88,7 @@ class _LoginPageState extends ConsumerState { ), const SizedBox(height: 8), Text( - 'Delivery Management System', + AppLocalizations.of(context)!.appDescription, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyLarge?.copyWith( color: Theme.of(context).colorScheme.onSurfaceVariant, @@ -96,17 +97,17 @@ class _LoginPageState extends ConsumerState { const SizedBox(height: 48), TextFormField( controller: _usernameController, - decoration: const InputDecoration( - labelText: 'Username', - hintText: 'Enter your username', - prefixIcon: Icon(Icons.person), - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: AppLocalizations.of(context)!.username, + hintText: AppLocalizations.of(context)!.usernameHint, + prefixIcon: const Icon(Icons.person), + border: const OutlineInputBorder(), ), textInputAction: TextInputAction.next, enabled: !_isLoading, validator: (value) { if (value == null || value.trim().isEmpty) { - return 'Please enter your username'; + return AppLocalizations.of(context)!.usernameRequired; } return null; }, @@ -115,8 +116,8 @@ class _LoginPageState extends ConsumerState { TextFormField( controller: _passwordController, decoration: InputDecoration( - labelText: 'Password', - hintText: 'Enter your password', + labelText: AppLocalizations.of(context)!.password, + hintText: AppLocalizations.of(context)!.passwordHint, prefixIcon: const Icon(Icons.lock), border: const OutlineInputBorder(), suffixIcon: IconButton( @@ -136,7 +137,7 @@ class _LoginPageState extends ConsumerState { onFieldSubmitted: (_) => _handleLogin(), validator: (value) { if (value == null || value.isEmpty) { - return 'Please enter your password'; + return AppLocalizations.of(context)!.passwordRequired; } return null; }, @@ -158,7 +159,7 @@ class _LoginPageState extends ConsumerState { ), ), ) - : const Text('Login'), + : Text(AppLocalizations.of(context)!.loginButton), ), ], ), diff --git a/lib/pages/routes_page.dart b/lib/pages/routes_page.dart index e73d29a..a24b65f 100644 --- a/lib/pages/routes_page.dart +++ b/lib/pages/routes_page.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../l10n/app_localizations.dart'; import 'package:image_picker/image_picker.dart'; import 'package:http/http.dart' as http; import '../models/delivery.dart'; @@ -86,7 +87,8 @@ class _RoutesPageState extends ConsumerState { final token = await authService.ensureValidToken(); if (token == null) { if (mounted) { - ToastHelper.showError(context, 'Authentication required'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.authenticationRequired); } return; } @@ -173,7 +175,8 @@ class _RoutesPageState extends ConsumerState { } if (mounted) { - ToastHelper.showSuccess(context, 'Delivery marked as completed'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showSuccess(context, l10n.deliverySuccessful); } } }, @@ -185,7 +188,8 @@ class _RoutesPageState extends ConsumerState { debugPrint('Complete delivery failed - Type: ${error.type}, Message: ${error.message}'); debugPrint('Error details: ${error.details}'); if (mounted) { - String errorMessage = 'Error: ${error.message}'; + final l10n = AppLocalizations.of(context)!; + String errorMessage = l10n.error(error.message); if (error.statusCode == 500) { errorMessage = 'Server error - Please contact support'; } @@ -251,6 +255,7 @@ class _RoutesPageState extends ConsumerState { } if (mounted) { + final l10n = AppLocalizations.of(context)!; ToastHelper.showSuccess(context, 'Delivery marked as uncompleted'); } } @@ -261,7 +266,8 @@ class _RoutesPageState extends ConsumerState { } if (mounted) { - ToastHelper.showError(context, 'Error: ${error.message}'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.error(error.message)); } }, ); @@ -286,7 +292,8 @@ class _RoutesPageState extends ConsumerState { final token = await authService.ensureValidToken(); if (token == null) { if (mounted) { - ToastHelper.showError(context, 'Authentication required'); + final l10n = AppLocalizations.of(context)!; + ToastHelper.showError(context, l10n.authenticationRequired); } return; } @@ -507,10 +514,11 @@ class _RoutesPageState extends ConsumerState { final routesData = ref.watch(deliveryRoutesProvider); final allDeliveriesData = ref.watch(allDeliveriesProvider); final userProfile = ref.watch(userProfileProvider); + final l10n = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( - title: const Text('Delivery Routes'), + title: Text(l10n.deliveryRoutes), elevation: 0, actions: [ IconButton( @@ -580,8 +588,8 @@ class _RoutesPageState extends ConsumerState { body: routesData.when( data: (routes) { if (routes.isEmpty) { - return const Center( - child: Text('No routes available'), + return Center( + child: Text(l10n.noRoutes), ); } return allDeliveriesData.when( @@ -642,11 +650,11 @@ class _RoutesPageState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Error loading deliveries: $error'), + Text(l10n.error(error.toString())), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(allDeliveriesProvider), - child: const Text('Retry'), + child: Text(l10n.retry), ), ], ), @@ -660,11 +668,11 @@ class _RoutesPageState extends ConsumerState { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Error: $error'), + Text(l10n.error(error.toString())), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.refresh(deliveryRoutesProvider), - child: const Text('Retry'), + child: Text(l10n.retry), ), ], ), diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index 037d066..cb4486e 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../l10n/app_localizations.dart'; import '../providers/providers.dart'; class SettingsPage extends ConsumerWidget { @@ -8,12 +9,18 @@ class SettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final userProfile = ref.watch(userProfileProvider); - final language = ref.watch(languageProvider); + final languageAsync = ref.watch(languageProvider); final themeMode = ref.watch(themeModeProvider); + final l10n = AppLocalizations.of(context)!; + + final language = languageAsync.maybeWhen( + data: (value) => value, + orElse: () => 'system', + ); return Scaffold( appBar: AppBar( - title: const Text('Settings'), + title: Text(l10n.settings), ), body: ListView( children: [ @@ -23,14 +30,14 @@ class SettingsPage extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Profile', + l10n.profile, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), userProfile.when( data: (profile) { if (profile == null) { - return const Text('No profile information'); + return Text(l10n.noProfileInfo); } return Card( child: Padding( @@ -78,7 +85,7 @@ class SettingsPage extends ConsumerWidget { } }, color: Theme.of(context).colorScheme.error, - tooltip: 'Logout', + tooltip: l10n.logout, ), ], ), @@ -88,7 +95,7 @@ class SettingsPage extends ConsumerWidget { ); }, loading: () => const CircularProgressIndicator(), - error: (error, stackTrace) => Text('Error: $error'), + error: (error, stackTrace) => Text(l10n.error(error.toString())), ), ], ), @@ -100,18 +107,18 @@ class SettingsPage extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Preferences', + l10n.preferences, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), ListTile( - title: const Text('Language'), + title: Text(l10n.language), subtitle: Text( language == 'system' - ? 'System' + ? l10n.systemLanguage : language == 'fr' - ? 'Français' - : 'English' + ? l10n.french + : l10n.english ), trailing: DropdownButton( value: language, @@ -120,18 +127,18 @@ class SettingsPage extends ConsumerWidget { ref.read(languageProvider.notifier).setLanguage(newValue); } }, - items: const [ + items: [ DropdownMenuItem( value: 'system', - child: Text('System'), + child: Text(l10n.systemLanguage), ), DropdownMenuItem( value: 'en', - child: Text('English'), + child: Text(l10n.english), ), DropdownMenuItem( value: 'fr', - child: Text('Français'), + child: Text(l10n.french), ), ], ), @@ -141,7 +148,7 @@ class SettingsPage extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Theme', + l10n.theme, style: Theme.of(context).textTheme.titleSmall, ), const SizedBox(height: 8), @@ -150,21 +157,21 @@ class SettingsPage extends ConsumerWidget { onSelectionChanged: (Set newSelection) { ref.read(themeModeProvider.notifier).setThemeMode(newSelection.first); }, - segments: const [ + segments: [ ButtonSegment( value: ThemeMode.light, - label: Text('Light'), - icon: Icon(Icons.light_mode), + label: Text(l10n.themeLight), + icon: const Icon(Icons.light_mode), ), ButtonSegment( value: ThemeMode.dark, - label: Text('Dark'), - icon: Icon(Icons.dark_mode), + label: Text(l10n.themeDark), + icon: const Icon(Icons.dark_mode), ), ButtonSegment( value: ThemeMode.system, - label: Text('Auto'), - icon: Icon(Icons.brightness_auto), + label: Text(l10n.themeSystem), + icon: const Icon(Icons.brightness_auto), ), ], ), @@ -180,17 +187,17 @@ class SettingsPage extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'About', + l10n.about, style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 16), ListTile( - title: const Text('App Version'), + title: Text(l10n.appVersion), subtitle: const Text('1.0.0'), ), ListTile( - title: const Text('Built with Flutter'), - subtitle: const Text('Plan B Logistics Management System'), + title: Text(l10n.builtWithFlutter), + subtitle: Text(l10n.appDescription), ), ], ), diff --git a/lib/providers/providers.dart b/lib/providers/providers.dart index fa63742..bd90506 100644 --- a/lib/providers/providers.dart +++ b/lib/providers/providers.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../api/types.dart'; import '../api/client.dart'; import '../api/openapi_config.dart'; @@ -125,15 +126,24 @@ final allDeliveriesProvider = FutureProvider>((ref) async { return allDeliveries; }); -// Language notifier for state management -class LanguageNotifier extends Notifier { - @override - String build() => 'system'; +// Language notifier for state management with SharedPreferences persistence +class LanguageNotifier extends AsyncNotifier { + static const String _languageKey = 'app_language'; - void setLanguage(String lang) => state = lang; + @override + Future build() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_languageKey) ?? 'system'; + } + + Future setLanguage(String lang) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_languageKey, lang); + state = AsyncValue.data(lang); + } } -final languageProvider = NotifierProvider(() { +final languageProvider = AsyncNotifierProvider(() { return LanguageNotifier(); });