import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import '../../../core/api/api_response.dart'; import '../../../core/router/app_router.dart'; import '../../../core/theme/app_colors.dart'; import '../../machines/data/machine_repository.dart'; import '../../wash/data/wash_repository.dart'; /// Écran de scan QR → page dédiée machine. class QrScannerScreen extends ConsumerStatefulWidget { const QrScannerScreen({super.key}); @override ConsumerState createState() => _QrScannerScreenState(); } class _QrScannerScreenState extends ConsumerState { final MobileScannerController _controller = MobileScannerController( detectionSpeed: DetectionSpeed.noDuplicates, facing: CameraFacing.back, ); bool _isProcessing = false; String? _lastScannedValue; static const _frameSize = 260.0; @override void dispose() { _controller.dispose(); super.dispose(); } Future _onDetect(BarcodeCapture capture) async { if (_isProcessing) return; final value = capture.barcodes.firstOrNull?.rawValue?.trim(); if (value == null || value.isEmpty) return; if (value == _lastScannedValue) return; setState(() { _isProcessing = true; _lastScannedValue = value; }); await _controller.stop(); try { final reference = QrMachineReference.parse(value); if (!reference.isValid) { throw const FormatException('QR code invalide'); } final detail = await ref.read(machineRepositoryProvider).lookup( qrCode: reference.qrCode, machineUuid: reference.machineUuid, ); if (!mounted) return; context.pop(); context.push(AppRoutes.machineAction(detail.machine.uuid)); } on DioException catch (error) { if (!mounted) return; _showErrorAndResume(ApiResponse.errorMessage(error, fallback: 'Machine introuvable')); } on FormatException catch (error) { if (!mounted) return; _showErrorAndResume(error.message); } catch (_) { if (!mounted) return; _showErrorAndResume('Impossible de lire ce QR code'); } } Future _showErrorAndResume(String message) async { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), backgroundColor: AppColors.error), ); setState(() { _isProcessing = false; _lastScannedValue = null; }); await _controller.start(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, appBar: AppBar( title: const Text('Scanner une machine'), backgroundColor: Colors.black, foregroundColor: Colors.white, actions: [ IconButton( tooltip: 'Lampe', onPressed: () => _controller.toggleTorch(), icon: const Icon(Icons.flash_on_rounded), ), ], ), body: Stack( fit: StackFit.expand, children: [ MobileScanner( controller: _controller, onDetect: _onDetect, ), CustomPaint( painter: _ScannerOverlayPainter(frameSize: _frameSize), child: const SizedBox.expand(), ), Align( alignment: Alignment.bottomCenter, child: Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(24, 24, 24, 32), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.transparent, Colors.black.withValues(alpha: 0.9)], ), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Placez le QR code dans le cadre', textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16, ), ), const SizedBox(height: 6), Text( 'Ex. démo : LAVERIE-DEMO-001', textAlign: TextAlign.center, style: TextStyle(color: Colors.white.withValues(alpha: 0.7), fontSize: 13), ), if (_isProcessing) ...[ const SizedBox(height: 16), const SizedBox( width: 28, height: 28, child: CircularProgressIndicator(strokeWidth: 2.5, color: Colors.white), ), const SizedBox(height: 8), const Text( 'Identification de la machine…', style: TextStyle(color: Colors.white70, fontSize: 13), ), ], ], ), ), ), ], ), ); } } class _ScannerOverlayPainter extends CustomPainter { _ScannerOverlayPainter({required this.frameSize}); final double frameSize; @override void paint(Canvas canvas, Size size) { final center = Offset(size.width / 2, size.height / 2); final rect = Rect.fromCenter(center: center, width: frameSize, height: frameSize); final rrect = RRect.fromRectAndRadius(rect, const Radius.circular(20)); canvas.drawPath( Path.combine( PathOperation.difference, Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height)), Path()..addRRect(rrect), ), Paint()..color = Colors.black.withValues(alpha: 0.55), ); canvas.drawRRect( rrect, Paint() ..color = Colors.white ..style = PaintingStyle.stroke ..strokeWidth = 2.5, ); const cornerLen = 28.0; final corner = Paint() ..color = AppColors.primary ..style = PaintingStyle.stroke ..strokeWidth = 4 ..strokeCap = StrokeCap.round; canvas.drawLine(rect.topLeft, rect.topLeft + const Offset(cornerLen, 0), corner); canvas.drawLine(rect.topLeft, rect.topLeft + const Offset(0, cornerLen), corner); canvas.drawLine(rect.topRight, rect.topRight + const Offset(-cornerLen, 0), corner); canvas.drawLine(rect.topRight, rect.topRight + const Offset(0, cornerLen), corner); canvas.drawLine(rect.bottomLeft, rect.bottomLeft + const Offset(cornerLen, 0), corner); canvas.drawLine(rect.bottomLeft, rect.bottomLeft + const Offset(0, -cornerLen), corner); canvas.drawLine(rect.bottomRight, rect.bottomRight + const Offset(-cornerLen, 0), corner); canvas.drawLine(rect.bottomRight, rect.bottomRight + const Offset(0, -cornerLen), corner); } @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; }