Skip to content

Commit

Permalink
Artist view update (#537)
Browse files Browse the repository at this point in the history
Co-authored-by: Samuel Oldham <so9010sami@gmail.com>
Co-authored-by: Jackson Goode <jacksongoode@proton.me>
  • Loading branch information
3 people authored Oct 16, 2024
1 parent 38d1c75 commit 0080967
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 11 deletions.
1 change: 1 addition & 0 deletions psst-gui/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub const CLOSE_ALL_WINDOWS: Selector = Selector::new("app.close-all-windows");
pub const QUIT_APP_WITH_SAVE: Selector = Selector::new("app.quit-with-save");
pub const SET_FOCUS: Selector = Selector::new("app.set-focus");
pub const COPY: Selector<String> = Selector::new("app.copy-to-clipboard");
pub const GO_TO_URL: Selector<String> = Selector::new("app.go-to-url");

// Find
pub const TOGGLE_FINDER: Selector = Selector::new("app.show-finder");
Expand Down
15 changes: 15 additions & 0 deletions psst-gui/src/data/artist.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub struct ArtistDetail {
pub albums: Promise<ArtistAlbums, ArtistLink>,
pub top_tracks: Promise<ArtistTracks, ArtistLink>,
pub related_artists: Promise<Cached<Vector<Artist>>, ArtistLink>,
pub artist_info: Promise<ArtistInfo, ArtistLink>,
}

#[derive(Clone, Data, Lens, Deserialize)]
Expand Down Expand Up @@ -40,6 +41,20 @@ pub struct ArtistAlbums {
pub compilations: Vector<Arc<Album>>,
pub appears_on: Vector<Arc<Album>>,
}
#[derive(Clone, Data, Lens)]
pub struct ArtistInfo {
pub main_image: Arc<str>,
pub stats: ArtistStats,
pub bio: String,
pub artist_links: Vector<String>,
}

#[derive(Clone, Data, Lens)]
pub struct ArtistStats {
pub followers: String,
pub monthly_listeners: String,
pub world_rank: String,
}

#[derive(Clone, Data, Lens)]
pub struct ArtistTracks {
Expand Down
3 changes: 2 additions & 1 deletion psst-gui/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ use psst_core::{item_id::ItemId, session::SessionService};

pub use crate::data::{
album::{Album, AlbumDetail, AlbumLink, AlbumType},
artist::{Artist, ArtistAlbums, ArtistDetail, ArtistLink, ArtistTracks},
artist::{Artist, ArtistAlbums, ArtistInfo, ArtistStats, ArtistDetail, ArtistLink, ArtistTracks},
config::{AudioQuality, Authentication, Config, Preferences, PreferencesTab, Theme},
ctx::Ctx,
find::{FindQuery, Finder, MatchFindQuery},
Expand Down Expand Up @@ -152,6 +152,7 @@ impl AppState {
albums: Promise::Empty,
top_tracks: Promise::Empty,
related_artists: Promise::Empty,
artist_info: Promise::Empty,
},
playlist_detail: PlaylistDetail {
playlist: Promise::Empty,
Expand Down
3 changes: 3 additions & 0 deletions psst-gui/src/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ impl AppDelegate<AppState> for Delegate {
} else if let Some(text) = cmd.get(cmd::COPY) {
Application::global().clipboard().put_string(text);
Handled::Yes
} else if let Some(text) = cmd.get(cmd::GO_TO_URL) {
let _ = webbrowser::open(text);
Handled::Yes
} else if let Handled::Yes = self.command_image(ctx, target, cmd, data) {
Handled::Yes
} else if let Some(link) = cmd.get(UNFOLLOW_PLAYLIST_CONFIRM) {
Expand Down
219 changes: 213 additions & 6 deletions psst-gui/src/ui/artist.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,32 @@
use druid::{
im::Vector,
kurbo::Circle,
widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List},
Data, Insets, LensExt, LocalizedString, Menu, MenuItem, Selector, UnitPoint, Widget, WidgetExt,
widget::{CrossAxisAlignment, Flex, Label, LabelText, LineBreaking, List, Scroll},
BoxConstraints, Data, Env, Event, EventCtx, Insets, LayoutCtx, LensExt, LifeCycle,
LifeCycleCtx, LocalizedString, Menu, MenuItem, PaintCtx, Point, Selector, Size, UnitPoint,
UpdateCtx, Widget, WidgetExt, WidgetPod,
};

use crate::{
cmd,
data::{
AppState, Artist, ArtistAlbums, ArtistDetail, ArtistLink, ArtistTracks, Cached, Ctx, Nav,
WithCtx,
AppState, Artist, ArtistAlbums, ArtistDetail, ArtistInfo, ArtistLink, ArtistTracks, Cached,
Ctx, Nav, WithCtx,
},
webapi::WebApi,
widget::{Async, MyWidgetExt, RemoteImage},
widget::{Async, Empty, MyWidgetExt, RemoteImage},
};

use super::{album, playable, theme, track, utils};
use super::{
album, playable, theme, track,
utils::{self},
};

pub const LOAD_DETAIL: Selector<ArtistLink> = Selector::new("app.artist.load-detail");

pub fn detail_widget() -> impl Widget<AppState> {
Flex::column()
.with_child(async_artist_info().padding((theme::grid(1.0), 0.0)))
.with_child(async_top_tracks_widget())
.with_child(async_albums_widget().padding((theme::grid(1.0), 0.0)))
.with_child(async_related_widget().padding((theme::grid(1.0), 0.0)))
Expand Down Expand Up @@ -71,6 +77,23 @@ fn async_albums_widget() -> impl Widget<AppState> {
)
}

fn async_artist_info() -> impl Widget<AppState> {
Async::new(utils::spinner_widget, artist_info_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
AppState::artist_detail.then(ArtistDetail::artist_info),
)
.then(Ctx::in_promise()),
)
.on_command_async(
LOAD_DETAIL,
|d| WebApi::global().get_artist_info(&d.id),
|_, data, d| data.artist_detail.artist_info.defer(d),
|_, data, r| data.artist_detail.artist_info.update(r),
)
}

fn async_related_widget() -> impl Widget<AppState> {
Async::new(utils::spinner_widget, related_widget, utils::error_widget)
.lens(AppState::artist_detail.then(ArtistDetail::related_artists))
Expand Down Expand Up @@ -144,6 +167,139 @@ pub fn cover_widget(size: f64) -> impl Widget<Artist> {
.clip(Circle::new((radius, radius), radius))
}

fn artist_info_widget() -> impl Widget<WithCtx<ArtistInfo>> {
let size = theme::grid(16.0);

let artist_image = RemoteImage::new(
utils::placeholder_widget(),
move |artist: &ArtistInfo, _| Some(artist.main_image.clone()),
)
.fix_size(size, size)
.clip(Size::new(size, size).to_rounded_rect(4.0))
.lens(Ctx::data());

let biography = Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(
Scroll::new(
Label::new(|data: &ArtistInfo, _env: &_| data.bio.clone())
.with_line_break_mode(LineBreaking::WordWrap)
.with_text_size(theme::TEXT_SIZE_NORMAL)
.lens(Ctx::data()),
)
.vertical()
.fix_height(size - theme::grid(1.5)),
);

let artist_stats = Flex::column()
.with_child(stat_row("Followers:", |info: &ArtistInfo| {
format!("{} followers", info.stats.followers)
}))
.with_default_spacer()
.with_child(stat_row("Monthly Listeners:", |info: &ArtistInfo| {
format!("{} listeners", info.stats.monthly_listeners)
}))
.with_default_spacer()
.with_child(stat_row("Ranking:", |info: &ArtistInfo| {
if !info.stats.world_rank.starts_with("0") {
format!("#{} in the world", info.stats.world_rank)
} else {
"N/A".to_string()
}
}));

Flex::row()
.with_child(artist_image)
.with_spacer(theme::grid(1.0))
.with_flex_child(
Flex::row().with_flex_child(ArtistInfoLayout::new(biography, artist_stats), 1.0),
1.0,
)
.context_menu(|artist| artist_info_menu(&artist.data))
.padding((0.0, theme::grid(1.0))) // Keep overall vertical padding
}

struct ArtistInfoLayout<T, B, S> {
biography: WidgetPod<T, B>,
stats: WidgetPod<T, S>,
}

impl<T, B, S> ArtistInfoLayout<T, B, S>
where
T: Data,
B: Widget<T>,
S: Widget<T>,
{
fn new(biography: B, stats: S) -> Self {
Self {
biography: WidgetPod::new(biography),
stats: WidgetPod::new(stats),
}
}
}

impl<T: Data, B: Widget<T>, S: Widget<T>> Widget<T> for ArtistInfoLayout<T, B, S> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) {
self.biography.event(ctx, event, data, env);
self.stats.event(ctx, event, data, env);
}

fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {
self.biography.lifecycle(ctx, event, data, env);
self.stats.lifecycle(ctx, event, data, env);
}

fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &T, data: &T, env: &Env) {
self.biography.update(ctx, data, env);
self.stats.update(ctx, data, env);
}

fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size {
let max = bc.max();
let wide_layout = max.width > theme::grid(60.0) + theme::GRID * 3.45;
let padding = theme::grid(1.0);

if wide_layout {
let biography_width = max.width * 0.67 - padding / 2.0;
let stats_width = max.width * 0.33 - padding / 2.0;

let biography_bc =
BoxConstraints::new(Size::ZERO, Size::new(biography_width, max.height));
let stats_bc = BoxConstraints::new(Size::ZERO, Size::new(stats_width, max.height));

let biography_size = self.biography.layout(ctx, &biography_bc, data, env);
let stats_size = self.stats.layout(ctx, &stats_bc, data, env);

self.biography.set_origin(ctx, Point::ORIGIN);
self.stats
.set_origin(ctx, Point::new(biography_width + padding, 0.0));

Size::new(max.width, biography_size.height.max(stats_size.height))
} else {
let biography_size = self.biography.layout(ctx, bc, data, env);
let stats_bc = BoxConstraints::new(
Size::ZERO,
Size::new(max.width, max.height - biography_size.height - padding),
);
let stats_size = self.stats.layout(ctx, &stats_bc, data, env);

self.biography.set_origin(ctx, Point::ORIGIN);
self.stats
.set_origin(ctx, Point::new(0.0, biography_size.height + padding));

Size::new(
max.width,
biography_size.height + padding + stats_size.height,
)
}
}

fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
self.biography.paint(ctx, data, env);
self.stats.paint(ctx, data, env);
}
}

