diff --git a/lib/widgets/chat_page/render_event.dart b/lib/widgets/chat_page/render_event.dart index c4a68f6..747c3b7 100644 --- a/lib/widgets/chat_page/render_event.dart +++ b/lib/widgets/chat_page/render_event.dart @@ -27,6 +27,7 @@ import "package:nexus/widgets/chat_page/lazy_loading/message_displayname.dart"; import "package:nexus/widgets/link_preview.dart"; import "package:nexus/widgets/loading.dart"; import "package:nexus/widgets/players/video.dart"; +import "package:nexus/widgets/players/audio.dart"; import "package:timeago/timeago.dart"; import "package:flutter_linkify/flutter_linkify.dart"; @@ -120,10 +121,7 @@ class RenderEvent extends ConsumerWidget { : colorScheme.surfaceContainer, child: Padding( - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: 8, - ), + padding: EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -238,7 +236,10 @@ class RenderEvent extends ConsumerWidget { :final info, ) => VideoPlayer(url, info), - // TODO: Support audio + AudioMessageContent( + :final info, + ) => + AudioPlayer(url, info), // FileMessageContent( // :final info, // ) => diff --git a/lib/widgets/players/audio.dart b/lib/widgets/players/audio.dart new file mode 100644 index 0000000..a851035 --- /dev/null +++ b/lib/widgets/players/audio.dart @@ -0,0 +1,101 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:media_kit/media_kit.dart"; +import "package:nexus/helpers/extensions/get_headers.dart"; +import "package:nexus/models/info/audio.dart"; + +class AudioPlayer extends HookConsumerWidget { + final Uri url; + final AudioInfo? info; + + const AudioPlayer(this.url, this.info, {super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final player = useMemoized( + () => Player( + configuration: PlayerConfiguration(bufferSize: 128 * 1024 * 1024), + ), + ); + + final playing = useState(false); + final position = useState(Duration.zero); + final duration = useState(Duration.zero); + + useEffect(() { + scheduleMicrotask(() async { + await player.open( + Media(url.toString(), httpHeaders: ref.headers), + play: false, + ); + + player.stream.playing.listen((value) { + playing.value = value; + }); + + player.stream.position.listen((value) { + position.value = value; + }); + + player.stream.duration.listen((value) { + duration.value = value; + }); + }); + + return player.dispose; + }, []); + + String format(Duration duration) { + final minutes = duration.inMinutes + .remainder(60) + .toString() + .padLeft(2, "0"); + final seconds = duration.inSeconds + .remainder(60) + .toString() + .padLeft(2, "0"); + + return "$minutes:$seconds"; + } + + return Card( + color: Theme.of(context).colorScheme.surfaceContainer, + child: Padding( + padding: EdgeInsetsGeometry.only(left: 8, right: 16), + child: Row( + children: [ + IconButton( + onPressed: player.playOrPause, + icon: Icon( + playing.value ? Icons.pause_circle : Icons.play_circle, + ), + ), + SizedBox(width: 8), + Text( + format(position.value), + style: Theme.of(context).textTheme.bodySmall, + ), + Expanded( + child: Slider( + min: 0, + max: duration.value.inMilliseconds <= 0 + ? 1 + : duration.value.inMilliseconds.toDouble(), + value: position.value.inMilliseconds.toDouble(), + onChanged: (value) => + player.seek(Duration(milliseconds: value.toInt())), + ), + ), + Text( + format(duration.value), + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ); + } +}