diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 650 |
1 files changed, 341 insertions, 309 deletions
diff --git a/src/main.rs b/src/main.rs index 5c262bb..b363500 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,247 @@ +// Copyright © 2021-2022 Jakob L. Kreuze <zerodaysfordays@sdf.org> +// +// This file is part of Tunes. +// +// Tunes is free software; you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation; either version 3 of the +// License, or (at your option) any later version. +// +// Tunes is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General +// Public License for more details. +// +// You should have received a copy of the GNU Affero General Public +// License along with Tunes. If not, see <http://www.gnu.org/licenses/>. + use futures::{channel::mpsc, StreamExt}; +use glib::clone; use gtk::prelude::*; -use gtk::{gdk_pixbuf, gio, glib}; +use gtk::subclass::prelude::ObjectSubclassExt; +use gtk::{gdk_pixbuf, gio, glib, pango}; use libhandy::prelude::*; use libhandy::{ApplicationWindow, HeaderBar}; use mpd::idle::Idle; use mpd::Client; -struct TunesUI { - header_bar: HeaderBar, - // album_art: Image, - // queue_switcher: Notebook, +const MPD_HOST: &str = "127.0.0.1:6600"; + +fn main() { + let application = gtk::Application::builder() + .application_id("space.jakob.Tunes") + .build(); + + // We have to wait until the `activate` signal is fired before we can do our + // setup. + application.connect_activate(|app| { + // Our event-handling code will look a bit like what's common in SDL + // with their `SDLPollEvent` interface, in the sense that we'll have all + // of the different sub-systems of this application notify the main + // event loop by way of a channel. + let (sender, mut receiver) = mpsc::channel(1024); + + // Load all of the mobile UI support code from `libhandy`. + libhandy::init(); + + // `mpd` will notify us of events. Let's spin up a thread to listen for + // those notifications, and shuttle them through a channel as they + // arrive. + std::thread::spawn(clone!(@strong sender => move || { + let mut conn = Client::connect(MPD_HOST).unwrap(); + while let Ok(_subsystems) = conn.wait(&[mpd::idle::Subsystem::Player]) { + let mut sender = sender.clone(); + sender + .try_send(StateUpdateKind::MpdEvent) + .expect("Couldn't notify thread"); + } + })); + + // We'll connect to the MPD daemon here so we can populate the UI with + // some information from the current state. + let mut conn = Client::connect(MPD_HOST).unwrap(); + + // We'll have two "views" in our application: one for viewing and + // manipulating the current `mpd` queue, and another for searching for + // songs to add to the queue. In GTK, we can handle switching between + // these different views using a Stack. + let stack = gtk::Stack::new(); + stack.set_expand(true); + + let song_info = SongInfo::new(sender.clone()); + stack.add_named(song_info.as_ref(), "current_song"); + stack.set_child_title(song_info.as_ref(), Some("Now Playing")); + stack.set_child_icon_name(song_info.as_ref(), Some("audio-speakers-symbolic")); + + let query_info = QueryInfo::new(sender.clone()); + stack.add_named(query_info.as_ref(), "query_songs"); + stack.set_child_title(query_info.as_ref(), Some("Search Database")); + stack.set_child_icon_name(query_info.as_ref(), Some("system-search-symbolic")); + + // The `HeaderBar` is a GTK concept that libhandy plays nicely with. On + // desktop, the elements for switching stack views will show up there. + // On mobile, it will show up in a `ViewSwitcherBar` at the bottom. + let header_bar = HeaderBar::builder() + .show_close_button(true) + .title(&header_title(&mut conn).unwrap()) + .build(); + let view_switcher_title = libhandy::ViewSwitcherTitle::builder() + .title("Tunes") + .stack(&stack) + .build(); + header_bar.add(&view_switcher_title); + let view_switcher_bar = libhandy::ViewSwitcherBar::builder() + .visible(true) + .can_focus(false) + .stack(&stack) + .reveal(true) + .build(); + + // The window needs a single child, so we'll join the header bar, the + // stack, and the view switcher into a single box. + let content = gtk::Box::new(gtk::Orientation::Vertical, 0); + content.set_vexpand(true); + content.add(&header_bar); + content.add(&stack); + content.add(&view_switcher_bar); + + // Finally, the window. It's tied to a child, which we made above, and + // the GtkApplication that we declared at the beginning of `main`. + let window = ApplicationWindow::builder() + .default_width(350) + .default_height(70) + .modal(true) + .child(&content) + .build(); + window.set_application(Some(app)); + window.show_all(); + + // This isn't perfect (it won't run when the window gets its initial + // size), but this is how we notify that the album art display should be + // resized. + window.connect_configure_event(clone!(@strong sender => move |_, _| { + let mut sender = sender.clone(); + sender + .try_send(StateUpdateKind::WindowResizeEvent) + .expect("Couldn't notify thread"); + false + })); + + // Now that everything's been allocated a window, let's go ahead and + // update the widgets. + song_info + .update(&mut conn) + .expect("Couldn't update song info"); + + // The following code will fill the search view with every song in the + // database. If you have a music library as big as mine, it will + // negatively impact startup time. This could be done in, for example, a + // worker thread, but I've just omitted it because I don't want this + // example to be more complex than it has to be. + // + // let mut query = mpd::Query::new(); + // query.and(mpd::Term::Any, ""); + // let songs = conn.search(&query, (0, 65535)); + // for song in songs.unwrap() { + // query_info.model.insert(0, &SongObject::new(&song)); + // } + + // Finally, we'll start the "main event loop" we've been talking about + // in the main context of the application. + let main_context = gtk::glib::MainContext::default(); + main_context.spawn_local(async move { + let mut conn = Client::connect(MPD_HOST).unwrap(); + while let Some(event_type) = receiver.next().await { + match event_type { + StateUpdateKind::MpdEvent => { + if let Ok(title) = header_title(&mut conn) { + header_bar.set_title(Some(&title)); + song_info + .update(&mut conn) + .expect("Couldn't update song info"); + } + } + StateUpdateKind::WindowResizeEvent => { + song_info + .update_album_art(&mut conn) + .expect("Couldn't update album art"); + } + StateUpdateKind::QueryUpdateEvent(query_string) => { + // Let's not produce massive queries while the user is typing :) + if query_string.len() <= 2 { + continue; + } + + // Start from a blank slate. + query_info.model.remove_all(); + + // Query on all fields, case-insensitively, for the text + // that the user input. + let mut query = mpd::Query::new(); + query.and(mpd::Term::Any, &query_string); + let songs = conn.search(&query, (0, 65535)); + + // Insert them all into the model. This is reversed, + // which I don't consider to be a big deal. It's far + // less complex than adding it in order, which you will + // see below in the code that handles the queue. + for song in songs.unwrap() { + query_info.model.insert(0, &SongObject::new(&song)); + } + } + StateUpdateKind::QueueDeleteRequest(index) => { + conn.delete(index).expect("Couldn't dequeue song"); + } + StateUpdateKind::QueueAddRequest(filename) => { + conn.push_str(filename).expect("Couldn't queue song"); + } + StateUpdateKind::PlaybackStateChange(action) => { + dispatch_playback_state_change(&mut conn, action) + .expect("Couldn't queue action"); + } + } + } + }); + }); + + application.run(); +} + +/// Take action on `conn` based on a `PlaybackStateChange` notification +fn dispatch_playback_state_change( + conn: &mut mpd::Client, + action: PlaybackStateChange, +) -> anyhow::Result<()> { + use PlaybackStateChange::*; + match action { + SkipBackwards => conn.prev()?, + SkipForwards => conn.next()?, + Start => conn.play()?, + Stop => conn.stop()?, + Pause => conn.pause(true)?, + } + Ok(()) +} + +/// Kind of event we can notify the UI future about +#[derive(Debug)] +enum StateUpdateKind { + MpdEvent, + WindowResizeEvent, + QueryUpdateEvent(String), + QueueAddRequest(String), + QueueDeleteRequest(u32), + PlaybackStateChange(PlaybackStateChange), +} + +/// A simple action that affects playback state. +#[derive(Debug)] +enum PlaybackStateChange { + Start, + Stop, + Pause, + SkipBackwards, + SkipForwards, } /// Produce a short status line for the current state of `conn`. @@ -47,6 +279,7 @@ impl SongInfo { let song_text = gtk::Label::new(None); song_text.set_justify(gtk::Justification::Center); song_text.set_line_wrap(true); + song_text.set_line_wrap_mode(pango::WrapMode::WordChar); container.add(&album_art); container.add(&song_text); @@ -58,142 +291,132 @@ impl SongInfo { gtk::IconSize::SmallToolbar, ); action_bar.add(&control_previous_song); - let sender1 = sender.clone(); - control_previous_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_previous_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( PlaybackStateChange::SkipBackwards, )) .expect("Couldn't notify thread"); - }); + })); let control_start_song = gtk::Button::from_icon_name( Some("media-playback-start-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_start_song); - let sender1 = sender.clone(); - control_start_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_start_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( - PlaybackStateChange::StartPlayback, + PlaybackStateChange::Start, )) .expect("Couldn't notify thread"); - }); + })); let control_pause_song = gtk::Button::from_icon_name( Some("media-playback-pause-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_pause_song); - let sender1 = sender.clone(); - control_pause_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_pause_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( - PlaybackStateChange::PausePlayback, + PlaybackStateChange::Pause, )) .expect("Couldn't notify thread"); - }); + })); let control_stop_song = gtk::Button::from_icon_name( Some("media-playback-stop-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_stop_song); - let sender1 = sender.clone(); - control_stop_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_stop_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( - PlaybackStateChange::StopPlayback, + PlaybackStateChange::Stop, )) .expect("Couldn't notify thread"); - }); + })); let control_next_song = gtk::Button::from_icon_name( Some("media-skip-forward-symbolic"), gtk::IconSize::SmallToolbar, ); action_bar.add(&control_next_song); - let sender1 = sender.clone(); - control_next_song.connect_clicked(move |_| { - let mut sender = sender1.clone(); + control_next_song.connect_clicked(clone!(@strong sender => move |_| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::PlaybackStateChange( PlaybackStateChange::SkipForwards, )) .expect("Couldn't notify thread"); - }); + })); let model = gio::ListStore::new(SongObject::static_type()); let listbox = gtk::ListBox::new(); - let sender1 = sender.clone(); - listbox.bind_model(Some(&model), move |item| { - let sender = sender1.clone(); + listbox.bind_model( + Some(&model), + clone!(@strong sender => move |item| { + let sender = sender.clone(); - let box_ = gtk::ListBoxRow::new(); - let item = item - .downcast_ref::<SongObject>() - .expect("Row data is of wrong type"); - - let grid = gtk::Grid::builder().column_homogeneous(true).build(); - - let remove_individual_song = gtk::Button::from_icon_name( - Some("list-remove-symbolic"), - gtk::IconSize::SmallToolbar, - ); - remove_individual_song.set_visible(true); - let index = item.property::<u32>("index"); - remove_individual_song.connect_clicked(move |_| { - let mut sender = sender.clone(); - sender - .try_send(StateUpdateKind::QueueDeleteRequest(index)) - .expect("Couldn't notify thread"); - - // We don't actually get notified by `mpd`, but we can pretend - // that we did! - sender - .try_send(StateUpdateKind::MpdEvent) - .expect("Couldn't notify thread"); - }); - grid.attach(&remove_individual_song, 0, 0, 1, 1); - - let title_label = gtk::Label::new(None); - title_label.set_line_wrap(true); - item.bind_property("title", &title_label, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - grid.attach(&title_label, 1, 0, 1, 1); + let box_ = gtk::ListBoxRow::new(); + let item = item + .downcast_ref::<SongObject>() + .expect("Row data is of wrong type"); - title_label.set_visible(true); // why? + let grid = gtk::Grid::builder().column_homogeneous(true).build(); - let album_label = gtk::Label::new(None); - album_label.set_line_wrap(true); - item.bind_property("album", &album_label, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - grid.attach(&album_label, 2, 0, 1, 1); + let remove_individual_song = gtk::Button::from_icon_name( + Some("list-remove-symbolic"), + gtk::IconSize::SmallToolbar, + ); + let index = item.property::<u32>("index"); + remove_individual_song.connect_clicked(move |_| { + let mut sender = sender.clone(); + sender + .try_send(StateUpdateKind::QueueDeleteRequest(index)) + .expect("Couldn't notify thread"); + sender + .try_send(StateUpdateKind::MpdEvent) + .expect("Couldn't notify thread"); + }); + grid.attach(&remove_individual_song, 0, 0, 1, 1); - album_label.set_visible(true); // why? + let title_label = gtk::Label::new(None); + title_label.set_line_wrap(true); + title_label.set_line_wrap_mode(pango::WrapMode::WordChar); + item.bind_property("title", &title_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + grid.attach(&title_label, 1, 0, 1, 1); - let artist_label = gtk::Label::new(None); - artist_label.set_line_wrap(true); - item.bind_property("artist", &artist_label, "label") - .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) - .build(); - grid.attach(&artist_label, 3, 0, 1, 1); - artist_label.set_visible(true); // why? + let album_label = gtk::Label::new(None); + album_label.set_line_wrap(true); + album_label.set_line_wrap_mode(pango::WrapMode::WordChar); + item.bind_property("album", &album_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + grid.attach(&album_label, 2, 0, 1, 1); - grid.set_visible(true); // why? - box_.add(&grid); + let artist_label = gtk::Label::new(None); + artist_label.set_line_wrap(true); + artist_label.set_line_wrap_mode(pango::WrapMode::WordChar); + item.bind_property("artist", &artist_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + grid.attach(&artist_label, 3, 0, 1, 1); - box_.upcast::<gtk::Widget>() - }); + grid.show_all(); + box_.add(&grid); + box_.upcast::<gtk::Widget>() + }), + ); let scrolled_window = gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); @@ -202,7 +425,6 @@ impl SongInfo { container.add(&action_bar); container.add(&scrolled_window); - container.show_all(); SongInfo { @@ -215,11 +437,11 @@ impl SongInfo { fn update_album_art(&self, conn: &mut mpd::Client) -> anyhow::Result<()> { if let Some(song) = conn.currentsong()? { - // If we've been allocated a window, pick the greatest dimension - // (width or height) and divide that dimension by two to get the - // size (in pixels) that we'll scale the album art to. Otherwise, we - // default to 128. - let album_art_size = std::cmp::max( + // If we've been allocated a window, pick the least dimension (width + // or height) and divide that dimension by two to get the size (in + // pixels) that we'll scale the album art to. Otherwise, we default + // to 128. + let album_art_size = std::cmp::min( self.container .window() .map(|x| x.width() / 2) @@ -252,16 +474,8 @@ impl SongInfo { self.update_album_art(conn)?; if let Some(song) = conn.currentsong()? { - let title = song - .title - .as_ref() - .map(|x| x.as_str()) - .unwrap_or("[Unknown]"); - let artist = song - .artist - .as_ref() - .map(|x| x.as_str()) - .unwrap_or("[Unknown]"); + let title = song.title.as_deref().unwrap_or("[Unknown]"); + let artist = song.artist.as_deref().unwrap_or("[Unknown]"); let album = song .tags .get("Album") @@ -270,15 +484,14 @@ impl SongInfo { let text = format!("{}\n{} - {}", title, artist, album); self.song_text.set_text(&text); + // We'll use `pango` attributes to make the display look nice and + // pretty. Scale the title of the song the most, and still make the + // other info reasonably large. let attr_list = gtk::pango::AttrList::new(); - - // Scale the title of the song the most. let mut attr = gtk::pango::AttrFloat::new_scale(2.0); attr.set_start_index(0); attr.set_end_index(title.len() as u32); attr_list.insert(attr); - - // And still make the other info reasonably large. let mut attr = gtk::pango::AttrFloat::new_scale(1.5); attr.set_start_index(title.len() as u32 + 1); attr_list.insert(attr); @@ -288,9 +501,10 @@ impl SongInfo { self.model.remove_all(); for (i, song) in conn.queue()?.iter().enumerate() { - let object = SongObject::new(&song); - object.set_index(i.try_into().unwrap()); - self.model.insert(i.try_into().unwrap(), &object) + let index = i.try_into().unwrap(); + let object = SongObject::new(song); + object.set_index(index); + self.model.insert(index, &object) } Ok(()) @@ -314,20 +528,18 @@ impl QueryInfo { let container = gtk::Box::new(gtk::Orientation::Vertical, 2); let query_input = gtk::Entry::builder().visible(true).build(); - let sender1 = sender.clone(); - query_input.connect_key_press_event(move |widget, _| { - let mut sender = sender1.clone(); // Interesting ownership puzzle :D + query_input.connect_key_press_event(clone!(@strong sender => move |widget, _| { + let mut sender = sender.clone(); sender .try_send(StateUpdateKind::QueryUpdateEvent(widget.text().into())) .expect("Couldn't notify thread"); gtk::Inhibit(false) - }); + })); let model = gio::ListStore::new(SongObject::static_type()); let listbox = gtk::ListBox::new(); - let sender1 = sender.clone(); - listbox.bind_model(Some(&model), move |item| { - let sender = sender1.clone(); + listbox.bind_model(Some(&model), clone!(@strong sender => move |item| { + let sender = sender.clone(); let box_ = gtk::ListBoxRow::new(); let item = item @@ -346,42 +558,40 @@ impl QueryInfo { sender .try_send(StateUpdateKind::QueueAddRequest(filename)) .expect("Couldn't notify thread"); + sender + .try_send(StateUpdateKind::MpdEvent) + .expect("Couldn't notify thread"); }); grid.attach(&add_individual_song, 0, 0, 1, 1); let title_label = gtk::Label::new(None); title_label.set_line_wrap(true); + title_label.set_line_wrap_mode(pango::WrapMode::WordChar); item.bind_property("title", &title_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); grid.attach(&title_label, 1, 0, 1, 1); - title_label.set_visible(true); // why? - let album_label = gtk::Label::new(None); album_label.set_line_wrap(true); + album_label.set_line_wrap_mode(pango::WrapMode::WordChar); item.bind_property("album", &album_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); grid.attach(&album_label, 2, 0, 1, 1); - album_label.set_visible(true); // why? - let artist_label = gtk::Label::new(None); artist_label.set_line_wrap(true); + artist_label.set_line_wrap_mode(pango::WrapMode::WordChar); item.bind_property("artist", &artist_label, "label") .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) .build(); grid.attach(&artist_label, 3, 0, 1, 1); - artist_label.set_visible(true); // why? - - grid.set_visible(true); // why? - + grid.show_all(); box_.add(&grid); - box_.upcast::<gtk::Widget>() - }); + })); let scrolled_window = gtk::ScrolledWindow::new(gtk::Adjustment::NONE, gtk::Adjustment::NONE); @@ -401,11 +611,13 @@ impl AsRef<gtk::Widget> for QueryInfo { } } +// Unfortunately, to use the `ListStore` interface, we'll need to represent our +// data as an actual `glib` object. This is a little hairy in Rust, involving a +// fair bit of boilerplate, but not too terrible. glib::wrapper! { pub struct SongObject(ObjectSubclass<imp::SongObject>); } -use gtk::subclass::prelude::ObjectSubclassExt; impl SongObject { pub fn new(song: &mpd::song::Song) -> Self { glib::Object::new(&[ @@ -415,7 +627,7 @@ impl SongObject { &song .title .as_ref() - .map(|x| x.clone()) + .cloned() .unwrap_or_else(|| "[Untitled]".into()), ), ( @@ -423,7 +635,7 @@ impl SongObject { &song .artist .as_ref() - .map(|x| x.clone()) + .cloned() .unwrap_or_else(|| "[No Artist]".into()), ), ( @@ -431,7 +643,7 @@ impl SongObject { &song .tags .get("Album") - .map(|x| x.clone()) + .cloned() .unwrap_or_else(|| "[Untitled]".into()), ), ]) @@ -444,6 +656,8 @@ impl SongObject { } } +// These class "implementations" are typically done in a separate +// file/directory. I wanted to keep the example self-contained. mod imp { use std::cell::{Cell, RefCell}; @@ -531,185 +745,3 @@ mod imp { } } } - -/// Kind of event we can notify the UI future about -enum StateUpdateKind { - MpdEvent, - WindowResizeEvent, - QueryUpdateEvent(String), - QueueAddRequest(String), - QueueDeleteRequest(u32), - PlaybackStateChange(PlaybackStateChange), -} - -#[derive(Debug)] -enum PlaybackStateChange { - SkipBackwards, - SkipForwards, - StartPlayback, - StopPlayback, - PausePlayback, -} - -fn main() { - let application = gtk::Application::builder() - .application_id("space.jakob.Tunes") - .build(); - - application.connect_activate(|app| { - libhandy::init(); - - let mut conn = Client::connect("127.0.0.1:6600").unwrap(); - let (sender, mut receiver) = mpsc::channel(1000); - - let mut sender1 = sender.clone(); - std::thread::spawn(move || loop { - let mut conn = Client::connect("127.0.0.1:6600").unwrap(); - if let Ok(_subsystems) = conn.wait(&[mpd::idle::Subsystem::Player]) { - sender1 - .try_send(StateUpdateKind::MpdEvent) - .expect("Couldn't notify thread"); - } else { - break; - } - }); - - // conn.volume(100).unwrap(); - // conn.load("My Lounge Playlist", ..).unwrap(); - // conn.play().unwrap(); - - let stack = gtk::Stack::new(); - stack.set_expand(true); - - let song_info = SongInfo::new(sender.clone()); - stack.add_named(song_info.as_ref(), "current_song"); - stack.set_child_title(song_info.as_ref(), Some("Now Playing")); - stack.set_child_icon_name(song_info.as_ref(), Some("audio-speakers-symbolic")); - - let query_info = QueryInfo::new(sender.clone()); - stack.add_named(query_info.as_ref(), "query_songs"); - stack.set_child_title(query_info.as_ref(), Some("Search Database")); - stack.set_child_icon_name(query_info.as_ref(), Some("system-search-symbolic")); - - let header_bar = HeaderBar::builder() - .show_close_button(true) - .title(&header_title(&mut conn).unwrap()) - .build(); - let view_switcher_title = libhandy::ViewSwitcherTitle::builder() - .title("Tunes") - .stack(&stack) - .build(); - header_bar.add(&view_switcher_title); - - let view_switcher_bar = libhandy::ViewSwitcherBar::builder() - .visible(true) - .can_focus(false) - .stack(&stack) - .reveal(true) - .build(); - - let ui = TunesUI { header_bar }; - - // Combine the content in a box - let content = gtk::Box::new(gtk::Orientation::Vertical, 0); - content.set_vexpand(true); - // Handy's ApplicationWindow does not include a HeaderBar - content.add(&ui.header_bar); - content.add(&stack); - content.add(&view_switcher_bar); - - let window = ApplicationWindow::builder() - .default_width(350) - .default_height(70) - .modal(true) - // add content to window - .child(&content) - .build(); - window.set_application(Some(app)); - window.show_all(); - - window.connect_size_allocate(move |_, _| { - let mut sender = sender.clone(); - sender - .try_send(StateUpdateKind::WindowResizeEvent) - .expect("Couldn't notify thread"); - }); - - // Now that everything's been allocated a window, let's go ahead and - // update the widgets. - song_info - .update(&mut conn) - .expect("Couldn't update song info"); - - let mut query = mpd::Query::new(); - query.and(mpd::Term::Any, ""); - let songs = conn.find(&mut query, (0, 65535)); - // println!("{:?}", songs); - for song in songs.unwrap() { - query_info.model.insert(0, &SongObject::new(&song)); - } - - let main_context = gtk::glib::MainContext::default(); - main_context.spawn_local(async move { - let mut conn = Client::connect("127.0.0.1:6600").unwrap(); - while let Some(event_type) = receiver.next().await { - match event_type { - StateUpdateKind::MpdEvent => { - if let Ok(title) = header_title(&mut conn) { - ui.header_bar.set_title(Some(&title)); - song_info - .update(&mut conn) - .expect("Couldn't update song info"); - } - } - StateUpdateKind::WindowResizeEvent => { - song_info - .update_album_art(&mut conn) - .expect("Couldn't update album art"); - } - StateUpdateKind::QueryUpdateEvent(query_string) => { - // Let's not produce massive queries while the user is typing :) - if query_string.len() <= 2 { - continue; - } - query_info.model.remove_all(); - let mut query = mpd::Query::new(); - query.and(mpd::Term::Any, &query_string); - let songs = conn.search(&mut query, (0, 65535)); - // println!("{:?}", songs); - for song in songs.unwrap() { - query_info.model.insert(0, &SongObject::new(&song)); - } - } - StateUpdateKind::QueueDeleteRequest(index) => { - conn.delete(index).expect("Couldn't dequeue song"); - } - StateUpdateKind::QueueAddRequest(filename) => { - conn.push_str(filename).expect("Couldn't queue song"); - } - StateUpdateKind::PlaybackStateChange(action) => { - dispatch_playback_state_change(&mut conn, action) - .expect("Couldn't queue action"); - } - } - } - }); - }); - - application.run(); -} - -fn dispatch_playback_state_change( - conn: &mut mpd::Client, - action: PlaybackStateChange, -) -> anyhow::Result<()> { - use PlaybackStateChange::*; - match action { - SkipBackwards => conn.prev()?, - SkipForwards => conn.next()?, - StartPlayback => conn.play()?, - StopPlayback => conn.stop()?, - PausePlayback => conn.pause(true)?, - } - Ok(()) -} |