mymuseum-visitapp/lib/Screens/Sections/GuidedPath/guided_path_audio_player.dart
2026-07-17 15:23:12 +02:00

182 lines
6.0 KiB
Dart

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<GuidedPathAudioPlayer> createState() => _GuidedPathAudioPlayerState();
}
class _GuidedPathAudioPlayerState extends State<GuidedPathAudioPlayer> {
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<void> _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<Color>(widget.accentColor),
),
),
],
),
);
}
}