fn top_tracks_widget() -> impl Widget<WithCtx<ArtistTracks>> {
playable::list_widget(playable::Display {
track: track::Display {
Expand Down Expand Up @@ -189,6 +345,39 @@ fn header_widget<T: Data>(text: impl Into<LabelText<T>>) -> impl Widget<T> {
.padding(Insets::new(0.0, theme::grid(2.0), 0.0, theme::grid(1.0)))
}

fn artist_info_menu(artist: &ArtistInfo) -> Menu<AppState> {
let mut menu = Menu::empty();

for artist_link in &artist.artist_links {
let platform = if artist_link.contains("wikipedia.org") {
"Wikipedia"
} else {
artist_link
.strip_prefix("https://")
.unwrap_or(artist_link)
.split('.')
.next()
.unwrap_or("Unknown")
};

let title = LocalizedString::new("menu-item-go-to-social").with_placeholder(format!(
"Go to their {}",
platform
.chars()
.next()
.unwrap()
.to_uppercase()
.collect::<String>()
+ &platform[1..]
));

menu =
menu.entry(MenuItem::new(title).command(cmd::GO_TO_URL.with(artist_link.to_owned())));
}

menu
}

fn artist_menu(artist: &ArtistLink) -> Menu<AppState> {
let mut menu = Menu::empty();

Expand All @@ -201,3 +390,21 @@ fn artist_menu(artist: &ArtistLink) -> Menu<AppState> {

menu
}

fn stat_row(
label: &'static str,
value_func: impl Fn(&ArtistInfo) -> String + 'static,
) -> impl Widget<WithCtx<ArtistInfo>> {
Flex::row()
.with_child(
Label::new(label)
.with_text_size(theme::TEXT_SIZE_SMALL)
.with_text_color(theme::PLACEHOLDER_COLOR),
)
.with_spacer(theme::grid(0.5))
.with_child(
Label::new(move |ctx: &WithCtx<ArtistInfo>, _env: &_| value_func(&ctx.data))
.with_text_size(theme::TEXT_SIZE_SMALL),
)
.align_left()
}
Loading

0 comments on commit 0080967

Please sign in to comment.