182 lines
6.0 KiB
Dart
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),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|