mymuseum-visitapp/lib/Components/VoiceModeSheet.dart
2026-07-17 15:23:12 +02:00

248 lines
9.3 KiB
Dart

import 'package:flutter/material.dart';
import 'package:mymuseum_visitapp/Models/visitContext.dart';
import 'package:mymuseum_visitapp/Services/meta_glasses_service.dart';
import 'package:mymuseum_visitapp/Services/voice_controller.dart';
import 'package:mymuseum_visitapp/constants.dart';
import 'package:provider/provider.dart';
/// Bottom sheet de sélection et gestion du mode vocal.
///
/// Accessible depuis GlassesStatusWidget (icône top-right) ou depuis
/// les settings. Affiche :
/// - État de connexion des lunettes (avec bouton connect/disconnect)
/// - Tuiles de sélection de mode (vocal seul / lunettes)
/// - Bouton de désactivation si un mode est actif
class VoiceModeSheet extends StatelessWidget {
final VisitAppContext visitAppContext;
const VoiceModeSheet({super.key, required this.visitAppContext});
static Future<void> show(BuildContext context, {required VisitAppContext visitAppContext}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => VoiceModeSheet(visitAppContext: visitAppContext),
);
}
@override
Widget build(BuildContext context) {
return Consumer<VoiceController>(
builder: (context, controller, _) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF1E1E2E),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle
Center(
child: Container(
width: 40, height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 20),
// Titre
const Text(
'Assistant vocal',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Section lunettes — toujours visible
_GlassesConnectionTile(),
const SizedBox(height: 20),
// Modes — uniquement si isAssistant
if (controller.isFeatureAvailable(visitAppContext)) ...[
const Text(
'Mode',
style: TextStyle(color: Colors.white54, fontSize: 12, letterSpacing: 1),
),
const SizedBox(height: 8),
_ModeTile(
icon: Icons.mic,
label: 'Mode vocal',
subtitle: 'Micro du téléphone',
targetMode: VoiceMode.voiceOnly,
controller: controller,
visitAppContext: visitAppContext,
enabled: true,
),
const SizedBox(height: 8),
ValueListenableBuilder<GlassesState>(
valueListenable: MetaGlassesService.instance.state,
builder: (context, glassesState, _) {
final connected = glassesState == GlassesState.connected ||
glassesState == GlassesState.streaming;
return _ModeTile(
icon: Icons.remove_red_eye_outlined,
label: 'Mode lunettes',
subtitle: connected ? 'Ray-Ban Meta connectées' : 'Lunettes non connectées',
targetMode: VoiceMode.glasses,
controller: controller,
visitAppContext: visitAppContext,
enabled: connected,
);
},
),
const SizedBox(height: 16),
// Bouton désactiver
if (controller.mode != VoiceMode.none)
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: controller.isActivating ? null : () => controller.deactivate(),
icon: const Icon(Icons.stop_circle_outlined, color: Colors.redAccent),
label: const Text('Désactiver', style: TextStyle(color: Colors.redAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.redAccent),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
),
),
),
] else
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text(
"L'assistant vocal n'est pas activé pour cette instance.",
style: TextStyle(color: Colors.white54, fontSize: 13),
),
),
],
),
);
},
);
}
}
class _GlassesConnectionTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<GlassesState>(
valueListenable: MetaGlassesService.instance.state,
builder: (context, state, _) {
final label = switch (state) {
GlassesState.connected => 'Connectées',
GlassesState.streaming => 'Connectées (streaming)',
GlassesState.connecting => 'Connexion...',
GlassesState.disconnected => 'Non connectées',
};
final color = (state == GlassesState.connected || state == GlassesState.streaming)
? Colors.greenAccent
: state == GlassesState.connecting
? Colors.orangeAccent
: Colors.white38;
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white10,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(Icons.remove_red_eye_outlined, color: color, size: 22),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Ray-Ban Meta', style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600)),
Text(label, style: TextStyle(color: color, fontSize: 12)),
],
),
),
if (state == GlassesState.disconnected)
TextButton(
onPressed: () => MetaGlassesService.instance.connect(),
child: const Text('Connecter', style: TextStyle(color: kMainColor1)),
)
else if (state == GlassesState.connected || state == GlassesState.streaming)
TextButton(
onPressed: () => MetaGlassesService.instance.disconnect(),
child: const Text('Déconnecter', style: TextStyle(color: Colors.white54)),
)
else
const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
],
),
);
},
);
}
}
class _ModeTile extends StatelessWidget {
final IconData icon;
final String label;
final String subtitle;
final VoiceMode targetMode;
final VoiceController controller;
final VisitAppContext visitAppContext;
final bool enabled;
const _ModeTile({
required this.icon,
required this.label,
required this.subtitle,
required this.targetMode,
required this.controller,
required this.visitAppContext,
required this.enabled,
});
@override
Widget build(BuildContext context) {
final isActive = controller.mode == targetMode;
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: isActive ? kMainColor1.withValues(alpha: 0.2) : Colors.white10,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isActive ? kMainColor1 : Colors.transparent,
width: 1.5,
),
),
child: ListTile(
leading: Icon(icon, color: isActive ? kMainColor1 : enabled ? Colors.white70 : Colors.white30),
title: Text(
label,
style: TextStyle(
color: enabled ? Colors.white : Colors.white38,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Text(subtitle, style: TextStyle(color: enabled ? Colors.white54 : Colors.white24, fontSize: 12)),
trailing: isActive
? const Icon(Icons.check_circle, color: kMainColor1)
: enabled
? const Icon(Icons.chevron_right, color: Colors.white38)
: const Icon(Icons.lock_outline, color: Colors.white24, size: 18),
onTap: (enabled && !controller.isActivating && !isActive)
? () => controller.activate(targetMode, visitAppContext)
: null,
),
);
}
}