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: Show followed artists in the sidebar #671

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions po/POTFILES
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ src/app/components/scrolling_header/scrolling_header.blp
src/app/components/details/album_header.blp
src/app/components/details/release_details.blp
src/app/components/details/details.blp
src/app/components/followed_artists/followed_artists.blp
src/app/components/now_playing/now_playing.blp
src/app/components/login/login.blp
src/app/components/playlist_details/playlist_details.blp
Expand Down
5 changes: 5 additions & 0 deletions src/api/api_models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,11 @@ impl WithImages for Artist {
}
}

#[derive(Deserialize, Debug, Clone)]
pub struct Artists {
pub artists: Page<Artist>,
}

#[derive(Deserialize, Debug, Clone)]
pub struct User {
pub id: String,
Expand Down
23 changes: 23 additions & 0 deletions src/api/cached_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ pub trait SpotifyApiClient {
) -> BoxFuture<SpotifyResult<()>>;

fn player_state(&self) -> BoxFuture<SpotifyResult<ConnectPlayerState>>;

fn get_followed_artists(
&self,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Artists>>;
}

enum SpotCacheKey<'a> {
Expand Down Expand Up @@ -824,6 +830,23 @@ impl SpotifyApiClient for CachedSpotifyClient {
.send_no_response(),
)
}

fn get_followed_artists(
&self,
offset: usize,
limit: usize,
) -> BoxFuture<SpotifyResult<Artists>> {
Box::pin(async move {
let result = self
.client
.get_followed_artists(offset, limit)
.send()
.await?
.deserialize()
.ok_or(SpotifyApiError::NoContent)?;
Ok(result.into())
})
}
}

#[cfg(test)]
Expand Down
12 changes: 12 additions & 0 deletions src/api/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,18 @@ impl SpotifyClient {
.method(Method::PUT)
.uri("/v1/me/player/volume".to_string(), Some(&query))
}

pub(crate) fn get_followed_artists(
&self,
_offset: usize,
_limit: usize,
) -> SpotifyRequest<'_, (), Artists> {
let query = make_query_params().append_pair("type", "artist").finish();

self.request()
.method(Method::GET)
.uri("/v1/me/following".to_string(), Some(&query))
}
}

#[cfg(test)]
Expand Down
36 changes: 36 additions & 0 deletions src/app/components/followed_artists/followed_artists.blp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Gtk 4.0;
using Adw 1;

template $FollowedArtistsWidget : Box {
ScrolledWindow scrolled_window {
hexpand: true;
vexpand: true;
vscrollbar-policy: always;
min-content-width: 250;

Overlay overlay {
FlowBox flowbox {
margin-start: 8;
margin-end: 8;
margin-top: 8;
margin-bottom: 8;
min-children-per-line: 1;
selection-mode: none;
activate-on-single-click: false;
}

[overlay]
Adw.StatusPage status_page {
/* Translators: A title that is shown when the user has no followed artists. */

title: _("You have no followed artists.");

/* Translators: A description of what happens when the user has followed artists. */

description: _("Your followed artists will be shown here.");
icon-name: "avatar-default-symbolic";
visible: true;
}
}
}
}
155 changes: 155 additions & 0 deletions src/app/components/followed_artists/followed_artists.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::CompositeTemplate;
use std::rc::Rc;

use super::FollowedArtistsModel;
use crate::app::components::artist::ArtistWidget;
use crate::app::components::{Component, EventListener};
use crate::app::dispatch::Worker;
use crate::app::models::ArtistModel;
use crate::app::state::LoginEvent;
use crate::app::{AppEvent, BrowserEvent, ListStore};

mod imp {

use super::*;

#[derive(Debug, Default, CompositeTemplate)]
#[template(resource = "/dev/alextren/Spot/components/followed_artists.ui")]
pub struct FollowedArtistsWidget {
#[template_child]
pub scrolled_window: TemplateChild<gtk::ScrolledWindow>,

#[template_child]
pub flowbox: TemplateChild<gtk::FlowBox>,
#[template_child]
pub status_page: TemplateChild<libadwaita::StatusPage>,
}

#[glib::object_subclass]
impl ObjectSubclass for FollowedArtistsWidget {
const NAME: &'static str = "FollowedArtistsWidget";
type Type = super::FollowedArtistsWidget;
type ParentType = gtk::Box;

fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}

fn instance_init(obj: &glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}

impl ObjectImpl for FollowedArtistsWidget {}
impl WidgetImpl for FollowedArtistsWidget {}
impl BoxImpl for FollowedArtistsWidget {}
}

glib::wrapper! {
pub struct FollowedArtistsWidget(ObjectSubclass<imp::FollowedArtistsWidget>) @extends gtk::Widget, gtk::Box;
}

