Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Use GtkGridView for albums #480

Draft
wants to merge 9 commits into
base: development
Choose a base branch
from
3 changes: 2 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ Nicolas Fella
Fridolin Weisser
Jan Przebor
Warren Hu
bbb651
bbb651
Ondřej Míchal
73 changes: 73 additions & 0 deletions src/app/components/album/album.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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<Option<glib::SignalHandlerId>>,

#[template_child]
pub album_label: TemplateChild<gtk::Label>,

Expand Down Expand Up @@ -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<F>(_factory: &gtk::SignalListItemFactory, list_item: &gtk::ListItem, worker: Worker, f: F)
where
F: Fn(String) + 'static
{
let album_model = list_item.item().unwrap().downcast::<AlbumModel>().unwrap();

let imp = list_item.child().unwrap().downcast::<AlbumWidget>().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: &gtk::SignalListItemFactory, list_item: &gtk::ListItem) {
let imp = list_item.child().unwrap().downcast::<AlbumWidget>().unwrap();
let widget = imp::AlbumWidget::from_instance(&imp);
let sig_handle = widget.clicked_sig_handle.take().unwrap();

glib::Object::disconnect(
&imp.upcast::<glib::Object>(),
sig_handle
);
}

pub fn for_model(album_model: &AlbumModel, worker: Worker) -> Self {
let _self = Self::new();
_self.bind(album_model, worker);
Expand Down
20 changes: 10 additions & 10 deletions src/app/components/album/album.ui
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
<property name="halign">center</property>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkImage" id="cover_image">
<property name="icon-name">media-playback-start-symbolic</property>
Expand Down Expand Up @@ -65,13 +65,13 @@
</child>
</object>
</child>
<style>
<class name="flat"/>
</style>
<style>
<class name="flat"/>
</style>
</object>
</child>
<style>
<class name="container"/>
<class name="container"/>
<class name="album"/>
</style>
</template>
Expand Down
54 changes: 28 additions & 26 deletions src/app/components/artist_details/artist_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mod imp {
pub top_tracks: TemplateChild<gtk::ListView>,

#[template_child]
pub artist_releases: TemplateChild<gtk::FlowBox>,
pub artist_releases: TemplateChild<gtk::GridView>,
}

#[glib::object_subclass]
Expand Down Expand Up @@ -84,27 +84,29 @@ impl ArtistDetailsWidget {
});
}

fn bind_artist_releases<F>(
&self,
worker: Worker,
store: &ListStore<AlbumModel>,
on_album_pressed: F,
) where
F: Fn(String) + Clone + 'static,
fn set_model<F>(&self, store: &ListStore<AlbumModel>, 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::<AlbumModel>().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::<gtk::Widget>()
});
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(&gtk::NoSelection::new(Some(store.unsafe_store()))));
}
}

Expand All @@ -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);
})
);
}

Expand Down
6 changes: 2 additions & 4 deletions src/app/components/artist_details/artist_details.ui
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,10 @@
<property name="margin-bottom">8</property>
<property name="expanded">1</property>
<child>
<object class="GtkFlowBox" id="artist_releases">
<object class="GtkGridView" id="artist_releases">
<property name="height-request">100</property>
<property name="hexpand">1</property>
<property name="min-children-per-line">1</property>
<property name="selection-mode">none</property>
<property name="activate-on-single-click">0</property>
<property name="min-columns">1</property>
</object>
</child>
<child type="label">
Expand Down
60 changes: 30 additions & 30 deletions src/app/components/library/library.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -22,7 +21,7 @@ mod imp {
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,

#[template_child]
pub flowbox: TemplateChild<gtk::FlowBox>,
pub gridview: TemplateChild<gtk::GridView>,

#[template_child]
pub status_page: TemplateChild<libadwaita::StatusPage>,
Expand Down Expand Up @@ -70,23 +69,29 @@ impl LibraryWidget {
});
}

fn bind_albums<F>(&self, worker: Worker, store: &ListStore<AlbumModel>, on_album_pressed: F)
fn set_model<F>(&self, store: &ListStore<AlbumModel>, 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(&gtk::NoSelection::new(Some(store.unsafe_store()))));
}

pub fn status_page(&self) -> &libadwaita::StatusPage {
Expand All @@ -96,42 +101,37 @@ impl LibraryWidget {

pub struct Library {
widget: LibraryWidget,
worker: Worker,
model: Rc<LibraryModel>,
}

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 {
fn on_event(&mut self, event: &AppEvent) {
match event {
AppEvent::Started => {
let _ = self.model.refresh_saved_albums();
self.bind_flowbox();
}
AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => {
let _ = self.model.refresh_saved_albums();
Expand Down
42 changes: 20 additions & 22 deletions src/app/components/library/library.ui
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,32 @@
<requires lib="gtk" version="4.0"/>
<template class="LibraryWidget" parent="GtkBox">
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<property name="vscrollbar-policy">always</property>
<property name="min-content-width">250</property>
<property name="child">
<object class="GtkOverlay" id="overlay">
<child>
<object class="GtkFlowBox" id="flowbox">
<object class="GtkOverlay" id="overlay">
<child>
<object class="GtkScrolledWindow" id="scrolled_window">
<property name="hexpand">1</property>
<property name="vexpand">1</property>
<property name="vscrollbar-policy">always</property>
<property name="min-content-width">250</property>
<property name="child">
<object class="GtkGridView" id="gridview">
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="min-children-per-line">1</property>
<property name="selection-mode">none</property>
<property name="activate-on-single-click">0</property>
<property name="min-columns">1</property>
</object>
</child>
<child type="overlay">
<object class="AdwStatusPage" id="status_page">
<property name="title" translatable="yes" comments="A title that is shown when the user has not saved any albums.">You have no saved albums.</property>
<property name="description" translatable="yes" comments="A description of what happens when the user has saved albums.">Your library will be shown here.</property>
<property name="icon-name">emblem-music-symbolic</property>
<property name="visible">true</property>
</object>
</child>
</property>
</object>
</child>
<child type="overlay">
<object class="AdwStatusPage" id="status_page">
<property name="title" translatable="yes" comments="A title that is shown when the user has not saved any albums.">You have no saved albums.</property>
<property name="description" translatable="yes" comments="A description of what happens when the user has saved albums.">Your library will be shown here.</property>
<property name="icon-name">emblem-music-symbolic</property>
<property name="visible">true</property>
</object>
</property>
</child>
</object>
</child>
</template>
Expand Down
Loading