OpenContacts/lib/widgets/messages/message_audio_player.dart
2024-07-15 00:23:04 -04:00

234 lines
9 KiB
Dart

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<MessageAudioPlayer> createState() => _MessageAudioPlayerState();
}
class _MessageAudioPlayerState extends State<MessageAudioPlayer> 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<AudioCacheClient>(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<AudioCacheClient>(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<PlayerState>(
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,
),
],
),
],
);
},
),
);
}
}