diff --git a/AUTHORS b/AUTHORS index b7d53764..2153d44a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,4 +16,5 @@ Nicolas Fella Fridolin Weisser Jan Przebor Warren Hu -bbb651 \ No newline at end of file +bbb651 +Ondřej Míchal diff --git a/src/app/components/album/album.rs b/src/app/components/album/album.rs index 014eb858..d33c38e3 100644 --- a/src/app/components/album/album.rs +++ b/src/app/components/album/album.rs @@ -7,6 +7,7 @@ use gtk::prelude::*; use gtk::subclass::prelude::*; use gtk::CompositeTemplate; use libadwaita::subclass::prelude::BinImpl; +use std::cell::RefCell; mod imp { @@ -15,6 +16,8 @@ mod imp { #[derive(Debug, Default, CompositeTemplate)] #[template(resource = "/dev/alextren/Spot/components/album.ui")] pub struct AlbumWidget { + pub clicked_sig_handle: RefCell>, + #[template_child] pub album_label: TemplateChild, @@ -61,6 +64,76 @@ impl AlbumWidget { glib::Object::new(&[]).expect("Failed to create an instance of AlbumWidget") } + /// Binds a ListItem widget to an item in a model. + /// + /// Has to be unbound due to connected signals, + pub fn bind_list_item(_factory: >k::SignalListItemFactory, list_item: >k::ListItem, worker: Worker, f: F) + where + F: Fn(String) + 'static + { + let album_model = list_item.item().unwrap().downcast::().unwrap(); + + let imp = list_item.child().unwrap().downcast::().unwrap(); + let widget = imp::AlbumWidget::from_instance(&imp); + + // TODO: Get rid of this (maybe XML?) + widget.cover_image.set_overflow(gtk::Overflow::Hidden); + + if let Some(url) = album_model.cover_url() { + let _imp = imp.downgrade(); + worker.send_local_task(async move { + if let Some(_imp) = _imp.upgrade() { + let loader = ImageLoader::new(); + let result = loader.load_remote(&url, "jpg", 200, 200).await; + _imp.set_image(result.as_ref()); + _imp.set_loaded(); + } + }); + } else { + imp.set_loaded(); + } + + album_model + .bind_property("album", &*widget.album_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + + album_model + .bind_property("artist", &*widget.artist_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + + match album_model.year() { + Some(_) => { + album_model + .bind_property("year", &*widget.year_label, "label") + .flags(glib::BindingFlags::DEFAULT | glib::BindingFlags::SYNC_CREATE) + .build(); + } + None => { + widget.year_label.hide(); + } + } + + widget.clicked_sig_handle.replace(Some( + widget.cover_btn.connect_clicked(clone!(@weak album_model => move |_| { + f(album_model.uri()) + })) + )); + } + + /// Unbinds a ListItem widget from an item in a model + pub fn unbind_list_item(_factory: >k::SignalListItemFactory, list_item: >k::ListItem) { + let imp = list_item.child().unwrap().downcast::().unwrap(); + let widget = imp::AlbumWidget::from_instance(&imp); + let sig_handle = widget.clicked_sig_handle.take().unwrap(); + + glib::Object::disconnect( + &imp.upcast::(), + sig_handle + ); + } + pub fn for_model(album_model: &AlbumModel, worker: Worker) -> Self { let _self = Self::new(); _self.bind(album_model, worker); diff --git a/src/app/components/album/album.ui b/src/app/components/album/album.ui index c13f09a4..0237b35f 100644 --- a/src/app/components/album/album.ui +++ b/src/app/components/album/album.ui @@ -9,12 +9,12 @@ center - center - start - 6 - 6 - vertical - 6 + center + start + 6 + 6 + vertical + 6 media-playback-start-symbolic @@ -65,13 +65,13 @@ - + diff --git a/src/app/components/artist_details/artist_details.rs b/src/app/components/artist_details/artist_details.rs index 729a81f1..60a3c615 100644 --- a/src/app/components/artist_details/artist_details.rs +++ b/src/app/components/artist_details/artist_details.rs @@ -25,7 +25,7 @@ mod imp { pub top_tracks: TemplateChild, #[template_child] - pub artist_releases: TemplateChild, + pub artist_releases: TemplateChild, } #[glib::object_subclass] @@ -84,27 +84,29 @@ impl ArtistDetailsWidget { }); } - fn bind_artist_releases( - &self, - worker: Worker, - store: &ListStore, - on_album_pressed: F, - ) where - F: Fn(String) + Clone + 'static, + fn set_model(&self, store: &ListStore, worker: Worker, on_clicked: F) + where + F: Fn(String) + Clone + 'static { - self.widget() - .artist_releases - .bind_model(Some(store.unsafe_store()), move |item| { - let item = item.downcast_ref::().unwrap(); - let child = gtk::FlowBoxChild::new(); - let album = AlbumWidget::for_model(item, worker.clone()); - let f = on_album_pressed.clone(); - album.connect_album_pressed(clone!(@weak item => move |_| { - f(item.uri()); - })); - child.set_child(Some(&album)); - child.upcast::() - }); + let gridview = &imp::ArtistDetailsWidget::from_instance(self).artist_releases; + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, list_item| { + list_item.set_child(Some(&AlbumWidget::new())); + // Onclick is handled by album and this causes two layers of highlight on hover + list_item.set_activatable(false); + }); + + factory.connect_bind(move |factory, list_item| { + AlbumWidget::bind_list_item(factory, list_item, worker.clone(), on_clicked.clone()); + }); + + factory.connect_unbind(move |factory, list_item| { + AlbumWidget::unbind_list_item(factory, list_item); + }); + + gridview.set_factory(Some(&factory)); + gridview.set_model(Some(>k::NoSelection::new(Some(store.unsafe_store())))); } } @@ -125,12 +127,12 @@ impl ArtistDetails { })); if let Some(store) = model.get_list_store() { - widget.bind_artist_releases( - worker.clone(), + widget.set_model( &*store, - clone!(@weak model => move |id| { - model.open_album(id); - }), + worker.clone(), + clone!(@weak model => move |uri| { + model.open_album(uri); + }) ); } diff --git a/src/app/components/artist_details/artist_details.ui b/src/app/components/artist_details/artist_details.ui index 974e3301..1e2dbc86 100644 --- a/src/app/components/artist_details/artist_details.ui +++ b/src/app/components/artist_details/artist_details.ui @@ -40,12 +40,10 @@ 8 1 - + 100 1 - 1 - none - 0 + 1 diff --git a/src/app/components/library/library.rs b/src/app/components/library/library.rs index cf97f578..e2b95edf 100644 --- a/src/app/components/library/library.rs +++ b/src/app/components/library/library.rs @@ -4,7 +4,6 @@ use gtk::CompositeTemplate; use std::rc::Rc; use super::LibraryModel; -use crate::app::components::utils::wrap_flowbox_item; use crate::app::components::{AlbumWidget, Component, EventListener}; use crate::app::dispatch::Worker; use crate::app::models::AlbumModel; @@ -22,7 +21,7 @@ mod imp { pub scrolled_window: TemplateChild, #[template_child] - pub flowbox: TemplateChild, + pub gridview: TemplateChild, #[template_child] pub status_page: TemplateChild, @@ -70,23 +69,29 @@ impl LibraryWidget { }); } - fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) + fn set_model(&self, store: &ListStore, worker: Worker, on_clicked: F) where - F: Fn(String) + Clone + 'static, + F: Fn(String) + Clone + 'static { - imp::LibraryWidget::from_instance(self).flowbox.bind_model( - Some(store.unsafe_store()), - move |item| { - wrap_flowbox_item(item, |album_model| { - let f = on_album_pressed.clone(); - let album = AlbumWidget::for_model(album_model, worker.clone()); - album.connect_album_pressed(clone!(@weak album_model => move |_| { - f(album_model.uri()); - })); - album - }) - }, - ); + let gridview = &imp::LibraryWidget::from_instance(self).gridview; + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, list_item| { + list_item.set_child(Some(&AlbumWidget::new())); + // Onclick is handled by album and this causes two layers of highlight on hover + list_item.set_activatable(false); + }); + + factory.connect_bind(move |factory, list_item| { + AlbumWidget::bind_list_item(factory, list_item, worker.clone(), on_clicked.clone()); + }); + + factory.connect_unbind(move |factory, list_item| { + AlbumWidget::unbind_list_item(factory, list_item); + }); + + gridview.set_factory(Some(&factory)); + gridview.set_model(Some(>k::NoSelection::new(Some(store.unsafe_store())))); } pub fn status_page(&self) -> &libadwaita::StatusPage { @@ -96,7 +101,6 @@ impl LibraryWidget { pub struct Library { widget: LibraryWidget, - worker: Worker, model: Rc, } @@ -104,26 +108,23 @@ impl Library { pub fn new(worker: Worker, model: LibraryModel) -> Self { let model = Rc::new(model); let widget = LibraryWidget::new(); + widget.connect_bottom_edge(clone!(@weak model => move || { model.load_more_albums(); })); + widget.set_model( + &model.get_list_store().unwrap(), + worker.clone(), + clone!(@weak model => move |uri| { + model.open_album(uri); + }) + ); Self { widget, - worker, model, } } - - fn bind_flowbox(&self) { - self.widget.bind_albums( - self.worker.clone(), - &*self.model.get_list_store().unwrap(), - clone!(@weak self.model as model => move |id| { - model.open_album(id); - }), - ); - } } impl EventListener for Library { @@ -131,7 +132,6 @@ impl EventListener for Library { match event { AppEvent::Started => { let _ = self.model.refresh_saved_albums(); - self.bind_flowbox(); } AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { let _ = self.model.refresh_saved_albums(); diff --git a/src/app/components/library/library.ui b/src/app/components/library/library.ui index 8f3ea44d..ba5033d4 100644 --- a/src/app/components/library/library.ui +++ b/src/app/components/library/library.ui @@ -3,34 +3,32 @@ diff --git a/src/app/components/saved_playlists/saved_playlists.rs b/src/app/components/saved_playlists/saved_playlists.rs index 3016c44e..7c6cc6b1 100644 --- a/src/app/components/saved_playlists/saved_playlists.rs +++ b/src/app/components/saved_playlists/saved_playlists.rs @@ -21,7 +21,8 @@ mod imp { pub scrolled_window: TemplateChild, #[template_child] - pub flowbox: TemplateChild, + pub gridview: TemplateChild, + #[template_child] pub status_page: TemplateChild, } @@ -68,26 +69,31 @@ impl SavedPlaylistsWidget { }); } - fn bind_albums(&self, worker: Worker, store: &ListStore, on_album_pressed: F) + fn set_model(&self, store: &ListStore, worker: Worker, on_clicked: F) where - F: Fn(String) + Clone + 'static, + F: Fn(String) + Clone + 'static { - imp::SavedPlaylistsWidget::from_instance(self) - .flowbox - .bind_model(Some(store.unsafe_store()), move |item| { - let album_model = item.downcast_ref::().unwrap(); - let child = gtk::FlowBoxChild::new(); - let album = AlbumWidget::for_model(album_model, worker.clone()); - - let f = on_album_pressed.clone(); - album.connect_album_pressed(clone!(@weak album_model => move |_| { - f(album_model.uri()); - })); - - child.set_child(Some(&album)); - child.upcast::() - }); + let gridview = &imp::SavedPlaylistsWidget::from_instance(self).gridview; + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, list_item| { + list_item.set_child(Some(&AlbumWidget::new())); + // Onclick is handled by album and this causes two layers of highlight on hover + list_item.set_activatable(false); + }); + + factory.connect_bind(move |factory, list_item| { + AlbumWidget::bind_list_item(factory, list_item, worker.clone(), on_clicked.clone()); + }); + + factory.connect_unbind(move |factory, list_item| { + AlbumWidget::unbind_list_item(factory, list_item); + }); + + gridview.set_factory(Some(&factory)); + gridview.set_model(Some(>k::NoSelection::new(Some(store.unsafe_store())))); } + pub fn get_status_page(&self) -> &libadwaita::StatusPage { &imp::SavedPlaylistsWidget::from_instance(self).status_page } @@ -95,36 +101,30 @@ impl SavedPlaylistsWidget { pub struct SavedPlaylists { widget: SavedPlaylistsWidget, - worker: Worker, model: Rc, } impl SavedPlaylists { pub fn new(worker: Worker, model: SavedPlaylistsModel) -> Self { let model = Rc::new(model); - let widget = SavedPlaylistsWidget::new(); widget.connect_bottom_edge(clone!(@weak model => move || { model.load_more_playlists(); })); + widget.set_model( + &model.get_list_store().unwrap(), + worker.clone(), + clone!(@weak model => move |uri| { + model.open_playlist(uri); + }) + ); Self { widget, - worker, model, } } - - fn bind_flowbox(&self) { - self.widget.bind_albums( - self.worker.clone(), - &*self.model.get_list_store().unwrap(), - clone!(@weak self.model as model => move |id| { - model.open_playlist(id); - }), - ); - } } impl EventListener for SavedPlaylists { @@ -132,7 +132,6 @@ impl EventListener for SavedPlaylists { match event { AppEvent::Started => { let _ = self.model.refresh_saved_playlists(); - self.bind_flowbox(); } AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => { let _ = self.model.refresh_saved_playlists(); diff --git a/src/app/components/saved_playlists/saved_playlists.ui b/src/app/components/saved_playlists/saved_playlists.ui index e01de39c..2456f623 100644 --- a/src/app/components/saved_playlists/saved_playlists.ui +++ b/src/app/components/saved_playlists/saved_playlists.ui @@ -3,34 +3,32 @@ diff --git a/src/app/components/search/search.rs b/src/app/components/search/search.rs index 496ef87e..a3ddeaa1 100644 --- a/src/app/components/search/search.rs +++ b/src/app/components/search/search.rs @@ -30,7 +30,7 @@ mod imp { pub search_results: TemplateChild, #[template_child] - pub albums_results: TemplateChild, + pub albums_results: TemplateChild, #[template_child] pub artist_results: TemplateChild, @@ -98,22 +98,29 @@ impl SearchResultsWidget { })); } - fn bind_albums_results(&self, worker: Worker, store: &gio::ListStore, on_album_pressed: F) + fn set_album_model(&self, store: &gio::ListStore, worker: Worker, on_clicked: F) where - F: Fn(String) + Clone + 'static, + F: Fn(String) + Clone + 'static { - self.widget() - .albums_results - .bind_model(Some(store), move |item| { - wrap_flowbox_item(item, |album_model| { - let f = on_album_pressed.clone(); - let album = AlbumWidget::for_model(album_model, worker.clone()); - album.connect_album_pressed(clone!(@weak album_model => move |_| { - f(album_model.uri()); - })); - album - }) - }); + let gridview = &imp::SearchResultsWidget::from_instance(self).albums_results; + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, list_item| { + list_item.set_child(Some(&AlbumWidget::new())); + // Onclick is handled by album and this causes two layers of highlight on hover + list_item.set_activatable(false); + }); + + factory.connect_bind(move |factory, list_item| { + AlbumWidget::bind_list_item(factory, list_item, worker.clone(), on_clicked.clone()); + }); + + factory.connect_unbind(move |factory, list_item| { + AlbumWidget::unbind_list_item(factory, list_item); + }); + + gridview.set_factory(Some(&factory)); + gridview.set_model(Some(>k::NoSelection::new(Some(store)))); } fn bind_artists_results(&self, worker: Worker, store: &gio::ListStore, on_artist_pressed: F) @@ -159,12 +166,12 @@ impl SearchResults { model.search(q); })); - widget.bind_albums_results( - worker.clone(), + widget.set_album_model( &album_results_model, + worker.clone(), clone!(@weak model => move |uri| { model.open_album(uri); - }), + }) ); widget.bind_artists_results( diff --git a/src/app/components/search/search.ui b/src/app/components/search/search.ui index 35a93571..e1dc12c5 100644 --- a/src/app/components/search/search.ui +++ b/src/app/components/search/search.ui @@ -54,15 +54,13 @@ never 0 - + start 1 0 start vertical - 1 - none - 0 + 1 @@ -122,4 +120,4 @@ - \ No newline at end of file + diff --git a/src/app/components/user_details/user_details.rs b/src/app/components/user_details/user_details.rs index 671ae894..75c79240 100644 --- a/src/app/components/user_details/user_details.rs +++ b/src/app/components/user_details/user_details.rs @@ -3,7 +3,6 @@ use gtk::subclass::prelude::*; use gtk::CompositeTemplate; use std::rc::Rc; -use crate::app::components::utils::wrap_flowbox_item; use crate::app::components::{display_add_css_provider, AlbumWidget, Component, EventListener}; use crate::app::{models::*, ListStore}; use crate::app::{AppEvent, BrowserEvent, Worker}; @@ -24,7 +23,7 @@ mod imp { pub user_name: TemplateChild, #[template_child] - pub user_playlists: TemplateChild, + pub user_playlists: TemplateChild, } #[glib::object_subclass] @@ -80,22 +79,29 @@ impl UserDetailsWidget { }); } - fn bind_user_playlists(&self, worker: Worker, store: &ListStore, on_pressed: F) + fn set_model(&self, store: &ListStore, worker: Worker, on_clicked: F) where - F: Fn(String) + Clone + 'static, + F: Fn(String) + Clone + 'static { - self.widget() - .user_playlists - .bind_model(Some(store.unsafe_store()), move |item| { - wrap_flowbox_item(item, |item: &AlbumModel| { - let f = on_pressed.clone(); - let album = AlbumWidget::for_model(item, worker.clone()); - album.connect_album_pressed(clone!(@weak item => move |_| { - f(item.uri()); - })); - album - }) - }); + let gridview = &imp::UserDetailsWidget::from_instance(self).user_playlists; + + let factory = gtk::SignalListItemFactory::new(); + factory.connect_setup(|_, list_item| { + list_item.set_child(Some(&AlbumWidget::new())); + // Onclick is handled by album and this causes two layers of highlight on hover + list_item.set_activatable(false); + }); + + factory.connect_bind(move |factory, list_item| { + AlbumWidget::bind_list_item(factory, list_item, worker.clone(), on_clicked.clone()); + }); + + factory.connect_unbind(move |factory, list_item| { + AlbumWidget::unbind_list_item(factory, list_item); + }); + + gridview.set_factory(Some(&factory)); + gridview.set_model(Some(>k::NoSelection::new(Some(store.unsafe_store())))); } } @@ -116,12 +122,12 @@ impl UserDetails { })); if let Some(store) = model.get_list_store() { - widget.bind_user_playlists( - worker, + widget.set_model( &*store, + worker.clone(), clone!(@weak model => move |uri| { model.open_playlist(uri); - }), + }) ); } diff --git a/src/app/components/user_details/user_details.ui b/src/app/components/user_details/user_details.ui index e0125738..c29a5d33 100644 --- a/src/app/components/user_details/user_details.ui +++ b/src/app/components/user_details/user_details.ui @@ -30,13 +30,11 @@ - + 100 start 1 - 1 - none - 0 + 1