import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; /// Lecteur audio inline (play/pause, waveform cliquable, durée), équivalent du /// lecteur custom de visitapp-web (`ParcoursSection` / `ProgressView`). class GuidedPathAudioPlayer extends StatefulWidget { final String url; final Color accentColor; final bool isGame; const GuidedPathAudioPlayer({ Key? key, required this.url, required this.accentColor, required this.isGame, }) : super(key: key); @override State createState() => _GuidedPathAudioPlayerState(); } class _GuidedPathAudioPlayerState extends State { final AudioPlayer _player = AudioPlayer(); Duration _position = Duration.zero; Duration _duration = Duration.zero; bool _playing = false; static const _bars = [4, 8, 13, 10, 16, 11, 7, 14, 9, 5, 12, 8, 10, 15, 7, 11, 13, 6, 14, 9, 5, 12, 10, 7, 13]; @override void initState() { super.initState(); _init(); } Future _init() async { try { await _player.setUrl(widget.url); } catch (_) {} _player.durationStream.listen((d) { if (d != null && mounted) setState(() => _duration = d); }); _player.positionStream.listen((p) { if (mounted) setState(() => _position = p); }); _player.playerStateStream.listen((s) { if (!mounted) return; if (s.processingState == ProcessingState.completed) { _player.pause(); _player.seek(Duration.zero); setState(() => _playing = false); } else { setState(() => _playing = s.playing); } }); } @override void dispose() { _player.dispose(); super.dispose(); } void _toggle() { if (_playing) { _player.pause(); } else { _player.play(); } } void _seekFraction(double f) { if (_duration.inMilliseconds > 0) { _player.seek(Duration(milliseconds: (f * _duration.inMilliseconds).round())); } } String _fmt(Duration d) { final m = d.inMinutes; final s = d.inSeconds % 60; return '$m:${s.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { final isGame = widget.isGame; final progress = _duration.inMilliseconds > 0 ? _position.inMilliseconds / _duration.inMilliseconds : 0.0; final muted = isGame ? const Color(0xFF8a8068) : const Color(0xFF6B7B86); return Container( margin: const EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: isGame ? Colors.white.withOpacity(0.06) : Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all( color: isGame ? const Color(0xFFD4AF37).withOpacity(0.2) : const Color(0xFFE8E3DA)), ), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), child: Row( children: [ GestureDetector( onTap: _toggle, child: Container( width: 44, height: 44, decoration: BoxDecoration( shape: BoxShape.circle, color: isGame ? const Color(0xFFD4AF37) : const Color(0xFF1E2A33), ), child: Icon( _playing ? Icons.pause : Icons.play_arrow, color: isGame ? const Color(0xFF1a1305) : Colors.white, size: 22, ), ), ), const SizedBox(width: 12), Expanded( child: LayoutBuilder( builder: (context, constraints) { return GestureDetector( onTapDown: (d) => _seekFraction((d.localPosition.dx / constraints.maxWidth).clamp(0.0, 1.0)), child: SizedBox( height: 20, child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ for (int i = 0; i < _bars.length; i++) Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 1), child: Container( height: _bars[i].toDouble(), decoration: BoxDecoration( borderRadius: BorderRadius.circular(1.5), color: (i / _bars.length) <= progress ? widget.accentColor : const Color(0x33646E78), ), ), ), ), ], ), ), ); }, ), ), const SizedBox(width: 12), Text( _duration.inMilliseconds > 0 ? _fmt(_duration) : '--:--', style: TextStyle(color: muted, fontSize: 12.5, fontWeight: FontWeight.w600), ), ], ), ), if (_duration.inMilliseconds > 0) ClipRRect( borderRadius: const BorderRadius.vertical(bottom: Radius.circular(16)), child: LinearProgressIndicator( value: progress.clamp(0.0, 1.0), minHeight: 3, backgroundColor: isGame ? Colors.white.withOpacity(0.06) : const Color(0xFFF0EBE3), valueColor: AlwaysStoppedAnimation(widget.accentColor), ), ), ], ), ); } }