import 'package:flutter/foundation.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; import 'package:mymuseum_visitapp/Models/visitContext.dart'; import 'package:mymuseum_visitapp/Services/assistantService.dart'; import 'package:mymuseum_visitapp/Services/glasses_tts_service.dart'; import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart'; import 'package:mymuseum_visitapp/constants.dart'; /// Décode les QR codes depuis les photos capturées par les lunettes. /// /// Déclencheur : bouton hardware des lunettes → MetaGlassesService.requestPhotoCapture() /// → capturePhoto() retourne un chemin fichier → analyzeImage() → stream barcodes /// /// Sur QR valide (format web.mymuseum.be ou web.myinfomate.be) : /// - Appelle AssistantService avec un prompt de résumé de la section /// - Joue la réponse via GlassesTtsService /// /// Anti-spam : 10s de cooldown entre deux traitements du même QR. class GlassesQrScannerService { GlassesQrScannerService._(); static final GlassesQrScannerService instance = GlassesQrScannerService._(); static const int _cooldownMs = 10000; static final RegExp _urlPattern1 = RegExp(r'https://web\.mymuseum\.be/([^/]+)/([^/]+)/([^/\s]+)'); static final RegExp _urlPattern2 = RegExp(r'https://web\.myinfomate\.be/([^/]+)/([^/]+)/([^/\s]+)'); VisitAppContext? _visitAppContext; AssistantService? _assistantService; MobileScannerController? _scannerController; final Map _lastScanTime = {}; void start({required VisitAppContext visitAppContext}) { _visitAppContext = visitAppContext; _assistantService = AssistantService(visitAppContext: visitAppContext); _scannerController = MobileScannerController(); _scannerController!.barcodes.listen((capture) { for (final barcode in capture.barcodes) { final raw = barcode.rawValue; if (raw != null) _processRawValue(raw); } }); // meta_wearables_dat : capturePhoto() retourne un CapturedPhoto avec .path MetaGlassesService.instance.onPhotoCaptured = _onPhotoCaptured; debugPrint('[GlassesQrScannerService] Started'); } void stop() { MetaGlassesService.instance.onPhotoCaptured = null; _scannerController?.dispose(); _scannerController = null; debugPrint('[GlassesQrScannerService] Stopped'); } Future _onPhotoCaptured(String photoPath) async { if (_visitAppContext == null || _scannerController == null) return; try { // analyzeImage déclenche le stream .barcodes si un QR est trouvé await _scannerController!.analyzeImage(photoPath); } catch (e) { debugPrint('[GlassesQrScannerService] analyzeImage error: $e'); } } void _processRawValue(String raw) { final sectionId = _extractSectionId(raw); if (sectionId == null) { debugPrint('[GlassesQrScannerService] QR not recognized: $raw'); return; } final ctx = _visitAppContext!; if (ctx.sectionIds != null && !ctx.sectionIds!.contains(sectionId)) { debugPrint('[GlassesQrScannerService] Section $sectionId not in current config'); return; } final now = DateTime.now().millisecondsSinceEpoch; if ((now - (_lastScanTime[sectionId] ?? 0)) < _cooldownMs) return; _lastScanTime[sectionId] = now; _explainSection(sectionId); } String? _extractSectionId(String raw) { final m1 = _urlPattern1.firstMatch(raw); if (m1 != null) return m1.group(3); final m2 = _urlPattern2.firstMatch(raw); if (m2 != null) return m2.group(3); if (_visitAppContext?.sectionIds?.contains(raw) == true) return raw; return null; } Future _explainSection(String sectionId) async { debugPrint('[GlassesQrScannerService] Explaining section: $sectionId'); try { final response = await _assistantService!.chat( message: 'Le visiteur vient de scanner le QR code de la section "$sectionId". ' 'Présente-lui ce contenu de façon engageante en 3 phrases maximum.', configurationId: _visitAppContext?.configuration?.id, ); if (response.reply.isNotEmpty) { await GlassesTtsService.instance.speak( response.reply, apiKey: kElevenLabsApiKey, voiceId: kElevenLabsVoiceId, ); } } catch (e) { debugPrint('[GlassesQrScannerService] explainSection error: $e'); } } }