diff --git a/data/resources/icons/scalable/actions/pause-symbolic.svg b/data/resources/icons/scalable/actions/pause-symbolic.svg
new file mode 100644
index 00000000..6ba52e09
--- /dev/null
+++ b/data/resources/icons/scalable/actions/pause-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/resources/icons/scalable/actions/play-symbolic.svg b/data/resources/icons/scalable/actions/play-symbolic.svg
new file mode 100644
index 00000000..f3f28d63
--- /dev/null
+++ b/data/resources/icons/scalable/actions/play-symbolic.svg
@@ -0,0 +1,2 @@
+
+
diff --git a/data/resources/resources.gresource.xml b/data/resources/resources.gresource.xml
index 575694b6..ce2b0019 100644
--- a/data/resources/resources.gresource.xml
+++ b/data/resources/resources.gresource.xml
@@ -35,6 +35,8 @@
icons/scalable/actions/menu-primary-symbolic.svg
icons/scalable/actions/menu-secondary-symbolic.svg
icons/scalable/actions/more-symbolic.svg
+ icons/scalable/actions/pause-symbolic.svg
+ icons/scalable/actions/play-symbolic.svg
icons/scalable/actions/refresh-symbolic.svg
icons/scalable/actions/remove-symbolic.svg
icons/scalable/actions/restore-symbolic.svg
diff --git a/data/resources/stylesheet/_components.scss b/data/resources/stylesheet/_components.scss
index 320f37e0..6ae56b44 100644
--- a/data/resources/stylesheet/_components.scss
+++ b/data/resources/stylesheet/_components.scss
@@ -166,3 +166,9 @@ user-page scrolledwindow > viewport > clamp > box {
margin: 12px;
border-spacing: 24px;
}
+
+audio-player waveform {
+ &:focus {
+ color: var(--accent-color);
+ }
+}
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 98d65903..8470ddf6 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -28,6 +28,7 @@ src/components/dialogs/room_preview.rs
src/components/dialogs/room_preview.blp
src/components/dialogs/user_profile.blp
src/components/offline_banner.rs
+src/components/media/audio_player/mod.rs
src/components/media/content_viewer.rs
src/components/media/location_viewer.rs
src/components/pill/at_room.rs
diff --git a/src/components/media/audio_player.blp b/src/components/media/audio_player.blp
deleted file mode 100644
index e2dcb65b..00000000
--- a/src/components/media/audio_player.blp
+++ /dev/null
@@ -1,8 +0,0 @@
-using Gtk 4.0;
-using Adw 1;
-
-template $AudioPlayer: Adw.Bin {
- Gtk.MediaControls {
- media-stream: bind template.media-file;
- }
-}
diff --git a/src/components/media/audio_player.rs b/src/components/media/audio_player.rs
deleted file mode 100644
index 9a986a50..00000000
--- a/src/components/media/audio_player.rs
+++ /dev/null
@@ -1,105 +0,0 @@
-use adw::{prelude::*, subclass::prelude::*};
-use gtk::{gio, glib};
-
-use crate::utils::BoundObject;
-
-mod imp {
- use std::cell::Cell;
-
- use glib::subclass::InitializingObject;
-
- use super::*;
-
- #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
- #[template(resource = "/org/gnome/Fractal/ui/components/media/audio_player.ui")]
- #[properties(wrapper_type = super::AudioPlayer)]
- pub struct AudioPlayer {
- /// The media file to play.
- #[property(get, set = Self::set_media_file, explicit_notify, nullable)]
- media_file: BoundObject,
- /// Whether to play the media automatically.
- #[property(get, set = Self::set_autoplay, explicit_notify)]
- autoplay: Cell,
- }
-
- #[glib::object_subclass]
- impl ObjectSubclass for AudioPlayer {
- const NAME: &'static str = "AudioPlayer";
- type Type = super::AudioPlayer;
- type ParentType = adw::Bin;
-
- fn class_init(klass: &mut Self::Class) {
- Self::bind_template(klass);
- }
-
- fn instance_init(obj: &InitializingObject) {
- obj.init_template();
- }
- }
-
- #[glib::derived_properties]
- impl ObjectImpl for AudioPlayer {}
-
- impl WidgetImpl for AudioPlayer {}
- impl BinImpl for AudioPlayer {}
-
- impl AudioPlayer {
- /// Set the media file to play.
- fn set_media_file(&self, media_file: Option) {
- if self.media_file.obj() == media_file {
- return;
- }
-
- self.media_file.disconnect_signals();
-
- if let Some(media_file) = media_file {
- let mut handlers = Vec::new();
-
- if self.autoplay.get() {
- let prepared_handler = media_file.connect_prepared_notify(|media_file| {
- if media_file.is_prepared() {
- media_file.play();
- }
- });
- handlers.push(prepared_handler);
- }
-
- self.media_file.set(media_file, handlers);
- }
-
- self.obj().notify_media_file();
- }
-
- /// Set whether to play the media automatically.
- fn set_autoplay(&self, autoplay: bool) {
- if self.autoplay.get() == autoplay {
- return;
- }
-
- self.autoplay.set(autoplay);
- self.obj().notify_autoplay();
- }
- }
-}
-
-glib::wrapper! {
- /// A widget displaying a video media file.
- pub struct AudioPlayer(ObjectSubclass)
- @extends gtk::Widget, adw::Bin,
- @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
-}
-
-impl AudioPlayer {
- /// Create a new audio player.
- pub fn new() -> Self {
- glib::Object::new()
- }
-
- /// Set the file to play.
- ///
- /// This is a convenience method that calls
- /// [`AudioPlayer::set_media_file()`].
- pub(crate) fn set_file(&self, file: Option<&gio::File>) {
- self.set_media_file(file.map(gtk::MediaFile::for_file));
- }
-}
diff --git a/src/components/media/audio_player/mod.blp b/src/components/media/audio_player/mod.blp
new file mode 100644
index 00000000..5bb6b249
--- /dev/null
+++ b/src/components/media/audio_player/mod.blp
@@ -0,0 +1,92 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $AudioPlayer: Adw.BreakpointBin {
+ margin-start: 6;
+ margin-end: 6;
+ width-request: 200;
+ height-request: 100;
+
+ Gtk.Box {
+ orientation: vertical;
+ spacing: 6;
+
+ Gtk.Box {
+ spacing: 6;
+
+ Gtk.Label position_label {
+ styles [
+ "caption",
+ ]
+ }
+
+ Gtk.Overlay {
+ $Waveform waveform {
+ hexpand: true;
+ seek => $seek() swapped;
+ }
+
+ [overlay]
+ Adw.Spinner spinner {
+ visible: false;
+ height-request: 20;
+ width-request: 20;
+ halign: center;
+ valign: center;
+ }
+
+ [overlay]
+ Gtk.Image error_img {
+ visible: false;
+ icon-name: "error-symbolic";
+ halign: center;
+ valign: center;
+ }
+ }
+
+ Gtk.Label remaining_label {
+ styles [
+ "caption",
+ ]
+ }
+ }
+
+ Gtk.Box bottom_box {
+ spacing: 6;
+
+ Adw.Bin play_button_bin {
+ child: Gtk.Button play_button {
+ halign: center;
+ clicked => $toggle_playing() swapped;
+
+ styles [
+ "flat",
+ ]
+ };
+ }
+
+ Gtk.Label filename_label {
+ hexpand: true;
+ xalign: 0.0;
+ ellipsize: end;
+ }
+
+ Gtk.Label position_label_narrow {
+ visible: false;
+ halign: end;
+ label: bind position_label.label;
+
+ styles [
+ "caption",
+ ]
+ }
+ }
+ }
+}
+
+Gtk.SizeGroup {
+ widgets [
+ position_label,
+ play_button_bin,
+ ]
+}
diff --git a/src/components/media/audio_player/mod.rs b/src/components/media/audio_player/mod.rs
new file mode 100644
index 00000000..cf16aa12
--- /dev/null
+++ b/src/components/media/audio_player/mod.rs
@@ -0,0 +1,609 @@
+use std::time::Duration;
+
+use adw::{prelude::*, subclass::prelude::*};
+use gettextrs::gettext;
+use gtk::{gio, glib, glib::clone};
+use tracing::warn;
+
+mod waveform;
+mod waveform_paintable;
+
+use self::waveform::Waveform;
+use crate::{
+ session::model::Session,
+ spawn,
+ utils::{
+ File, LoadingState,
+ matrix::{AudioMessageExt, MediaMessage, MessageCacheKey},
+ media::{
+ self, MediaFileError,
+ audio::{generate_waveform, load_audio_info},
+ },
+ },
+};
+
+mod imp {
+ use std::cell::{Cell, RefCell};
+
+ use glib::subclass::InitializingObject;
+
+ use super::*;
+
+ #[derive(Debug, Default, gtk::CompositeTemplate, glib::Properties)]
+ #[template(resource = "/org/gnome/Fractal/ui/components/media/audio_player/mod.ui")]
+ #[properties(wrapper_type = super::AudioPlayer)]
+ pub struct AudioPlayer {
+ #[template_child]
+ position_label: TemplateChild,
+ #[template_child]
+ waveform: TemplateChild,
+ #[template_child]
+ spinner: TemplateChild,
+ #[template_child]
+ error_img: TemplateChild,
+ #[template_child]
+ remaining_label: TemplateChild,
+ #[template_child]
+ bottom_box: TemplateChild,
+ #[template_child]
+ play_button: TemplateChild,
+ #[template_child]
+ filename_label: TemplateChild,
+ #[template_child]
+ position_label_narrow: TemplateChild,
+ /// The source to play.
+ source: RefCell