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

Lyric support #535

Merged
merged 13 commits into from
Oct 7, 2024
2 changes: 1 addition & 1 deletion psst-gui/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fn add_windows_icon() {
res.compile().expect("Could not attach exe icon");

fn load_images() -> Vec<IcoFrame<'static>> {
let sizes = vec![32, 64, 128, 256];
let sizes = [32, 64, 128, 256];
sizes
.iter()
.map(|s| {
Expand Down
1 change: 1 addition & 0 deletions psst-gui/src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ pub const PLAY_STOP: Selector = Selector::new("app.play-stop");
pub const ADD_TO_QUEUE: Selector<(QueueEntry, PlaybackItem)> = Selector::new("app.add-to-queue");
pub const PLAY_QUEUE_BEHAVIOR: Selector<QueueBehavior> = Selector::new("app.play-queue-behavior");
pub const PLAY_SEEK: Selector<f64> = Selector::new("app.play-seek");
pub const SKIP_TO_POSITION: Selector<u64> = Selector::new("app.skip-to-position");

// Sorting control
pub const SORT_BY_DATE_ADDED: Selector = Selector::new("app.sort-by-date-added");
Expand Down
6 changes: 3 additions & 3 deletions psst-gui/src/controller/nav.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
use druid::widget::{prelude::*, Controller};

use crate::{
cmd,
data::{AppState, Nav, SpotifyUrl},
ui::{album, artist, library, playlist, recommend, search, show},
ui::{album, artist, library, lyrics, playlist, recommend, search, show},
};
use druid::widget::{prelude::*, Controller};

pub struct NavController;

impl NavController {
fn load_route_data(&self, ctx: &mut EventCtx, data: &mut AppState) {
match &data.nav {
Nav::Home => {}
Nav::Lyrics => {}
Nav::SavedTracks => {
if !data.library.saved_tracks.is_resolved() {
ctx.submit_command(library::LOAD_TRACKS);
Expand Down
22 changes: 21 additions & 1 deletion psst-gui/src/controller/playback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ use souvlaki::{

use crate::{
cmd,
data::{AppState, Config, Playback, PlaybackOrigin, PlaybackState, QueueBehavior, QueueEntry},
data::Nav,
data::{
AppState, Config, NowPlaying, Playback, PlaybackOrigin, PlaybackState, QueueBehavior,
QueueEntry,
},
ui::lyrics,
};

pub struct PlaybackController {
Expand Down Expand Up @@ -310,6 +315,12 @@ impl PlaybackController {
},
}));
}

fn update_lyrics(&self, ctx: &mut EventCtx, data: &AppState, now_playing: &NowPlaying) {
if matches!(data.nav, Nav::Lyrics) {
ctx.submit_command(lyrics::SHOW_LYRICS.with(now_playing.clone()));
}
}
SO9010 marked this conversation as resolved.
Show resolved Hide resolved
}

impl<W> Controller<AppState, W> for PlaybackController
Expand Down Expand Up @@ -348,6 +359,9 @@ where
data.start_playback(queued.item, queued.origin, progress.to_owned());
self.update_media_control_playback(&data.playback);
self.update_media_control_metadata(&data.playback);
if let Some(now_playing) = &data.playback.now_playing {
self.update_lyrics(ctx, data, now_playing);
}
} else {
log::warn!("played item not found in playback queue");
}
Expand Down Expand Up @@ -436,6 +450,12 @@ where
}
ctx.set_handled();
}
Event::Command(cmd) if cmd.is(cmd::SKIP_TO_POSITION) => {
let location = cmd.get_unchecked(cmd::SKIP_TO_POSITION);
self.seek(Duration::from_millis(location.clone()));

ctx.set_handled();
}
// Keyboard shortcuts.
Event::KeyDown(key) if key.code == Code::Space => {
self.pause_or_resume();
Expand Down
4 changes: 3 additions & 1 deletion psst-gui/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub use crate::data::{
search::{Search, SearchResults, SearchTopic},
show::{Episode, EpisodeId, EpisodeLink, Show, ShowDetail, ShowEpisodes, ShowLink},
slider_scroll_scale::SliderScrollScale,
track::{AudioAnalysis, Track, TrackId},
track::{AudioAnalysis, Track, TrackId, TrackLines},
user::{PublicUser, UserProfile},
utils::{Cached, Float64, Image, Page},
};
Expand Down Expand Up @@ -83,6 +83,7 @@ pub struct AppState {
pub alerts: Vector<Alert>,
pub finder: Finder,
pub added_queue: Vector<QueueEntry>,
pub lyrics: Promise<Vector<TrackLines>>,
}

impl AppState {
Expand Down Expand Up @@ -164,6 +165,7 @@ impl AppState {
common_ctx,
alerts: Vector::new(),
finder: Finder::new(),
lyrics: Promise::Empty,
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions psst-gui/src/data/nav.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use super::RecommendationsRequest;
#[derive(Copy, Clone, Debug, Data, PartialEq, Eq, Hash)]
pub enum Route {
Home,
Lyrics,
SavedTracks,
SavedAlbums,
SavedShows,
Expand All @@ -26,6 +27,7 @@ pub enum Route {
pub enum Nav {
#[default]
Home,
Lyrics,
SavedTracks,
SavedAlbums,
SavedShows,
Expand All @@ -41,6 +43,7 @@ impl Nav {
pub fn route(&self) -> Route {
match self {
Nav::Home => Route::Home,
Nav::Lyrics => Route::Lyrics,
Nav::SavedTracks => Route::SavedTracks,
Nav::SavedAlbums => Route::SavedAlbums,
Nav::SavedShows => Route::SavedShows,
Expand All @@ -56,6 +59,7 @@ impl Nav {
pub fn title(&self) -> String {
match self {
Nav::Home => "Home".to_string(),
Nav::Lyrics => "Lyrics".to_string(),
Nav::SavedTracks => "Saved Tracks".to_string(),
Nav::SavedAlbums => "Saved Albums".to_string(),
Nav::SavedShows => "Saved Podcasts".to_string(),
Expand All @@ -71,6 +75,7 @@ impl Nav {
pub fn full_title(&self) -> String {
match self {
Nav::Home => "Home".to_string(),
Nav::Lyrics => "Lyrics".to_string(),
Nav::SavedTracks => "Saved Tracks".to_string(),
Nav::SavedAlbums => "Saved Albums".to_string(),
Nav::SavedShows => "Saved Shows".to_string(),
Expand Down
9 changes: 9 additions & 0 deletions psst-gui/src/data/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct Track {
pub popularity: Option<u32>,
#[serde(skip)]
pub track_pos: usize,
pub lyrics: Option<Vector<TrackLines>>,
}

impl Track {
Expand Down Expand Up @@ -74,6 +75,14 @@ impl Track {
}
}

#[derive(Clone, Debug, Data, Lens, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct TrackLines {
pub start_time_ms: String,
pub words: String,
pub end_time_ms: String,
}

#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Hash, Deserialize, Serialize)]
#[serde(try_from = "String")]
#[serde(into = "String")]
Expand Down
52 changes: 24 additions & 28 deletions psst-gui/src/ui/home.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,16 @@ pub fn home_widget() -> impl Widget<AppState> {
}

fn simple_title_label(title: &str) -> impl Widget<AppState> {
Flex::column()
.with_default_spacer()
.with_child(Label::new(title)
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0))
Flex::column().with_default_spacer().with_child(
Label::new(title)
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0)),
)
}

pub fn made_for_you() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -64,7 +63,7 @@ pub fn made_for_you() -> impl Widget<AppState> {
}

pub fn recommended_stations() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -82,27 +81,24 @@ pub fn recommended_stations() -> impl Widget<AppState> {

fn uniquely_yours_results_widget() -> impl Widget<WithCtx<MixedView>> {
Either::new(
|results: &WithCtx<MixedView>, _| {
results.data.playlists.is_empty()
},
|results: &WithCtx<MixedView>, _| results.data.playlists.is_empty(),
Empty,
Flex::column().with_default_spacer()
.with_child(Label::new("Uniquely yours")
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0))
).with_child(
Scroll::new(
Flex::row()
.with_child(playlist_results_widget())
Flex::column()
.with_default_spacer()
.with_child(
Label::new("Uniquely yours")
.with_text_size(theme::grid(2.5))
.align_left()
.padding((theme::grid(1.5), 0.0)),
)
.align_left(),
),
.with_child(
Scroll::new(Flex::row().with_child(playlist_results_widget())).align_left(),
),
)
}

pub fn uniquely_yours() -> impl Widget<AppState> {
Async::new(spinner_widget, uniquely_yours_results_widget, || {Empty})
Async::new(spinner_widget, uniquely_yours_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -119,7 +115,7 @@ pub fn uniquely_yours() -> impl Widget<AppState> {
}

pub fn user_top_mixes() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -136,7 +132,7 @@ pub fn user_top_mixes() -> impl Widget<AppState> {
}

pub fn best_of_artists() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -153,7 +149,7 @@ pub fn best_of_artists() -> impl Widget<AppState> {
}

pub fn your_shows() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -170,7 +166,7 @@ pub fn your_shows() -> impl Widget<AppState> {
}

pub fn jump_back_in() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand All @@ -187,7 +183,7 @@ pub fn jump_back_in() -> impl Widget<AppState> {
}

pub fn shows_that_you_might_like() -> impl Widget<AppState> {
Async::new(spinner_widget, loaded_results_widget, || {Empty})
Async::new(spinner_widget, loaded_results_widget, || Empty)
.lens(
Ctx::make(
AppState::common_ctx,
Expand Down
94 changes: 94 additions & 0 deletions psst-gui/src/ui/lyrics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use druid::widget::{Container, CrossAxisAlignment, Flex, Label, LineBreaking, List, Scroll};
use druid::{Insets, LensExt, Selector, Widget, WidgetExt};

use crate::cmd;
use crate::data::{AppState, Ctx, NowPlaying, Playable, TrackLines};
use crate::widget::MyWidgetExt;
use crate::{webapi::WebApi, widget::Async};

use super::theme;
use super::utils;

pub const SHOW_LYRICS: Selector<NowPlaying> = Selector::new("app.home.show_lyrics");

pub fn lyrics_widget() -> impl Widget<AppState> {
Scroll::new(
Container::new(
Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Center)
.with_default_spacer()
.with_child(track_info_widget())
.with_spacer(theme::grid(2.0))
.with_child(track_lyrics_widget()),
)
.fix_width(400.0)
.center(),
)
.vertical()
}

fn track_info_widget() -> impl Widget<AppState> {
Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
Label::dynamic(|data: &AppState, _| {
data.playback.now_playing.as_ref().map_or_else(
|| "No track playing".to_string(),
|np| match &np.item {
Playable::Track(track) => track.name.clone().to_string(),
_ => "Unknown track".to_string(),
},
)
})
.with_font(theme::UI_FONT_MEDIUM)
.with_text_size(theme::TEXT_SIZE_LARGE),
)
.with_spacer(theme::grid(0.5))
.with_child(
Label::dynamic(|data: &AppState, _| {
data.playback.now_playing.as_ref().map_or_else(
|| "".to_string(),
|np| match &np.item {
Playable::Track(track) => {
format!("{} - {}", track.artist_name(), track.album_name())
}
_ => "".to_string(),
},
)
})
.with_text_size(theme::TEXT_SIZE_SMALL)
.with_text_color(theme::PLACEHOLDER_COLOR),
)
}

fn track_lyrics_widget() -> impl Widget<AppState> {
Async::new(
utils::spinner_widget,
|| {
List::new(|| {
Label::raw()
.with_line_break_mode(LineBreaking::WordWrap)
.lens(Ctx::data().then(TrackLines::words))
.expand_width()
.center()
.padding(Insets::uniform_xy(theme::grid(1.0), theme::grid(0.5)))
.link()
.rounded(theme::BUTTON_BORDER_RADIUS)
.on_left_click(|ctx, _, c, _| {
ctx.submit_command(
cmd::SKIP_TO_POSITION
.with(c.data.start_time_ms.parse::<u64>().unwrap()),
)
})
})
},
|| Label::new("No lyrics found for this track").center(),
)
.lens(Ctx::make(AppState::common_ctx, AppState::lyrics).then(Ctx::in_promise()))
.on_command_async(
SHOW_LYRICS,
|t| WebApi::global().get_lyrics(t.item.id().to_base62()),
|_, data, _| data.lyrics.defer(()),
|_, data, r| data.lyrics.update(((), r.1)),
)
}
Loading