diff --git a/Cargo.lock b/Cargo.lock index 4fe6158..8bad3db 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" name = "audrey" version = "0.1.0" dependencies = [ + "async-broadcast", "async-channel", "base16ct", "bindgen", diff --git a/Cargo.toml b/Cargo.toml index 2ce638b..fd6e52e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] adw = { version = "0.7.0", package = "libadwaita", features = ["v1_6"] } +async-broadcast = "0.7.1" async-channel = "2.3.1" base16ct = { version = "0.2.0", features = ["std"] } chrono = { version = "0.4.38", features = ["serde"] } diff --git a/src/broadcast.rs b/src/broadcast.rs new file mode 100644 index 0000000..1490ecb --- /dev/null +++ b/src/broadcast.rs @@ -0,0 +1,34 @@ +// nice wrapper to have a Default impl for a (Sender, InactiveReceiver) pair + +use async_broadcast::{broadcast, InactiveReceiver, Receiver, Sender, TrySendError}; + +pub struct Broadcast { + pub sender: Sender, + inactive_receiver: InactiveReceiver, +} + +impl Default for Broadcast { + fn default() -> Self { + let (sender, receiver) = broadcast(N); + Self { + sender, + inactive_receiver: receiver.deactivate(), + } + } +} + +impl Broadcast where T: Clone { + pub fn receiver(&self) -> Receiver { + self.inactive_receiver.activate_cloned() + } + + // like sender.try_broadcast, but ignores if there aren't any active listeners + pub fn try_broadcast(&self, msg: T) -> Result<(), TrySendError> { + match self.sender.try_broadcast(msg) { + Err(TrySendError::Inactive(_)) => Ok(()), // ignore + Err(err) => Err(err), + Ok(Some(_)) => unreachable!("we do not enable overflow mode"), + Ok(None) => Ok(()), + } + } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..61c1dc4 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,29 @@ +#[derive(Clone, Debug)] +pub enum Event { + PlaybinVolumeChanged, + PlaybinMutedChanged, + PlaybinPausedChanged, + PlaybinCurrentEntryChanged, + + PlaybinEntryInserted(usize), + PlaybinStopped, + PlaybinEntryRemoved(usize), + + PlaybinFileStarted, +} + +use adw::prelude::*; +use gtk::glib; + +pub fn spawn_object_listener>( + mut receiver: async_broadcast::Receiver, + obj: &O, + mut f: impl FnMut(O, Event) + 'static, +) { + let weak = obj.downgrade(); + glib::spawn_future_local(async move { + while let Some(obj) = weak.upgrade() { + f(obj, receiver.recv_direct().await.unwrap()); + } + }); +} diff --git a/src/main.rs b/src/main.rs index 3a61aa3..ea0e337 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,9 @@ pub type Playbin = playbin::Playbin; mod signal; pub use signal::{Signal, SignalEmitter, SignalHandler}; +pub mod event; +pub use event::Event; + use gettextrs::{bind_textdomain_codeset, bindtextdomain, setlocale, textdomain, LocaleCategory}; use gtk::prelude::*; use gtk::{gio, glib}; diff --git a/src/playbin.rs b/src/playbin.rs index 1499d2b..d6dcdaa 100644 --- a/src/playbin.rs +++ b/src/playbin.rs @@ -1,5 +1,6 @@ use crate::mpv; use crate::signal::{Signal, SignalEmitter}; +use crate::Event; use event_listener::EventListener; use std::cell::{Ref, RefCell}; use tracing::{event, span, Level}; @@ -20,20 +21,19 @@ pub struct Playbin { mpv: mpv::Handle, entries: RefCell>, - volume_changed: SignalEmitter, - muted_changed: SignalEmitter, - paused_changed: SignalEmitter, - current_entry_changed: SignalEmitter, + sender: async_broadcast::Sender, - entry_inserted: SignalEmitter, stopped: SignalEmitter, entry_removed: SignalEmitter, file_started: SignalEmitter, } -impl Default for Playbin { - fn default() -> Self { +impl Playbin +where + E: PlaybinEntry, +{ + pub fn new(sender: async_broadcast::Sender) -> Self { let mpv = mpv::Handle::new(); mpv.set_property("audio-client-name", "audrey").unwrap(); mpv.set_property("user-agent", crate::USER_AGENT).unwrap(); @@ -53,24 +53,15 @@ impl Default for Playbin { mpv, entries: RefCell::new(vec![]), - volume_changed: Default::default(), - muted_changed: Default::default(), - paused_changed: Default::default(), - current_entry_changed: Default::default(), + sender, - entry_inserted: Default::default(), stopped: Default::default(), entry_removed: Default::default(), file_started: Default::default(), } } -} -impl Playbin -where - E: PlaybinEntry, -{ pub fn volume(&self) -> i64 { self.mpv.get_property("volume").unwrap() } @@ -138,7 +129,9 @@ where entries.push(entry); drop(entries); - self.entry_inserted.emit(self, index); + self.sender + .try_broadcast(Event::PlaybinEntryInserted(index)) + .unwrap(); } pub fn insert_entry(&self, index: usize, entry: E) { @@ -149,7 +142,9 @@ where entries.insert(index, entry); drop(entries); - self.entry_inserted.emit(self, index); + self.sender + .try_broadcast(Event::PlaybinEntryInserted(index)) + .unwrap(); } // stop playback and clear playlist @@ -159,7 +154,7 @@ where entries.clear(); drop(entries); - self.stopped.emit(self, ()); + self.sender.try_broadcast(Event::PlaybinStopped).unwrap(); } pub fn remove_entry(&self, index: usize) { @@ -170,7 +165,9 @@ where entries.remove(index); drop(entries); - self.entry_removed.emit(self, index); + self.sender + .try_broadcast(Event::PlaybinEntryRemoved(index)) + .unwrap(); } pub fn move_entry(&self, _from: usize, _to: usize) { @@ -192,25 +189,33 @@ where mpv::Event::PropertyChange(event) => match event.reply_userdata { 0 => { assert_eq!(&event.name, "volume"); - self.volume_changed.emit(self, ()); + self.sender + .try_broadcast(Event::PlaybinVolumeChanged) + .unwrap(); event!(Level::DEBUG, "volume change {}", self.volume()); } 1 => { assert_eq!(&event.name, "mute"); - self.muted_changed.emit(self, ()); + self.sender + .try_broadcast(Event::PlaybinMutedChanged) + .unwrap(); event!(Level::DEBUG, "mute state change to {}", self.muted()); } 2 => { assert_eq!(&event.name, "pause"); - self.paused_changed.emit(self, ()); + self.sender + .try_broadcast(Event::PlaybinPausedChanged) + .unwrap(); event!(Level::DEBUG, "pause state change to {}", self.paused()); } 3 => { assert_eq!(&event.name, "playlist-pos"); - self.current_entry_changed.emit(self, ()); + self.sender + .try_broadcast(Event::PlaybinCurrentEntryChanged) + .unwrap(); event!( Level::DEBUG, "playlist-pos change {:?}", @@ -235,7 +240,9 @@ where mpv::Event::StartFile(_) => { // since we set up the hook before, the current song is guaranteed not to change // under our feet - self.file_started.emit(self, ()); + self.sender + .try_broadcast(Event::PlaybinFileStarted) + .unwrap(); // sanity check assert_eq!( @@ -248,22 +255,6 @@ where } } - pub fn volume_changed(&self) -> Signal<'_, Self, ()> { - self.volume_changed.signal() - } - - pub fn muted_changed(&self) -> Signal<'_, Self, ()> { - self.muted_changed.signal() - } - - pub fn current_entry_changed(&self) -> Signal<'_, Self, ()> { - self.current_entry_changed.signal() - } - - pub fn entry_inserted(&self) -> Signal<'_, Self, usize> { - self.entry_inserted.signal() - } - pub fn stopped(&self) -> Signal<'_, Self, ()> { self.stopped.signal() } diff --git a/src/ui/play_queue.rs b/src/ui/play_queue.rs index e6510e4..98194fa 100644 --- a/src/ui/play_queue.rs +++ b/src/ui/play_queue.rs @@ -61,7 +61,7 @@ mod imp { #[template_callback] fn on_song_list_setup(&self, item: >k::ListItem, _factory: >k::SignalListItemFactory) { - let child = super::Song::new(self.playbin.borrow().as_ref().unwrap()); + let child = super::Song::new(Some(&self.obj().window())); child.set_draggable(true); child.set_show_position(true); @@ -75,10 +75,7 @@ mod imp { fn on_song_list_bind(&self, item: >k::ListItem, _factory: >k::SignalListItemFactory) { let child = item.child().and_downcast::().unwrap(); - child.bind( - item.position(), - item.item().unwrap().downcast_ref::().unwrap(), - ); + child.bind(item.position(), &self.obj().window()); } #[template_callback] @@ -106,6 +103,7 @@ mod imp { } use crate::Playbin; +use adw::prelude::*; use adw::subclass::prelude::*; use gtk::glib; use std::rc::Rc; @@ -124,15 +122,6 @@ impl PlayQueue { .replace(Some(Rc::clone(playbin))) .is_none()); // only set once - playbin - .entry_inserted() - .connect_object(self, |playbin, play_queue, index| { - play_queue - .model() - .unwrap() - .insert(index as u32, &playbin.entries()[index]); - true - }); playbin .stopped() .connect_object(self, |_playbin, play_queue, ()| { @@ -146,4 +135,8 @@ impl PlayQueue { true }); } + + fn window(&self) -> crate::ui::Window { + self.root().unwrap().dynamic_cast().unwrap() + } } diff --git a/src/ui/play_queue/song.rs b/src/ui/play_queue/song.rs index 3950a39..e281aaa 100644 --- a/src/ui/play_queue/song.rs +++ b/src/ui/play_queue/song.rs @@ -1,10 +1,8 @@ mod imp { - use crate::signal::SignalHandler; - use crate::{Playbin, PlaybinSong}; + use crate::PlaybinSong; use glib::subclass::InitializingObject; use gtk::{gdk, gio, glib, prelude::*, subclass::prelude::*}; use std::cell::{Cell, RefCell}; - use std::rc::Rc; #[derive(gtk::CompositeTemplate, glib::Properties, Default)] #[template(resource = "/eu/callcc/audrey/play_queue_song.ui")] @@ -21,6 +19,8 @@ mod imp { #[property(set = Self::set_current, get)] current: Cell, + #[property(set, get)] + position: Cell, #[property(set, get)] displayed_position: Cell, @@ -29,9 +29,6 @@ mod imp { drag_pos: Cell<(i32, i32)>, drag_widget: Cell>, - - pub(super) playbin: RefCell>>, - pub(super) connection: Cell, } #[glib::object_subclass] @@ -62,6 +59,7 @@ mod imp { move |_, _, _| { self_ .obj() + .window() .playbin() .remove_entry(self_.obj().displayed_position() as usize - 1) } @@ -123,7 +121,7 @@ mod imp { fn on_drag_begin(&self, drag: &gdk::Drag) { let drag_widget = gtk::ListBox::new(); - let drag_row = super::Song::new(&self.obj().playbin()); + let drag_row = super::Song::new(None); drag_row.set_draggable(false); drag_row.set_show_position(self.obj().show_position()); drag_row.set_show_artist(self.obj().show_artist()); @@ -165,11 +163,10 @@ mod imp { } } -use crate::{Playbin, PlaybinSong}; -use adw::subclass::prelude::*; +use crate::PlaybinSong; +use adw::prelude::*; use glib::Object; use gtk::glib; -use std::rc::Rc; glib::wrapper! { pub struct Song(ObjectSubclass) @@ -178,38 +175,41 @@ glib::wrapper! { } impl Song { - pub fn new(playbin: &Rc) -> Self { + pub fn new(window: Option<&crate::ui::Window>) -> Self { let song: Self = Object::new(); - assert!(song - .imp() - .playbin - .replace(Some(Rc::clone(playbin))) - .is_none()); // only set once + if let Some(window) = window { + use crate::Event; + + crate::event::spawn_object_listener( + window.receiver(), + &song, + |song, event| match event { + Event::PlaybinCurrentEntryChanged => { + song.set_current( + song.window().playbin().current_entry() + == Some(song.position() as usize), + ); + } + + _ => {} + }, + ); + } song } - fn playbin(&self) -> Rc { - Rc::clone(self.imp().playbin.borrow().as_ref().unwrap()) + fn window(&self) -> crate::ui::Window { + self.root().unwrap().dynamic_cast().unwrap() } - pub fn bind(&self, position: u32, song: &PlaybinSong) { + pub fn bind(&self, position: u32, window: &crate::ui::Window) { self.set_displayed_position(position + 1); - self.set_song(song); - self.set_current(self.playbin().current_entry() == Some(position as usize)); - self.imp() - .connection - .replace(self.playbin().current_entry_changed().connect_object( - self, - move |playbin, song, ()| { - song.set_current(playbin.current_entry() == Some(position as usize)); - true - }, - )); + self.set_song(&window.playbin().entries()[position as usize]); + self.set_current(window.playbin().current_entry() == Some(position as usize)); + self.set_position(position); } - pub fn unbind(&self) { - self.imp().connection.take().disconnect(); - } + pub fn unbind(&self) {} } diff --git a/src/ui/window.rs b/src/ui/window.rs index dbe0d72..8dea634 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -8,7 +8,7 @@ mod imp { use std::rc::Rc; use tracing::{event, Level}; - #[derive(gtk::CompositeTemplate, glib::Properties, Default)] + #[derive(gtk::CompositeTemplate, glib::Properties)] #[template(resource = "/eu/callcc/audrey/window.ui")] #[properties(wrapper_type = super::Window)] pub struct Window { @@ -31,6 +31,28 @@ mod imp { pub(super) playbin: Rc, pub(super) api: RefCell>>, + + pub(super) sender: async_broadcast::Sender, + pub(super) inactive_receiver: async_broadcast::InactiveReceiver, + } + + impl Default for Window { + fn default() -> Self { + let (sender, receiver) = async_broadcast::broadcast(100); // TODO: constantize + + Self { + playbar: Default::default(), + play_queue: Default::default(), + can_click_shuffle_all: Cell::new(false), + playing_cover_art: Default::default(), + song: Default::default(), + setup: Default::default(), + playbin: Rc::new(Playbin::new(sender.clone())), + api: Default::default(), + sender, + inactive_receiver: receiver.deactivate(), + } + } } #[glib::object_subclass] @@ -54,6 +76,37 @@ mod imp { fn constructed(&self) { self.parent_constructed(); + crate::event::spawn_object_listener( + self.inactive_receiver.activate_cloned(), + self.obj().as_ref(), + |window, event| { + use crate::Event; + match dbg!(event) { + Event::PlaybinVolumeChanged => { + window + .imp() + .playbar + .set_volume(window.playbin().volume() as i32); + } + + Event::PlaybinMutedChanged => { + window.imp().playbar.set_mute(window.playbin().muted()); + } + + Event::PlaybinEntryInserted(index) => { + window + .imp() + .play_queue + .model() + .unwrap() + .insert(index as u32, &window.playbin().entries()[index]); + } + + _ => {} + } + }, + ); + let playbin = Rc::downgrade(&self.playbin); glib::spawn_future_local(glib::clone!(async move { loop { @@ -197,13 +250,6 @@ impl Window { .imp() .playbar .set_volume(window.imp().playbin.volume() as i32); - window.imp().playbin.volume_changed().connect_object( - &*window.imp().playbar, - |playbin, playbar, ()| { - playbar.set_volume(playbin.volume() as i32); - true - }, - ); window.imp().playbar.connect_notify_local( Some("volume"), glib::clone!( @@ -214,13 +260,6 @@ impl Window { ); window.imp().playbar.set_mute(window.imp().playbin.muted()); - window.imp().playbin.muted_changed().connect_object( - &*window.imp().playbar, - |playbin, playbar, ()| { - playbar.set_mute(playbin.muted()); - true - }, - ); window.imp().playbar.connect_notify_local( Some("mute"), glib::clone!( @@ -282,4 +321,12 @@ impl Window { window } + + pub fn playbin(&self) -> &crate::Playbin { + &self.imp().playbin + } + + pub fn receiver(&self) -> async_broadcast::Receiver { + self.imp().inactive_receiver.activate_cloned() + } }