(Feat) Update Loading screen
This commit is contained in:
@@ -4,3 +4,6 @@ android.useAndroidX=true
|
|||||||
android.newDsl=false
|
android.newDsl=false
|
||||||
# This builtInKotlin flag was added by the Flutter template
|
# This builtInKotlin flag was added by the Flutter template
|
||||||
android.builtInKotlin=false
|
android.builtInKotlin=false
|
||||||
|
# Le cache incrémental Kotlin plante quand le pub cache (C:) et le projet (D:)
|
||||||
|
# sont sur des disques différents : il ne peut pas calculer de chemin relatif.
|
||||||
|
kotlin.incremental=false
|
||||||
|
|||||||
+4
-6
@@ -1,8 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import 'screens/home_shell.dart';
|
import 'screens/demarrage_screen.dart';
|
||||||
import 'services/produit_repository.dart';
|
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
|
|
||||||
Future<void> main() async {
|
Future<void> main() async {
|
||||||
@@ -14,9 +13,8 @@ Future<void> main() async {
|
|||||||
DeviceOrientation.portraitDown,
|
DeviceOrientation.portraitDown,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Connexion au serveur (Raspberry Pi) + chargement + temps réel.
|
// On affiche l'interface tout de suite : la connexion au serveur se fait
|
||||||
await ProduitRepository.instance.initialiser();
|
// dans DemarrageScreen, loader visible (plus d'écran blanc au lancement).
|
||||||
|
|
||||||
runApp(const MonApp());
|
runApp(const MonApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +29,7 @@ class MonApp extends StatelessWidget {
|
|||||||
theme: AppTheme.clair(),
|
theme: AppTheme.clair(),
|
||||||
darkTheme: AppTheme.sombre(),
|
darkTheme: AppTheme.sombre(),
|
||||||
themeMode: ThemeMode.light,
|
themeMode: ThemeMode.light,
|
||||||
home: const HomeShell(),
|
home: const DemarrageScreen(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../services/produit_repository.dart';
|
||||||
|
import '../theme.dart';
|
||||||
|
import 'home_shell.dart';
|
||||||
|
|
||||||
|
/// Écran affiché au lancement pendant la connexion au serveur.
|
||||||
|
/// Évite l'écran blanc : on voit le loader et l'étape en cours, et en cas
|
||||||
|
/// d'échec on peut réessayer ou continuer avec les données hors ligne.
|
||||||
|
class DemarrageScreen extends StatefulWidget {
|
||||||
|
const DemarrageScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<DemarrageScreen> createState() => _DemarrageScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DemarrageScreenState extends State<DemarrageScreen> {
|
||||||
|
bool _fini = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_lancer();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _lancer() async {
|
||||||
|
final repo = ProduitRepository.instance;
|
||||||
|
await repo.initialiser();
|
||||||
|
if (!mounted) return;
|
||||||
|
// Serveur joignable (ou cache déjà rempli) : on entre directement dans l'app.
|
||||||
|
if (repo.connecte || repo.charge) {
|
||||||
|
setState(() => _fini = true);
|
||||||
|
} else {
|
||||||
|
setState(() {}); // affiche l'erreur + bouton Réessayer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _reessayer() async {
|
||||||
|
await ProduitRepository.instance.charger();
|
||||||
|
if (!mounted) return;
|
||||||
|
final repo = ProduitRepository.instance;
|
||||||
|
if (repo.connecte || repo.charge) setState(() => _fini = true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (_fini) return const HomeShell();
|
||||||
|
|
||||||
|
final repo = ProduitRepository.instance;
|
||||||
|
return Scaffold(
|
||||||
|
body: SafeArea(
|
||||||
|
child: Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 420),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: ListenableBuilder(
|
||||||
|
listenable: repo,
|
||||||
|
builder: (context, _) {
|
||||||
|
final echec = !repo.enCours && repo.erreur != null;
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
echec ? Icons.cloud_off_rounded : Icons.storefront_rounded,
|
||||||
|
size: 56,
|
||||||
|
color: echec ? const Color(0xFFB26B00) : AppTheme.accent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Text(
|
||||||
|
'Gestion Prix',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 26,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
letterSpacing: -0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
if (echec) ..._echec(repo) else ..._chargement(repo),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loader + étape en cours juste en dessous.
|
||||||
|
List<Widget> _chargement(ProduitRepository repo) => [
|
||||||
|
const SizedBox(
|
||||||
|
width: 34,
|
||||||
|
height: 34,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 3),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
Text(
|
||||||
|
repo.etape.isEmpty ? 'Démarrage…' : repo.etape,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14.5,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppTheme.grisTexte,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
repo.serveurUrl,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 12, color: AppTheme.grisTexte),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Serveur injoignable et aucun produit en cache : on explique et on propose.
|
||||||
|
List<Widget> _echec(ProduitRepository repo) => [
|
||||||
|
const Text(
|
||||||
|
'Serveur injoignable',
|
||||||
|
style: TextStyle(fontSize: 17, fontWeight: FontWeight.w700),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
repo.serveurUrl,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 12.5, color: AppTheme.grisTexte),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Text(
|
||||||
|
'Vérifie que le serveur est allumé et que la tablette est bien '
|
||||||
|
'connectée au réseau (Tailscale actif).',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(fontSize: 13, color: AppTheme.grisTexte),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
FilledButton.icon(
|
||||||
|
onPressed: _reessayer,
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Réessayer'),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => setState(() => _fini = true),
|
||||||
|
child: const Text('Continuer hors ligne'),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ class ProduitRepository extends ChangeNotifier {
|
|||||||
|
|
||||||
static const _collection = 'produits';
|
static const _collection = 'produits';
|
||||||
|
|
||||||
|
/// Au-delà, on considère le serveur injoignable (évite d'attendre indéfiniment
|
||||||
|
/// au démarrage quand le Raspberry Pi est éteint ou hors réseau).
|
||||||
|
static const _delaiMax = Duration(seconds: 12);
|
||||||
|
|
||||||
PocketBase? _pb;
|
PocketBase? _pb;
|
||||||
String _serveurUrl = '';
|
String _serveurUrl = '';
|
||||||
UnsubscribeFunc? _annulerAbo;
|
UnsubscribeFunc? _annulerAbo;
|
||||||
@@ -29,11 +33,20 @@ class ProduitRepository extends ChangeNotifier {
|
|||||||
bool _enCours = false;
|
bool _enCours = false;
|
||||||
bool _connecte = false;
|
bool _connecte = false;
|
||||||
String? _erreur;
|
String? _erreur;
|
||||||
|
String _etape = '';
|
||||||
|
|
||||||
List<Produit> get produits => List.unmodifiable(_produits);
|
List<Produit> get produits => List.unmodifiable(_produits);
|
||||||
bool get charge => _charge;
|
bool get charge => _charge;
|
||||||
bool get enCours => _enCours;
|
bool get enCours => _enCours;
|
||||||
|
|
||||||
|
/// Étape en cours du démarrage, affichée sous le loader de l'écran d'accueil.
|
||||||
|
String get etape => _etape;
|
||||||
|
|
||||||
|
void _setEtape(String texte) {
|
||||||
|
_etape = texte;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Vrai si la dernière synchro avec le serveur a réussi (temps réel actif).
|
/// Vrai si la dernière synchro avec le serveur a réussi (temps réel actif).
|
||||||
bool get connecte => _connecte;
|
bool get connecte => _connecte;
|
||||||
String? get erreur => _erreur;
|
String? get erreur => _erreur;
|
||||||
@@ -43,9 +56,13 @@ class ProduitRepository extends ChangeNotifier {
|
|||||||
/// aux mises à jour temps réel. Ne lève jamais : en cas d'échec, [erreur]
|
/// aux mises à jour temps réel. Ne lève jamais : en cas d'échec, [erreur]
|
||||||
/// est renseigné pour que l'écran propose de réessayer.
|
/// est renseigné pour que l'écran propose de réessayer.
|
||||||
Future<void> initialiser() async {
|
Future<void> initialiser() async {
|
||||||
|
_setEtape('Lecture des réglages…');
|
||||||
_serveurUrl = await Reglages.instance.serveurUrl();
|
_serveurUrl = await Reglages.instance.serveurUrl();
|
||||||
_pb = PocketBase(_serveurUrl);
|
_pb = PocketBase(_serveurUrl);
|
||||||
|
|
||||||
|
_setEtape('Chargement des produits enregistrés…');
|
||||||
await _chargerCache(); // affichage instantané + hors ligne
|
await _chargerCache(); // affichage instantané + hors ligne
|
||||||
|
|
||||||
await charger(); // serveur + temps réel
|
await charger(); // serveur + temps réel
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,21 +115,33 @@ class ProduitRepository extends ChangeNotifier {
|
|||||||
Future<void> charger() async {
|
Future<void> charger() async {
|
||||||
_enCours = true;
|
_enCours = true;
|
||||||
_erreur = null;
|
_erreur = null;
|
||||||
notifyListeners();
|
_setEtape('Connexion au serveur…');
|
||||||
try {
|
try {
|
||||||
final records =
|
final records = await _pb!
|
||||||
await _pb!.collection(_collection).getFullList(sort: '-created');
|
.collection(_collection)
|
||||||
|
.getFullList(sort: '-created')
|
||||||
|
.timeout(_delaiMax);
|
||||||
|
|
||||||
|
_setEtape('Récupération des produits…');
|
||||||
_produits
|
_produits
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(records.map(_versProduit));
|
..addAll(records.map(_versProduit));
|
||||||
_charge = true;
|
_charge = true;
|
||||||
_connecte = true;
|
_connecte = true;
|
||||||
|
|
||||||
|
_setEtape('Enregistrement hors ligne…');
|
||||||
await _sauverCache();
|
await _sauverCache();
|
||||||
if (_annulerAbo == null) await _abonner();
|
|
||||||
|
if (_annulerAbo == null) {
|
||||||
|
_setEtape('Activation du temps réel…');
|
||||||
|
await _abonner();
|
||||||
|
}
|
||||||
|
_setEtape('Prêt');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_connecte = false;
|
_connecte = false;
|
||||||
// On garde les produits en cache ; le bandeau « non synchronisé » suffit.
|
// On garde les produits en cache ; le bandeau « non synchronisé » suffit.
|
||||||
if (_produits.isEmpty) _erreur = 'Serveur injoignable ($_serveurUrl).\n$e';
|
if (_produits.isEmpty) _erreur = 'Serveur injoignable ($_serveurUrl).\n$e';
|
||||||
|
_setEtape('Hors ligne');
|
||||||
} finally {
|
} finally {
|
||||||
_enCours = false;
|
_enCours = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -132,7 +161,7 @@ class ProduitRepository extends ChangeNotifier {
|
|||||||
_connecte = true;
|
_connecte = true;
|
||||||
unawaited(_sauverCache());
|
unawaited(_sauverCache());
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
}).timeout(_delaiMax);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Pas de temps réel (serveur momentanément indisponible) : pas bloquant,
|
// Pas de temps réel (serveur momentanément indisponible) : pas bloquant,
|
||||||
// les données se rechargeront à la prochaine ouverture / réessai.
|
// les données se rechargeront à la prochaine ouverture / réessai.
|
||||||
|
|||||||
@@ -10,8 +10,15 @@ class Reglages {
|
|||||||
static const _cleDerniereUnite = 'derniere_unite';
|
static const _cleDerniereUnite = 'derniere_unite';
|
||||||
static const _cleServeur = 'serveur_url';
|
static const _cleServeur = 'serveur_url';
|
||||||
|
|
||||||
/// Adresse par défaut du serveur PocketBase (le Raspberry Pi du magasin).
|
/// Adresse par défaut du serveur PocketBase (le Raspberry Pi du magasin,
|
||||||
static const serveurParDefaut = 'http://192.168.1.32:8090';
|
/// exposé via Tailscale : accessible aussi bien au magasin qu'à l'extérieur).
|
||||||
|
static const serveurParDefaut = 'https://db.tailb756e1.ts.net';
|
||||||
|
|
||||||
|
/// Anciennes adresses par défaut : si la tablette a encore l'une d'elles
|
||||||
|
/// enregistrée, on bascule automatiquement sur la nouvelle.
|
||||||
|
static const _anciensDefauts = {
|
||||||
|
'http://192.168.1.32:8090',
|
||||||
|
};
|
||||||
|
|
||||||
SharedPreferences? _prefs;
|
SharedPreferences? _prefs;
|
||||||
|
|
||||||
@@ -19,8 +26,16 @@ class Reglages {
|
|||||||
_prefs ??= await SharedPreferences.getInstance();
|
_prefs ??= await SharedPreferences.getInstance();
|
||||||
|
|
||||||
/// Adresse du serveur PocketBase (modifiable dans les réglages).
|
/// Adresse du serveur PocketBase (modifiable dans les réglages).
|
||||||
Future<String> serveurUrl() async =>
|
Future<String> serveurUrl() async {
|
||||||
(await _p).getString(_cleServeur) ?? serveurParDefaut;
|
final p = await _p;
|
||||||
|
final url = p.getString(_cleServeur);
|
||||||
|
if (url == null || _anciensDefauts.contains(url)) {
|
||||||
|
// Jamais réglé, ou ancienne adresse par défaut restée en mémoire.
|
||||||
|
if (url != null) await p.setString(_cleServeur, serveurParDefaut);
|
||||||
|
return serveurParDefaut;
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> setServeurUrl(String url) async =>
|
Future<void> setServeurUrl(String url) async =>
|
||||||
(await _p).setString(_cleServeur, url.trim());
|
(await _p).setString(_cleServeur, url.trim());
|
||||||
|
|||||||
Reference in New Issue
Block a user