impl FollowedArtistsWidget {
pub fn new() -> Self {
glib::Object::new()
}

fn connect_bottom_edge<F>(&self, f: F)
where
F: Fn() + 'static,
{
self.imp()
.scrolled_window
.connect_edge_reached(move |_, pos| {
if let gtk::PositionType::Bottom = pos {
f()
}
});
}

fn bind_artists<F>(&self, worker: Worker, store: &ListStore<ArtistModel>, on_artist_pressed: F)
where
F: Fn(String) + Clone + 'static,
{
self.imp()
.flowbox
.bind_model(Some(store.unsafe_store()), move |item| {
let artist_model = item.downcast_ref::<ArtistModel>().unwrap();
let child = gtk::FlowBoxChild::new();
let artist = ArtistWidget::for_model(artist_model, worker.clone());

let f = on_artist_pressed.clone();
artist.connect_artist_pressed(clone!(@weak artist_model => move |_| {
f(artist_model.id());
}));

child.set_child(Some(&artist));
child.upcast::<gtk::Widget>()
});
}
pub fn get_status_page(&self) -> &libadwaita::StatusPage {
&self.imp().status_page
}
}

pub struct FollowedArtists {
widget: FollowedArtistsWidget,
worker: Worker,
model: Rc<FollowedArtistsModel>,
}

impl FollowedArtists {
pub fn new(worker: Worker, model: FollowedArtistsModel) -> Self {
let model = Rc::new(model);

let widget = FollowedArtistsWidget::new();

widget.connect_bottom_edge(clone!(@weak model => move || {
model.load_more_followed_artists();
}));

Self {
widget,
worker,
model,
}
}

fn bind_flowbox(&self) {
self.widget.bind_artists(
self.worker.clone(),
&self.model.get_list_store().unwrap(),
clone!(@weak self.model as model => move |id| {
model.open_artist(id);
}),
);
}
}

impl EventListener for FollowedArtists {
fn on_event(&mut self, event: &AppEvent) {
match event {
AppEvent::Started => {
let _ = self.model.refresh_followed_artists();
self.bind_flowbox();
}
AppEvent::LoginEvent(LoginEvent::LoginCompleted(_)) => {
let _ = self.model.refresh_followed_artists();
}
AppEvent::BrowserEvent(BrowserEvent::FollowedArtistsUpdated) => {
self.widget
.get_status_page()
.set_visible(!self.model.has_followed_artists());
}
_ => {}
}
}
}

impl Component for FollowedArtists {
fn get_root_widget(&self) -> &gtk::Widget {
self.widget.as_ref()
}
}
98 changes: 98 additions & 0 deletions src/app/components/followed_artists/followed_artists_model.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
use std::cell::Ref;
use std::ops::Deref;
use std::rc::Rc;

use crate::app::models::*;
use crate::app::state::HomeState;
use crate::app::{ActionDispatcher, AppAction, AppModel, BrowserAction, ListStore};

pub struct FollowedArtistsModel {
app_model: Rc<AppModel>,
dispatcher: Box<dyn ActionDispatcher>,
}

impl FollowedArtistsModel {
pub fn new(app_model: Rc<AppModel>, dispatcher: Box<dyn ActionDispatcher>) -> Self {
Self {
app_model,
dispatcher,
}
}

fn state(&self) -> Option<Ref<'_, HomeState>> {
self.app_model.map_state_opt(|s| s.browser.home_state())
}

pub fn get_list_store(&self) -> Option<impl Deref<Target = ListStore<ArtistModel>> + '_> {
Some(Ref::map(self.state()?, |s| &s.followed_artists))
}

pub fn refresh_followed_artists(&self) -> Option<()> {
let api = self.app_model.get_spotify();
let batch_size = self.state()?.next_followed_artists_page.batch_size;

self.dispatcher
.call_spotify_and_dispatch(move || async move {
api.get_followed_artists(0, batch_size)
.await
.map(|artists| {
BrowserAction::SetFollowedArtistsContent(
artists
.artists
.into_iter()
.map(|artist| ArtistDescription {
id: artist.id,
name: artist.name,
albums: Vec::new(),
top_tracks: Vec::new(),
})
.collect(),
)
.into()
})
});

Some(())
}

pub fn has_followed_artists(&self) -> bool {
self.get_list_store()
.map(|list| list.len() > 0)
.unwrap_or(false)
}

pub fn load_more_followed_artists(&self) -> Option<()> {
let api = self.app_model.get_spotify();

let next_page = &self.state()?.next_followed_artists_page;
let batch_size = next_page.batch_size;
let offset = next_page.next_offset?;

self.dispatcher
.call_spotify_and_dispatch(move || async move {
api.get_followed_artists(offset, batch_size)
.await
.map(|artists| {
BrowserAction::AppendFollowedArtistsContent(
artists
.artists
.into_iter()
.map(|artist| ArtistDescription {
id: artist.id,
name: artist.name,
albums: Vec::new(),
top_tracks: Vec::new(),
})
.collect(),
)
.into()
})
});

Some(())
}

pub fn open_artist(&self, id: String) {
self.dispatcher.dispatch(AppAction::ViewArtist(id));
}
}
5 changes: 5 additions & 0 deletions src/app/components/followed_artists/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod followed_artists;
mod followed_artists_model;

pub use followed_artists::*;
pub use followed_artists_model::*;
3 changes: 3 additions & 0 deletions src/app/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ pub use now_playing::*;
mod device_selector;
pub use device_selector::*;

mod followed_artists;
pub use followed_artists::*;

mod saved_tracks;
pub use saved_tracks::*;

Expand Down
Loading