audrey/src/ui/window.rs

487 lines
17 KiB
Rust
Raw Normal View History

2024-11-02 11:24:25 +00:00
mod imp {
2024-11-04 11:31:43 +00:00
use crate::mpv;
use crate::PlaybinSong;
2024-11-02 11:24:25 +00:00
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::subclass::InitializingObject;
2024-11-04 11:31:43 +00:00
use gtk::{gdk, gio, glib};
2024-11-02 11:24:25 +00:00
use std::cell::{Cell, RefCell};
2024-11-03 09:37:37 +00:00
use std::rc::Rc;
2024-11-04 21:00:17 +00:00
use tracing::{event, span, Level};
2024-10-29 13:02:29 +00:00
2024-11-04 09:40:51 +00:00
#[derive(gtk::CompositeTemplate, glib::Properties)]
2024-11-02 11:24:25 +00:00
#[template(resource = "/eu/callcc/audrey/window.ui")]
#[properties(wrapper_type = super::Window)]
pub struct Window {
#[template_child]
pub(super) playbar: TemplateChild<crate::ui::Playbar>,
2024-11-03 17:45:52 +00:00
#[template_child]
pub(super) play_queue: TemplateChild<crate::ui::PlayQueue>,
2024-11-02 11:24:25 +00:00
#[property(get, set, default = false)]
can_click_shuffle_all: Cell<bool>,
#[property(get, set, nullable)]
playing_cover_art: RefCell<Option<gdk::Paintable>>,
2024-11-04 12:29:10 +00:00
#[property(type = Option<PlaybinSong>, get = Self::song, nullable)]
_song: (),
2024-11-02 11:24:25 +00:00
pub(super) setup: crate::ui::Setup,
2024-11-03 12:41:02 +00:00
2024-11-04 07:03:59 +00:00
pub(super) api: RefCell<Option<Rc<crate::subsonic::Client>>>,
2024-11-04 09:40:51 +00:00
2024-11-04 11:31:43 +00:00
pub(super) mpv: mpv::Handle,
#[property(get)]
playlist_model: gio::ListStore,
2024-11-05 09:01:07 +00:00
#[property(type = u32, get = Self::volume, set = Self::set_volume)]
2024-11-04 11:31:43 +00:00
_volume: (),
#[property(type = bool, get = Self::mute, set = Self::set_mute)]
_mute: (),
#[property(type = bool, get = Self::pause, set = Self::set_pause)]
_pause: (),
2024-11-05 09:01:07 +00:00
#[property(type = i32, get = Self::playlist_pos)]
2024-11-04 11:31:43 +00:00
_playlist_pos: (),
2024-11-04 12:22:25 +00:00
#[property(type = f64, get = Self::time_pos)]
_time_pos: (),
2024-11-05 09:01:07 +00:00
#[property(type = f64, get = Self::duration)]
_duration: (), // as reported by mpv, compare with song.duration
2024-11-04 12:43:30 +00:00
#[property(type = bool, get = Self::idle_active)]
_idle_active: (),
2024-11-05 09:01:07 +00:00
#[property(type = u32, get = Self::playlist_count)]
_playlist_count: (),
2024-11-04 09:40:51 +00:00
}
impl Default for Window {
fn default() -> Self {
2024-11-04 11:31:43 +00:00
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();
2024-11-04 12:43:30 +00:00
mpv.observe_property(4, "idle-active").unwrap();
mpv.observe_property(5, "time-pos").unwrap();
2024-11-05 09:01:07 +00:00
mpv.observe_property(6, "playlist-count").unwrap();
mpv.observe_property(7, "duration").unwrap();
2024-11-04 11:31:43 +00:00
// "Useful to drain property changes before a new file is loaded."
mpv.add_hook(0, "on_before_start_file", 0).unwrap();
2024-11-04 09:40:51 +00:00
Self {
playbar: Default::default(),
play_queue: Default::default(),
can_click_shuffle_all: Cell::new(false),
playing_cover_art: Default::default(),
2024-11-04 12:29:10 +00:00
_song: (),
2024-11-04 09:40:51 +00:00
setup: Default::default(),
api: Default::default(),
2024-11-04 11:31:43 +00:00
mpv,
playlist_model: gio::ListStore::new::<PlaybinSong>(),
_volume: (),
_mute: (),
_pause: (),
_playlist_pos: (),
2024-11-04 12:22:25 +00:00
_time_pos: (),
2024-11-05 09:01:07 +00:00
_duration: (),
2024-11-04 12:43:30 +00:00
_idle_active: (),
2024-11-05 09:01:07 +00:00
_playlist_count: (),
2024-11-04 09:40:51 +00:00
}
}
2024-10-29 13:02:29 +00:00
}
2024-11-02 11:24:25 +00:00
#[glib::object_subclass]
impl ObjectSubclass for Window {
const NAME: &'static str = "AudreyUiWindow";
type Type = super::Window;
type ParentType = adw::ApplicationWindow;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
klass.bind_template_callbacks();
}
fn instance_init(obj: &InitializingObject<Self>) {
obj.init_template();
}
2024-10-29 13:02:29 +00:00
}
2024-11-02 11:24:25 +00:00
#[glib::derived_properties]
2024-11-03 09:37:37 +00:00
impl ObjectImpl for Window {
fn constructed(&self) {
self.parent_constructed();
2024-11-04 11:31:43 +00:00
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;
2024-11-04 21:00:17 +00:00
let span = span!(Level::DEBUG, "mpv_wait_event");
let _guard = span.enter();
2024-11-04 11:31:43 +00:00
match event {
Event::PropertyChange(event) => match event.reply_userdata {
0 => {
assert_eq!(event.name, "volume");
window.notify("volume");
}
1 => {
assert_eq!(event.name, "mute");
window.notify("mute");
}
2 => {
assert_eq!(event.name, "pause");
window.notify("pause");
}
3 => {
assert_eq!(event.name, "playlist-pos");
window.notify("playlist-pos");
}
2024-11-04 12:43:30 +00:00
4 => {
assert_eq!(event.name, "idle-active");
window.notify("idle-active");
}
5 => {
assert_eq!(event.name, "time-pos");
window.notify("time-pos");
}
2024-11-05 09:01:07 +00:00
6 => {
assert_eq!(event.name, "playlist-count");
window.notify("playlist-count");
}
7 => {
assert_eq!(event.name, "duration");
window.notify("duration");
}
2024-11-04 11:31:43 +00:00
_ => unreachable!(),
},
2024-11-04 12:22:25 +00:00
Event::StartFile(_) => {
2024-11-04 12:29:10 +00:00
window.notify("song");
2024-11-04 12:22:25 +00:00
// TODO: load cover art
}
Event::Hook(event) => match event.reply_userdata {
2024-11-04 11:31:43 +00:00
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!(),
},
2024-11-04 12:52:33 +00:00
Event::LogMessage(event) => {
2024-11-04 21:00:17 +00:00
match event.log_level {
// level has to be 'static so this sux
l if l <= 20 => {
event!(target: "mpv", Level::ERROR, prefix = event.prefix, event = event.text.trim())
}
l if l <= 30 => {
event!(target: "mpv", Level::WARN, prefix = event.prefix, event = event.text.trim())
}
l if l <= 40 => {
event!(target: "mpv", Level::INFO, prefix = event.prefix, event = event.text.trim())
}
l if l <= 60 => {
event!(target: "mpv", Level::DEBUG, prefix = event.prefix, event = event.text.trim())
}
l if l <= 70 => {
event!(target: "mpv", Level::TRACE, prefix = event.prefix, event = event.text.trim())
}
// should be unused
_ => event!(
target: "mpv",
Level::DEBUG,
prefix = event.prefix,
event = event.text.trim(),
"unknown mpv level: {}",
event.log_level,
),
2024-11-04 12:52:33 +00:00
};
}
2024-11-05 05:28:48 +00:00
Event::Unknown(_) => {
// either deprecated or future, ignore
}
2024-11-04 11:31:43 +00:00
_ => event!(Level::DEBUG, "unhandled {event:?}"),
2024-11-03 09:37:37 +00:00
}
}
2024-11-04 11:31:43 +00:00
drop(window);
listener.await;
2024-11-03 09:37:37 +00:00
}
2024-11-04 11:31:43 +00:00
});
2024-11-03 17:45:52 +00:00
2024-11-03 09:37:37 +00:00
// set up mpris
let window = self.obj().clone();
glib::spawn_future_local(async move {
let conn = zbus::connection::Builder::session()
.expect("could not connect to the session bus")
.internal_executor(false)
.build()
.await
.expect("could not build connection to the session bus");
// run this in glib's main loop
glib::spawn_future_local(glib::clone!(
#[strong]
conn,
async move {
loop {
conn.executor().tick().await;
}
}
));
crate::Mpris::setup(conn.object_server(), &window)
.await
.expect("could not serve mpris");
2024-11-04 11:31:43 +00:00
/*
2024-11-04 07:03:59 +00:00
crate::mpris::Player::setup(conn.object_server(), &window.imp().playbin)
2024-11-03 09:37:37 +00:00
.await
.expect("could not serve mpris player");
2024-11-04 12:22:25 +00:00
FIXME */
2024-11-03 09:37:37 +00:00
drop(window); // don't keep this alive
// always set up handlers before requesting service name
conn.request_name("org.mpris.MediaPlayer2.audrey")
.await
.expect("could not register name in session bus");
});
}
}
2024-11-02 11:24:25 +00:00
impl WidgetImpl for Window {}
impl WindowImpl for Window {}
impl ApplicationWindowImpl for Window {}
impl AdwApplicationWindowImpl for Window {}
#[gtk::template_callbacks]
impl Window {
#[template_callback]
fn show_playbar_cover_art(&self, stack_child: Option<&str>) -> bool {
stack_child != Some("play-queue")
}
#[template_callback]
async fn shuffle_all(&self) {
2024-11-03 15:11:25 +00:00
self.obj().set_can_click_shuffle_all(false);
2024-11-04 11:31:43 +00:00
self.mpv.command(["stop"]).unwrap();
self.playlist_model.remove_all();
2024-11-04 12:43:30 +00:00
self.set_pause(false);
2024-11-04 11:31:43 +00:00
2024-11-04 12:22:25 +00:00
let api = {
let api = self.api.borrow();
Rc::clone(api.as_ref().unwrap())
};
2024-11-03 15:11:25 +00:00
for song in api.get_random_songs(10).await.unwrap().into_iter() {
2024-11-04 12:22:25 +00:00
let song = PlaybinSong::from_child(&api, &song);
2024-11-04 11:31:43 +00:00
self.mpv
.command(["loadfile", &song.stream_url(), "append-play"])
.unwrap();
self.playlist_model.append(&song);
2024-11-03 15:11:25 +00:00
}
self.obj().set_can_click_shuffle_all(true);
2024-11-02 11:24:25 +00:00
}
#[template_callback]
fn show_setup_dialog(&self) {
self.setup.present(Some(self.obj().as_ref()));
}
2024-11-05 09:01:07 +00:00
fn volume(&self) -> u32 {
self.mpv
.get_property::<i64>("volume")
.unwrap()
.try_into()
.unwrap()
2024-11-04 11:31:43 +00:00
}
2024-11-05 09:01:07 +00:00
fn set_volume(&self, volume: u32) {
self.mpv.set_property("volume", volume as i64).unwrap();
2024-11-04 11:31:43 +00:00
}
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();
}
2024-11-05 09:01:07 +00:00
fn playlist_pos(&self) -> i32 {
self.mpv
.get_property::<i64>("playlist-pos")
.unwrap()
.try_into()
.unwrap()
2024-11-04 11:31:43 +00:00
}
2024-11-04 12:22:25 +00:00
fn time_pos(&self) -> f64 {
match self.mpv.get_property("time-pos") {
Ok(time_pos) => Ok(time_pos),
Err(err) if err.is_property_unavailable() => Ok(0.0), //placeholder
Err(err) => Err(err),
}
.unwrap()
}
2024-11-04 12:29:10 +00:00
2024-11-05 09:01:07 +00:00
fn duration(&self) -> f64 {
let duration = match self.mpv.get_property::<f64>("duration") {
Ok(duration) => Ok(Some(duration)),
Err(err) if err.is_property_unavailable() => {
Ok(self.song().as_ref().map(|song| song.duration() as f64))
}
Err(err) => Err(err),
}
.unwrap();
assert_eq!(
duration.map(|f| f as i64),
self.song().as_ref().map(crate::PlaybinSong::duration),
"mpv duration doesn not match subsonic duration (this should probably be a warn)"
);
duration.unwrap_or(0.0) // placeholder
}
2024-11-04 12:43:30 +00:00
fn idle_active(&self) -> bool {
self.mpv.get_property("idle-active").unwrap()
}
2024-11-05 09:01:07 +00:00
fn playlist_count(&self) -> u32 {
self.mpv
.get_property::<i64>("playlist-count")
.unwrap()
.try_into()
.unwrap()
}
2024-11-04 12:29:10 +00:00
fn song(&self) -> Option<PlaybinSong> {
if self.obj().playlist_pos() < 0 {
None
} else {
2024-11-04 12:54:30 +00:00
let song: PlaybinSong = self
.obj()
.playlist_model()
.item(self.obj().playlist_pos() as u32)
.unwrap()
.dynamic_cast()
.unwrap();
// sanity check
2024-11-05 09:16:22 +00:00
match self.mpv.get_property::<String>("path") {
Ok(path) => assert_eq!(song.stream_url(), path),
Err(err) if err.is_property_unavailable() => {
// NOTE: this happens between EndFile and StartFile
event!(Level::WARN, "can't do sanity check, path is unavailable")
}
Err(err) => Err(err).unwrap(),
};
2024-11-04 12:54:30 +00:00
Some(song)
2024-11-04 12:29:10 +00:00
}
}
2024-11-03 09:37:37 +00:00
}
impl Drop for Window {
fn drop(&mut self) {
2024-11-04 09:12:13 +00:00
event!(Level::DEBUG, "dropping AudreyUiWindow");
2024-11-03 09:37:37 +00:00
}
2024-10-29 13:02:29 +00:00
}
}
use adw::prelude::*;
2024-11-02 11:24:25 +00:00
use adw::subclass::prelude::*;
2024-11-03 19:18:12 +00:00
use gtk::{gio, glib};
2024-11-04 12:34:30 +00:00
use std::rc::Rc;
2024-10-29 13:02:29 +00:00
glib::wrapper! {
2024-11-02 11:24:25 +00:00
pub struct Window(ObjectSubclass<imp::Window>)
2024-10-29 13:02:29 +00:00
@extends adw::ApplicationWindow, gtk::ApplicationWindow, gtk::Window, gtk::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}
impl Window {
pub fn new(app: &impl IsA<gtk::Application>) -> Self {
2024-11-02 11:24:25 +00:00
let window: Self = glib::Object::builder().property("application", app).build();
2024-11-04 12:34:30 +00:00
window.imp().setup.set_window(&window);
2024-11-02 11:24:25 +00:00
window.imp().setup.load();
window
2024-11-01 12:50:29 +00:00
}
2024-11-04 09:40:51 +00:00
2024-11-04 11:31:43 +00:00
pub fn playbin(&self) -> ! {
todo!()
2024-11-04 09:40:51 +00:00
}
2024-11-04 11:35:38 +00:00
pub fn playlist_play_index(&self, index: i64) {
2024-11-04 11:31:43 +00:00
self.imp()
.mpv
.command(["playlist-play-index", &index.to_string()])
.unwrap();
2024-11-04 09:40:51 +00:00
}
2024-11-04 11:37:46 +00:00
pub fn playlist_remove(&self, index: i64) {
self.imp()
.mpv
.command(["playlist-remove", &index.to_string()])
.unwrap();
self.playlist_model().remove(index as u32);
}
2024-11-04 12:34:30 +00:00
pub fn setup_connected(&self, api: crate::subsonic::Client) {
self.imp().api.replace(Some(Rc::new(api)));
self.set_can_click_shuffle_all(true);
}
2024-11-05 09:16:22 +00:00
pub fn playlist_next(&self) {
self.imp().mpv.command(["playlist-next"]).unwrap();
}
pub fn playlist_prev(&self) {
self.imp().mpv.command(["playlist-prev"]).unwrap();
}
pub fn seek(&self, new_position: f64) {
self.imp()
.mpv
.command(["seek", &new_position.to_string(), "absolute", "exact"])
.unwrap();
}
2024-10-29 13:02:29 +00:00
}