import 'dart:async'; import 'package:flutter/material.dart'; import 'package:meta_wearables_dat/meta_wearables_dat.dart'; import 'package:mymuseum_visitapp/Services/Glasses/glasses_orchestrator.dart'; import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart'; import 'package:mymuseum_visitapp/constants.dart'; /// Panneau de debug pour l'intégration Ray-Ban Meta. /// À ouvrir via un bouton discret dans l'app (ex: appui long sur le logo). /// Ne pas inclure en production. class GlassesDebugPanel extends StatefulWidget { const GlassesDebugPanel({super.key}); static void show(BuildContext context) { showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.grey[900], shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (_) => const GlassesDebugPanel(), ); } @override State createState() => _GlassesDebugPanelState(); } class _GlassesDebugPanelState extends State { String _log = ''; bool _busy = false; bool _monitoring = false; final List _subs = []; @override void dispose() { for (final s in _subs) { s.cancel(); } super.dispose(); } void _addLog(String msg) { if (!mounted) return; setState(() => _log = '${DateTime.now().toIso8601String().substring(11, 19)} $msg\n$_log'); } void _toggleMonitor() { if (_monitoring) { for (final s in _subs) { s.cancel(); } _subs.clear(); setState(() => _monitoring = false); _addLog('⏹ Monitor arrêté'); return; } _subs.add(Wearables.instance.registrationStateStream.listen( (s) => _addLog('📋 registration: ${s.state} err=${s.error}'), onError: (e) => _addLog('📋 registration error: $e'), )); _subs.add(Wearables.instance.devicesStream.listen( (d) => _addLog('📱 devices: $d'), onError: (e) => _addLog('📱 devices error: $e'), )); _subs.add(Wearables.instance.streamStateStream.listen( (s) => _addLog('🎥 streamState: $s'), onError: (e) => _addLog('🎥 streamState error: $e'), )); _subs.add(Wearables.instance.videoFramesStream.listen( (f) => _addLog('🖼 videoFrame: ${f.length} bytes'), onError: (e) => _addLog('🖼 videoFrame error: $e'), )); setState(() => _monitoring = true); _addLog('▶ Monitor démarré — interagis avec les lunettes'); } Future _run(String label, Future Function() fn) async { if (_busy) return; setState(() => _busy = true); _addLog('▶ $label'); try { await fn(); _addLog('✓ $label'); } catch (e) { _addLog('✗ $label: $e'); } finally { setState(() => _busy = false); } } @override Widget build(BuildContext context) { return DraggableScrollableSheet( expand: false, initialChildSize: 0.75, maxChildSize: 0.95, builder: (_, scroll) => Column( children: [ // Handle Container( margin: const EdgeInsets.symmetric(vertical: 8), width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[600], borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( children: [ const Icon(Icons.bug_report, color: Colors.amber, size: 18), const SizedBox(width: 8), const Text('Glasses Debug', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), const Spacer(), // État en temps réel ValueListenableBuilder( valueListenable: MetaGlassesService.instance.state, builder: (_, state, __) { final color = state == GlassesState.streaming ? Colors.green : state == GlassesState.connected ? Colors.lightGreen : state == GlassesState.connecting ? Colors.orange : Colors.red; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.2), border: Border.all(color: color), borderRadius: BorderRadius.circular(12), ), child: Text( state.name.toUpperCase(), style: TextStyle(color: color, fontSize: 11, fontWeight: FontWeight.bold), ), ); }, ), ], ), ), const Divider(color: Colors.grey), // Boutons actions Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Wrap( spacing: 8, runSpacing: 8, children: [ _ActionButton( label: 'Activer caméra', icon: Icons.camera_alt, onTap: () => _run('requestCameraPermission + startStream', () async { await Wearables.instance.requestCameraPermission(); await Wearables.instance.startStream( videoQuality: 'MEDIUM', frameRate: 24, ); }), ), _ActionButton( label: 'Start stream', icon: Icons.videocam, onTap: () => _run('startStream (direct)', () async { await Wearables.instance.startStream( videoQuality: 'MEDIUM', frameRate: 24, ); }), ), _ActionButton( label: 'Capture photo', icon: Icons.photo_camera, onTap: () => _run('capturePhoto', () async { await MetaGlassesService.instance.requestPhotoCapture(); }), ), _ActionButton( label: 'Test TTS', icon: Icons.volume_up, onTap: () => _run('TTS test', () async { final o = activeOrchestrator; if (o == null) { _addLog('⚠ Orchestrateur non initialisé'); return; } // Stoppe l'écoute wake word pendant le TTS pour éviter le conflit audio focus await o.wakeWordEngine.stop(); try { await o.ttsEngine.speak( 'Bonjour. Je suis votre assistant de visite. Bienvenue au musée.', languageCode: 'fr-FR', ); } finally { await o.wakeWordEngine.start( onDetected: () => o.triggerConversation(), onDetectedWithCommand: (cmd) => o.triggerConversation(), ); } }), ), _ActionButton( label: _monitoring ? 'Stop monitor' : 'Event monitor', icon: _monitoring ? Icons.sensors_off : Icons.sensors, color: _monitoring ? Colors.orange : Colors.purple, onTap: _toggleMonitor, ), _ActionButton( label: 'Stop stream', icon: Icons.stop, color: Colors.red, onTap: () => _run('stopStream', () async { await Wearables.instance.stopStream(); }), ), _ActionButton( label: 'Reconnect', icon: Icons.refresh, onTap: () => _run('disconnect + connect', () async { await MetaGlassesService.instance.disconnect(); await Future.delayed(const Duration(milliseconds: 500)); await MetaGlassesService.instance.connect(); }), ), ], ), ), const Divider(color: Colors.grey), // Transcription en direct if (activeOrchestrator != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ValueListenableBuilder( valueListenable: activeOrchestrator!.isListeningForCommand, builder: (_, listening, __) => ValueListenableBuilder( valueListenable: activeOrchestrator!.lastTranscription, builder: (_, text, __) => Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: listening ? Colors.green.withValues(alpha: 0.15) : Colors.grey[850], borderRadius: BorderRadius.circular(8), border: Border.all( color: listening ? Colors.green : Colors.grey[700]!, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Icon( listening ? Icons.mic : Icons.mic_none, color: listening ? Colors.green : Colors.grey, size: 14, ), const SizedBox(width: 6), Text( listening ? 'Écoute en cours...' : 'Dernière transcription', style: TextStyle( color: listening ? Colors.green : Colors.grey, fontSize: 11, ), ), ]), if (text.isNotEmpty) ...[ const SizedBox(height: 4), Text( '"$text"', style: const TextStyle( color: Colors.white, fontSize: 13, fontStyle: FontStyle.italic, ), ), ], ], ), ), ), ), ), // Dernier texte TTS if (activeOrchestrator != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: ValueListenableBuilder( valueListenable: activeOrchestrator!.lastTtsText, builder: (_, text, __) => text.isEmpty ? const SizedBox.shrink() : Container( width: double.infinity, padding: const EdgeInsets.all(10), decoration: BoxDecoration( color: Colors.blue.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue.withValues(alpha: 0.4)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Icon(Icons.volume_up, color: Colors.blue, size: 14), const SizedBox(width: 6), const Text('Dernier TTS', style: TextStyle(color: Colors.blue, fontSize: 11)), ]), const SizedBox(height: 4), Text( text, style: const TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ), ), ), const Divider(color: Colors.grey), // Log Expanded( child: SingleChildScrollView( controller: scroll, padding: const EdgeInsets.all(12), child: _log.isEmpty ? const Text('Logs apparaîtront ici...', style: TextStyle(color: Colors.grey, fontSize: 12)) : Text( _log, style: const TextStyle( color: Colors.greenAccent, fontSize: 11, fontFamily: 'monospace', ), ), ), ), ], ), ); } } class _ActionButton extends StatelessWidget { final String label; final IconData icon; final VoidCallback onTap; final Color color; const _ActionButton({ required this.label, required this.icon, required this.onTap, this.color = Colors.blue, }); @override Widget build(BuildContext context) { return ElevatedButton.icon( style: ElevatedButton.styleFrom( backgroundColor: color.withValues(alpha: 0.15), foregroundColor: color, side: BorderSide(color: color.withValues(alpha: 0.4)), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), icon: Icon(icon, size: 16), label: Text(label, style: const TextStyle(fontSize: 12)), onPressed: onTap, ); } }