import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import '../models/produit.dart'; import '../services/openfoodfacts_service.dart'; import '../services/produit_repository.dart'; import '../theme.dart'; import 'produit_edit_screen.dart'; /// Onglet 2 : scan d'un code-barres → recherche OpenFoodFacts → formulaire. class ScanScreen extends StatefulWidget { final bool active; const ScanScreen({super.key, required this.active}); @override State createState() => _ScanScreenState(); } class _ScanScreenState extends State { final _controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, formats: const [ BarcodeFormat.ean13, BarcodeFormat.ean8, BarcodeFormat.upcA, BarcodeFormat.upcE, BarcodeFormat.code128, ], ); final _off = OpenFoodFactsService(); final _repo = ProduitRepository.instance; bool _traite = false; // La caméra est démarrée/arrêtée automatiquement par le widget MobileScanner // quand il est monté/démonté selon l'onglet actif (autoStart). On ne fait // AUCUN start()/stop() manuel ici : ça provoquerait un double démarrage. @override void didUpdateWidget(covariant ScanScreen old) { super.didUpdateWidget(old); // En revenant sur l'onglet Scanner, on réautorise la détection. if (widget.active && !old.active) _traite = false; } @override void dispose() { _controller.dispose(); super.dispose(); } Future _onDetect(BarcodeCapture capture) async { if (_traite || !widget.active) return; final code = capture.barcodes.firstOrNull?.rawValue; if (code == null || code.isEmpty) return; // On bloque les détections suivantes pendant le traitement (sans couper la caméra). setState(() => _traite = true); // Déjà connu ? → on ouvre directement la fiche. final existant = _repo.parCodeBarres(code); if (existant != null) { await _ouvrirExistant(existant); return; } // Sinon on interroge OpenFoodFacts (avec un petit loader). final info = await _off.chercherParCodeBarres(code); if (!mounted) return; final ajoute = await Navigator.of(context).push( MaterialPageRoute( builder: (_) => ProduitEditScreen( codeBarresInitial: code, nomInitial: info?.nom, imageUrlInitial: info?.imageUrl, quantiteInitial: info?.quantite, uniteInitial: info?.unite, ), ), ); if (mounted) { setState(() => _traite = false); if (ajoute == true) await _popupAjoute(); } } Future _ouvrirExistant(Produit p) async { await Navigator.of(context).push( MaterialPageRoute(builder: (_) => ProduitEditScreen(produit: p)), ); if (mounted) setState(() => _traite = false); } /// Ajout manuel via le bouton « + » (sans scanner). Future _ajouterManuel() async { setState(() => _traite = true); // bloque les détections pendant la saisie final ajoute = await Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ProduitEditScreen()), ); if (mounted) { setState(() => _traite = false); if (ajoute == true) await _popupAjoute(); } } /// Pop-up de confirmation « Produit ajouté », se ferme tout seul. Future _popupAjoute() async { await showDialog( context: context, barrierDismissible: true, builder: (ctx) { final nav = Navigator.of(ctx); Future.delayed(const Duration(milliseconds: 1300), () { if (nav.canPop()) nav.pop(); }); return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Padding( padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.green.withValues(alpha: 0.12), shape: BoxShape.circle, ), child: const Icon(Icons.check_circle, color: Colors.green, size: 44), ), const SizedBox(height: 16), const Text('Produit ajouté', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700)), ], ), ), ); }, ); } @override Widget build(BuildContext context) { // La caméra (et donc la demande d'autorisation) n'est construite que // lorsque l'onglet Scanner est réellement affiché. if (!widget.active) { return const ColoredBox(color: Colors.black); } return Scaffold( backgroundColor: Colors.black, floatingActionButton: FloatingActionButton.extended( onPressed: _traite ? null : _ajouterManuel, backgroundColor: Colors.white, foregroundColor: AppTheme.noir, icon: const Icon(Icons.add), label: const Text('Ajout manuel'), ), body: Stack( fit: StackFit.expand, children: [ MobileScanner( controller: _controller, onDetect: _onDetect, errorBuilder: (context, error) => _erreurCamera(error), ), _cadre(), _bandeauHaut(), if (_traite) Container( color: Colors.black54, child: const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(color: Colors.white), SizedBox(height: 16), Text('Recherche du produit…', style: TextStyle(color: Colors.white, fontSize: 16)), ], ), ), ), ], ), ); } Widget _bandeauHaut() { return SafeArea( child: Align( alignment: Alignment.topCenter, child: Container( margin: const EdgeInsets.only(top: 24), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.55), borderRadius: BorderRadius.circular(30), ), child: const Text( 'Vise le code-barres du produit', style: TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600), ), ), ), ); } Widget _cadre() { return Center( child: Container( width: 260, height: 170, decoration: BoxDecoration( border: Border.all(color: AppTheme.accent, width: 3), borderRadius: BorderRadius.circular(20), ), ), ); } Widget _erreurCamera(MobileScannerException error) { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.no_photography_outlined, color: Colors.white70, size: 56), const SizedBox(height: 16), const Text( 'Accès à la caméra impossible.\nAutorise la caméra dans les réglages.', textAlign: TextAlign.center, style: TextStyle(color: Colors.white70, fontSize: 15), ), const SizedBox(height: 20), FilledButton( onPressed: () => _controller.start(), child: const Text('Réessayer'), ), ], ), ), ); } }