import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:just_audio/just_audio.dart'; import 'package:provider/provider.dart'; import 'package:OpenContacts/auxiliary.dart'; import 'package:OpenContacts/clients/audio_cache_client.dart'; import 'package:OpenContacts/models/message.dart'; import 'package:OpenContacts/widgets/messages/message_state_indicator.dart'; class MessageAudioPlayer extends StatefulWidget { const MessageAudioPlayer({required this.message, this.foregroundColor, super.key}); final Message message; final Color? foregroundColor; @override State createState() => _MessageAudioPlayerState(); } class _MessageAudioPlayerState extends State with WidgetsBindingObserver { final AudioPlayer _audioPlayer = AudioPlayer(); Future? _audioFileFuture; double _sliderValue = 0; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused) { _audioPlayer.stop(); } } @override void didChangeDependencies() { super.didChangeDependencies(); final audioCache = Provider.of(context); _audioFileFuture = audioCache .cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content))) .then((value) => _audioPlayer.setFilePath(value.path)) .whenComplete(() => _audioPlayer.setLoopMode(LoopMode.off)); } @override void didUpdateWidget(covariant MessageAudioPlayer oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.message.id == widget.message.id) return; final audioCache = Provider.of(context); _audioFileFuture = audioCache .cachedNetworkAudioFile(AudioClipContent.fromMap(jsonDecode(widget.message.content))) .then((value) async { final path = _audioPlayer.setFilePath(value.path); await _audioPlayer.setLoopMode(LoopMode.off); await _audioPlayer.pause(); await _audioPlayer.seek(Duration.zero); return path; }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _audioPlayer.dispose().onError((error, stackTrace) {}); super.dispose(); } Widget _createErrorWidget(String error) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( Icons.error_outline, color: Theme.of(context).colorScheme.error, ), const SizedBox( height: 4, ), Text( error, textAlign: TextAlign.center, softWrap: true, maxLines: 3, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.error), ), ], ), ); } @override Widget build(BuildContext context) { return IntrinsicWidth( child: StreamBuilder( stream: _audioPlayer.playerStateStream, builder: (context, snapshot) { if (snapshot.hasError) { FlutterError.reportError(FlutterErrorDetails(exception: snapshot.error!, stack: snapshot.stackTrace)); return _createErrorWidget("Failed to load audio-message."); } final playerState = snapshot.data; return Column( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ FutureBuilder( future: _audioFileFuture, builder: (context, fileSnapshot) { if (fileSnapshot.hasError) { FlutterError.reportError( FlutterErrorDetails(exception: fileSnapshot.error!, stack: fileSnapshot.stackTrace)); return const IconButton( icon: Icon(Icons.warning), tooltip: "Failed to load audio-message.", onPressed: null, ); } return IconButton( onPressed: fileSnapshot.hasData && snapshot.hasData && playerState != null && playerState.processingState != ProcessingState.loading ? () { switch (playerState.processingState) { case ProcessingState.idle: case ProcessingState.loading: case ProcessingState.buffering: break; case ProcessingState.ready: if (playerState.playing) { _audioPlayer.pause(); } else { _audioPlayer.play(); } break; case ProcessingState.completed: _audioPlayer.seek(Duration.zero); _audioPlayer.play(); break; } } : null, color: widget.foregroundColor, icon: Icon( ((_audioPlayer.duration ?? const Duration(days: 9999)) - _audioPlayer.position) .inMilliseconds < 10 ? Icons.replay : ((playerState?.playing ?? false) ? Icons.pause : Icons.play_arrow), ), ); }, ), StreamBuilder( stream: _audioPlayer.positionStream, builder: (context, snapshot) { _sliderValue = _audioPlayer.duration == null ? 0 : (_audioPlayer.position.inMilliseconds / (_audioPlayer.duration!.inMilliseconds)) .clamp(0, 1); return StatefulBuilder( // Not sure if this makes sense here... builder: (context, setState) { return SliderTheme( data: SliderThemeData( inactiveTrackColor: widget.foregroundColor?.withAlpha(100), ), child: Slider( thumbColor: widget.foregroundColor, value: _sliderValue, min: 0.0, max: 1.0, onChanged: (value) async { _audioPlayer.pause(); setState(() { _sliderValue = value; }); _audioPlayer.seek( Duration( milliseconds: (value * (_audioPlayer.duration?.inMilliseconds ?? 0)).round(), ), ); }, ), ); }, ); }, ) ], ), Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ const SizedBox( width: 4, ), StreamBuilder( stream: _audioPlayer.positionStream, builder: (context, snapshot) { return Text( "${snapshot.data?.format() ?? "??"}/${_audioPlayer.duration?.format() ?? "??"}", style: Theme.of(context) .textTheme .bodySmall ?.copyWith(color: widget.foregroundColor?.withAlpha(150)), ); }, ), const Spacer(), MessageStateIndicator( message: widget.message, foregroundColor: widget.foregroundColor, ), ], ), ], ); }, ), ); } }