diff --git a/resources/window.blp b/resources/window.blp index 641ec3b..a264e91 100644 --- a/resources/window.blp +++ b/resources/window.blp @@ -119,6 +119,8 @@ template $AudreyUiWindow: Adw.ApplicationWindow { margin-end: 24; styles [ "frame" ] + model: bind template.playlist_model; + playlist-pos: bind template.playlist-pos; //playbin: bind template.playbin; } }; @@ -130,6 +132,10 @@ template $AudreyUiWindow: Adw.ApplicationWindow { song: bind template.song; playing_cover_art: bind template.playing_cover_art; show_cover_art: bind $show_playbar_cover_art (stack.visible-child-name) as ; + + volume: bind template.volume bidirectional; + mute: bind template.mute bidirectional; + pause: bind template.pause bidirectional; } } } diff --git a/src/playbin.rs b/src/playbin.rs index 8092708..3ae122e 100644 --- a/src/playbin.rs +++ b/src/playbin.rs @@ -127,7 +127,7 @@ impl Playbin { "loadfile", &entry.stream_url(), "insert-at-play", - index.to_string(), + &index.to_string(), ]) .unwrap(); entries.insert(index, entry); diff --git a/src/ui/play_queue.rs b/src/ui/play_queue.rs index 98194fa..28cd2f2 100644 --- a/src/ui/play_queue.rs +++ b/src/ui/play_queue.rs @@ -4,8 +4,8 @@ pub use song::Song; mod imp { use crate::{Playbin, PlaybinSong}; use adw::{gio, glib, prelude::*, subclass::prelude::*}; - use glib::subclass::InitializingObject; - use std::cell::RefCell; + use glib::{subclass::InitializingObject, WeakRef}; + use std::cell::{Cell, RefCell}; use std::rc::Rc; use tracing::{event, Level}; @@ -14,9 +14,9 @@ mod imp { #[properties(wrapper_type = super::PlayQueue)] pub struct PlayQueue { #[property(get, set)] - pub(super) model: RefCell>, - - pub(super) playbin: RefCell>>, + pub(super) model: WeakRef, + #[property(get, set)] + _playlist_pos: Cell, } #[glib::object_subclass] @@ -39,8 +39,6 @@ mod imp { impl ObjectImpl for PlayQueue { fn constructed(&self) { self.parent_constructed(); - - self.obj().set_model(gio::ListStore::new::()); } } @@ -61,13 +59,17 @@ mod imp { #[template_callback] fn on_song_list_setup(&self, item: >k::ListItem, _factory: >k::SignalListItemFactory) { - let child = super::Song::new(Some(&self.obj().window())); + let child: super::Song = glib::Object::new(); child.set_draggable(true); child.set_show_position(true); child.set_show_artist(true); child.set_show_cover(true); + self.obj() + .bind_property("playlist-pos", &child, "playlist-pos") + .build(); + item.set_child(Some(&child)); } @@ -87,11 +89,7 @@ mod imp { #[template_callback] fn on_row_activated(&self, position: u32) { - self.playbin - .borrow() - .as_ref() - .unwrap() - .play_entry(position as usize); + self.obj().window().play_index(position as i64); } } @@ -115,27 +113,6 @@ glib::wrapper! { } impl PlayQueue { - pub fn set_playbin(&self, playbin: &Rc) { - assert!(self - .imp() - .playbin - .replace(Some(Rc::clone(playbin))) - .is_none()); // only set once - - playbin - .stopped() - .connect_object(self, |_playbin, play_queue, ()| { - play_queue.model().unwrap().remove_all(); - true - }); - playbin - .entry_removed() - .connect_object(self, |_playbin, play_queue, index| { - play_queue.model().unwrap().remove(index as u32); - 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 97a81f8..6c26298 100644 --- a/src/ui/play_queue/song.rs +++ b/src/ui/play_queue/song.rs @@ -8,6 +8,9 @@ mod imp { #[template(resource = "/eu/callcc/audrey/play_queue_song.ui")] #[properties(wrapper_type = super::Song)] pub struct Song { + #[property(get, set = Self::set_playlist_pos)] + playlist_pos: Cell, + #[property(set, get)] draggable: Cell, #[property(set, get)] @@ -57,11 +60,13 @@ mod imp { #[weak(rename_to = self_)] self, move |_, _, _| { + /* self_ .obj() .window() .playbin() - .remove_entry(self_.obj().displayed_position() as usize - 1) + .remove_entry(self_.obj().displayed_position() as usize - 1)*/ + todo!() } )) .build(); @@ -121,7 +126,7 @@ mod imp { fn on_drag_begin(&self, drag: &gdk::Drag) { let drag_widget = gtk::ListBox::new(); - let drag_row = super::Song::new(None); + let drag_row: super::Song = glib::Object::new(); drag_row.set_draggable(false); drag_row.set_show_position(self.obj().show_position()); drag_row.set_show_artist(self.obj().show_artist()); @@ -160,10 +165,18 @@ mod imp { true */ false } + + fn set_playlist_pos(&self, playlist_pos: i64) { + self.playlist_pos.set(playlist_pos); + self.obj() + .set_current(playlist_pos == self.position.get() as i64); + } } } +use crate::PlaybinSong; use adw::prelude::*; +use adw::subclass::prelude::*; use glib::Object; use gtk::glib; @@ -174,40 +187,21 @@ glib::wrapper! { } impl Song { - pub fn new(window: Option<&crate::ui::Window>) -> Self { - let song: Self = Object::new(); - - 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 window(&self) -> crate::ui::Window { self.root().unwrap().dynamic_cast().unwrap() } pub fn bind(&self, position: u32, window: &crate::ui::Window) { + let song: PlaybinSong = window + .playlist_model() + .item(position) + .unwrap() + .dynamic_cast() + .unwrap(); self.set_displayed_position(position + 1); - self.set_song(&window.playbin().entries()[position as usize]); - self.set_current(window.playbin().current_entry() == Some(position as usize)); + self.set_song(&song); self.set_position(position); + self.set_playlist_pos(window.playlist_pos()); } pub fn unbind(&self) {} diff --git a/src/ui/playbar.rs b/src/ui/playbar.rs index 4cd2f77..305eb12 100644 --- a/src/ui/playbar.rs +++ b/src/ui/playbar.rs @@ -19,9 +19,12 @@ mod imp { show_cover_art: Cell, #[property(get, set)] - volume: Cell, + _volume: Cell, #[property(get, set)] - mute: Cell, + _mute: Cell, + #[property(get, set)] + _pause: Cell, + #[property(get, set)] position: Cell, #[property(get, set)] @@ -157,6 +160,10 @@ mod imp { fn on_mute_toggle(&self) { self.obj().set_mute(!self.obj().mute()); } + + fn window(&self) -> crate::ui::Window { + self.obj().root().unwrap().dynamic_cast().unwrap() + } } impl Drop for Playbar { diff --git a/src/ui/window.rs b/src/ui/window.rs index a41c83c..99c0912 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -1,9 +1,10 @@ mod imp { - use crate::{Playbin, PlaybinSong}; + use crate::mpv; + use crate::PlaybinSong; use adw::prelude::*; use adw::subclass::prelude::*; use glib::subclass::InitializingObject; - use gtk::{gdk, glib}; + use gtk::{gdk, gio, glib}; use std::cell::{Cell, RefCell}; use std::rc::Rc; use tracing::{event, Level}; @@ -29,16 +30,38 @@ mod imp { pub(super) setup: crate::ui::Setup, - pub(super) playbin: Rc, pub(super) api: RefCell>>, - pub(super) _sender: async_broadcast::Sender, - pub(super) inactive_receiver: async_broadcast::InactiveReceiver, + pub(super) mpv: mpv::Handle, + #[property(get)] + playlist_model: gio::ListStore, + + #[property(type = i64, get = Self::volume, set = Self::set_volume)] + _volume: (), + #[property(type = bool, get = Self::mute, set = Self::set_mute)] + _mute: (), + #[property(type = bool, get = Self::pause, set = Self::set_pause)] + _pause: (), + #[property(type = i64, get = Self::playlist_pos)] + _playlist_pos: (), } impl Default for Window { fn default() -> Self { - let (sender, receiver) = async_broadcast::broadcast(100); // TODO: constantize + let mpv = mpv::Handle::new(); + mpv.set_property("audio-client-name", "audrey").unwrap(); + mpv.set_property("user-agent", crate::USER_AGENT).unwrap(); + mpv.set_property("video", false).unwrap(); + mpv.set_property("prefetch-playlist", true).unwrap(); + mpv.set_property("gapless-audio", true).unwrap(); + + mpv.observe_property(0, "volume").unwrap(); + mpv.observe_property(1, "mute").unwrap(); + mpv.observe_property(2, "pause").unwrap(); + mpv.observe_property(3, "playlist-pos").unwrap(); + + // "Useful to drain property changes before a new file is loaded." + mpv.add_hook(0, "on_before_start_file", 0).unwrap(); Self { playbar: Default::default(), @@ -47,10 +70,14 @@ mod imp { playing_cover_art: Default::default(), song: Default::default(), setup: Default::default(), - playbin: Rc::new(Playbin::new(sender.clone())), api: Default::default(), - _sender: sender, - inactive_receiver: receiver.deactivate(), + mpv, + playlist_model: gio::ListStore::new::(), + + _volume: (), + _mute: (), + _pause: (), + _playlist_pos: (), } } } @@ -76,52 +103,56 @@ 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); - } + let window = self.obj().downgrade(); + glib::spawn_future_local(async move { + while let Some(window) = window.upgrade() { + let listener = window.imp().mpv.wakeup_listener(); + while let Some(event) = window.imp().mpv.wait_event(0.0) { + use crate::mpv::Event; - Event::PlaybinMutedChanged => { - window.imp().playbar.set_mute(window.playbin().muted()); - } + match event { + Event::PropertyChange(event) => match event.reply_userdata { + 0 => { + assert_eq!(event.name, "volume"); + window.notify("volume"); + } - Event::PlaybinEntryInserted(index) => { - window - .imp() - .play_queue - .model() - .unwrap() - .insert(index as u32, &window.playbin().entries()[index]); - } + 1 => { + assert_eq!(event.name, "mute"); + window.notify("mute"); + } - _ => {} - } - }, - ); + 2 => { + assert_eq!(event.name, "pause"); + window.notify("pause"); + } - let playbin = Rc::downgrade(&self.playbin); - glib::spawn_future_local(glib::clone!(async move { - loop { - match playbin.upgrade() { - None => break, - Some(playbin) => { - let listener = playbin.tick(); - drop(playbin); - listener.await; + 3 => { + assert_eq!(event.name, "playlist-pos"); + window.notify("playlist-pos"); + } + + _ => unreachable!(), + }, + + mpv::Event::Hook(event) => match event.reply_userdata { + 0 => { + assert_eq!(&event.name, "on_before_start_file"); + event!(Level::DEBUG, "on_before_start_file triggered"); + // just use this as a barrier + window.imp().mpv.continue_hook(event.id).unwrap(); + } + + _ => unreachable!(), + }, + + _ => event!(Level::DEBUG, "unhandled {event:?}"), } } + drop(window); + listener.await; } - })); - - self.play_queue.set_playbin(&self.playbin); + }); // set up mpris let window = self.obj().clone(); @@ -147,9 +178,11 @@ mod imp { crate::Mpris::setup(conn.object_server(), &window) .await .expect("could not serve mpris"); + /* crate::mpris::Player::setup(conn.object_server(), &window.imp().playbin) .await .expect("could not serve mpris player"); + */ drop(window); // don't keep this alive @@ -179,11 +212,18 @@ mod imp { #[template_callback] async fn shuffle_all(&self) { self.obj().set_can_click_shuffle_all(false); - self.playbin.stop(); + + self.mpv.command(["stop"]).unwrap(); + self.playlist_model.remove_all(); + let api = self.api.borrow(); let api = api.as_ref().unwrap(); for song in api.get_random_songs(10).await.unwrap().into_iter() { - self.playbin.push_entry(PlaybinSong::from_child(api, &song)); + let song = PlaybinSong::from_child(api, &song); + self.mpv + .command(["loadfile", &song.stream_url(), "append-play"]) + .unwrap(); + self.playlist_model.append(&song); } self.obj().set_can_click_shuffle_all(true); } @@ -221,6 +261,34 @@ mod imp { */ todo!() } + + fn volume(&self) -> i64 { + self.mpv.get_property("volume").unwrap() + } + + fn set_volume(&self, volume: i64) { + self.mpv.set_property("volume", volume).unwrap(); + } + + fn mute(&self) -> bool { + self.mpv.get_property("mute").unwrap() + } + + fn set_mute(&self, mute: bool) { + self.mpv.set_property("mute", mute).unwrap(); + } + + fn pause(&self) -> bool { + self.mpv.get_property("pause").unwrap() + } + + fn set_pause(&self, pause: bool) { + self.mpv.set_property("pause", pause).unwrap(); + } + + fn playlist_pos(&self) -> i64 { + self.mpv.get_property("playlist-pos").unwrap() + } } impl Drop for Window { @@ -233,7 +301,6 @@ mod imp { use adw::prelude::*; use adw::subclass::prelude::*; use gtk::{gio, glib}; -use std::rc::Rc; glib::wrapper! { pub struct Window(ObjectSubclass) @@ -246,19 +313,14 @@ impl Window { let window: Self = glib::Object::builder().property("application", app).build(); // manual bidirectional sync + /* window .imp() .playbar - .set_volume(window.imp().playbin.volume() as i32); - window.imp().playbar.connect_notify_local( - Some("volume"), - glib::clone!( - #[weak(rename_to = playbin)] - window.imp().playbin, - move |playbar, _| playbin.set_volume(playbar.volume() as i64) - ), - ); + .set_volume(window.imp().mpv.get_property::("volume") as i32); + */ + /* window.imp().playbar.set_mute(window.imp().playbin.muted()); window.imp().playbar.connect_notify_local( Some("mute"), @@ -276,9 +338,10 @@ impl Window { playbar.set_duration(entry.duration() as f64); true }, - ); + );*/ // update position every 100 ms + /* glib::source::timeout_add_local(std::time::Duration::from_millis(100), { let playbar = window.imp().playbar.downgrade(); let playbin = Rc::downgrade(&window.imp().playbin); @@ -294,7 +357,7 @@ impl Window { playbar.set_position(playbin.position().unwrap_or(0.0)); glib::ControlFlow::Continue } - }); + });*/ window .imp() @@ -302,12 +365,13 @@ impl Window { .connected() .connect_object(&window, |_setup, window, api| { window.imp().api.replace(Some(api)); - window.imp().playbin.stop(); + //window.imp().playbin.stop(); window.set_can_click_shuffle_all(true); true }); window.imp().setup.load(); + /* window .imp() .playbin @@ -317,16 +381,19 @@ impl Window { .imp() .now_playing(&playbin.entries()[playbin.current_entry().unwrap()]); true - }); + });*/ window } - pub fn playbin(&self) -> &crate::Playbin { - &self.imp().playbin + pub fn playbin(&self) -> ! { + todo!() } - pub fn receiver(&self) -> async_broadcast::Receiver { - self.imp().inactive_receiver.activate_cloned() + pub fn play_index(&self, index: i64) { + self.imp() + .mpv + .command(["playlist-play-index", &index.to_string()]) + .unwrap(); } }