From 7ff01f1471c2da126638a0e0051dfd2725564762 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Wed, 3 Jan 2024 17:04:21 +0000 Subject: [PATCH] Redesign listen view --- .github/screenshots/listen.webp | 4 ++-- Cargo.lock | 1 + shalom/Cargo.toml | 3 ++- shalom/src/config.rs | 6 ++++++ shalom/src/oracle.rs | 1 + shalom/src/pages/omni.rs | 9 ++++++--- shalom/src/pages/room.rs | 42 +++++++++++++++++++++++++++++------------- shalom/src/pages/room/listen.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------- shalom/src/subscriptions.rs | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------- shalom/src/theme.rs | 13 ++++++++++++- shalom/src/widgets/media_player.rs | 262 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------------------------------------------- shalom/src/widgets/track_card.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++--------------------------- 12 files changed, 489 insertions(+), 209 deletions(-) diff --git a/.github/screenshots/listen.webp b/.github/screenshots/listen.webp index 9ddd047..1830207 100644 --- a/.github/screenshots/listen.webp +++ b/.github/screenshots/listen.webp @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:202171412da17ceb5126dde885259b93301fa22a652ad9c1dfb72dbd77fa5d64 -size 56054 +oid sha256:f7a117d25df6a310f93cd836f85622043164d6a24d5204723ae3ed490e492c32 +size 57180 diff --git a/Cargo.lock b/Cargo.lock index 5361944..1738165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2907,6 +2907,7 @@ version = "0.1.0" dependencies = [ "atomic", "bytemuck", + "bytes", "iced", "image", "internment", diff --git a/shalom/Cargo.toml b/shalom/Cargo.toml index 694d855..21b9f54 100644 --- a/shalom/Cargo.toml +++ b/shalom/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] atomic = "0.6" bytemuck = "1.14" +bytes = "1" iced = { version = "0.10", features = ["tokio", "svg", "lazy", "advanced", "image", "canvas"] } image = "0.24" once_cell = "1.18" @@ -17,7 +18,7 @@ itertools = "0.11" keyframe = "1.1" lru = "0.12" palette = "0.7" -reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots"] } +reqwest = { version = "0.11.22", default-features = false, features = ["rustls-tls", "rustls-tls-native-roots", "json"] } serde = { version = "1.0", features = ["derive"] } serde_with = { version = "3.4", features = ["macros"] } serde_json = { version = "1.0", features = ["raw_value"] } diff --git a/shalom/src/config.rs b/shalom/src/config.rs index 520a98a..3fae16e 100644 --- a/shalom/src/config.rs +++ b/shalom/src/config.rs @@ -2,6 +2,12 @@ use serde::Deserialize; +#[allow(dead_code)] +pub const LAST_FM_API_KEY: &str = "732433605ea7893c761d340a05752695"; +#[allow(dead_code)] +pub const LAST_FM_SHARED_SECRET: &str = "420fdb301e6b4a62a888bf51def71670"; +pub const FANART_PROJECT_KEY: &str = "df5eb171c6e0e49122ad59830cdf789f"; + #[derive(Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Config { diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs index 64290df..8cb4f80 100644 --- a/shalom/src/oracle.rs +++ b/shalom/src/oracle.rs @@ -269,6 +269,7 @@ impl Oracle { StateAttributes::MediaPlayer(attrs) => { let entity_id = Intern::::from(state_changed.entity_id.as_ref()).as_ref(); + eprintln!("{entity_id} updated"); let new_state = MediaPlayer::new( attrs, &state_changed.new_state.state, diff --git a/shalom/src/pages/omni.rs b/shalom/src/pages/omni.rs index e3fa596..90ac268 100644 --- a/shalom/src/pages/omni.rs +++ b/shalom/src/pages/omni.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, collections::BTreeMap, sync::Arc}; +use std::{any::TypeId, collections::BTreeMap, convert::identity, sync::Arc}; use iced::{ advanced::graphics::core::Element, @@ -170,8 +170,11 @@ impl Omni { let camera_image_downloads = Subscription::batch(self.cameras.iter().filter_map(|(k, v)| { if let CameraImage::Unresolved(url, _) = v { - Some(download_image(*k, url.clone(), |id, url, handle| { - Message::CameraImageDownloaded(id, url, handle) + let k = *k; + let url = url.clone(); + + Some(download_image(url.clone(), identity, move |handle| { + Message::CameraImageDownloaded(k, url, handle) })) } else { None diff --git a/shalom/src/pages/room.rs b/shalom/src/pages/room.rs index 233dd18..e7d04c7 100644 --- a/shalom/src/pages/room.rs +++ b/shalom/src/pages/room.rs @@ -7,12 +7,13 @@ use iced::{ advanced::graphics::core::Element, font::{Stretch, Weight}, theme, - widget::{row, text, Column}, + widget::{container, row, text, Column}, Color, Font, Length, Renderer, Subscription, }; use crate::{ oracle::Oracle, + subscriptions::MaybePendingImage, widgets::{ image_background::image_background, room_navigation::{Page, RoomNavigation}, @@ -58,16 +59,19 @@ impl Room { } pub fn view(&self) -> Element<'_, Message, Renderer> { - let header = text(self.room.name.as_ref()) - .size(60) - .font(Font { - weight: Weight::Bold, - stretch: Stretch::Condensed, - ..Font::with_name("Helvetica Neue") - }) - .style(theme::Text::Color(Color::WHITE)); + let header = container( + text(self.room.name.as_ref()) + .size(60) + .font(Font { + weight: Weight::Bold, + stretch: Stretch::Condensed, + ..Font::with_name("Helvetica Neue") + }) + .style(theme::Text::Color(Color::WHITE)), + ) + .padding([40, 40, 0, 40]); - let mut col = Column::new().spacing(20).padding(40).push(header); + let mut col = Column::new().spacing(20).push(header); col = col.push(match self.current_page { Page::Climate => Element::from(row![]), @@ -75,14 +79,26 @@ impl Room { Page::Lights => self.lights.view().map(Message::Lights), }); + let background = match self.current_page { + Page::Listen => self + .listen + .background + .as_ref() + .and_then(MaybePendingImage::handle), + _ => None, + }; + row![ RoomNavigation::new(self.current_page) .width(Length::FillPortion(2)) .on_change(Message::ChangePage) .on_exit(Message::Exit), - image_background(crate::theme::Image::Sunset, col.width(Length::Fill).into()) - .width(Length::FillPortion(15)) - .height(Length::Fill), + image_background( + background.unwrap_or_else(|| crate::theme::Image::Sunset.into()), + col.width(Length::Fill).into() + ) + .width(Length::FillPortion(15)) + .height(Length::Fill), ] .height(Length::Fill) .width(Length::Fill) diff --git a/shalom/src/pages/room/listen.rs b/shalom/src/pages/room/listen.rs index 981cca9..0186a9b 100644 --- a/shalom/src/pages/room/listen.rs +++ b/shalom/src/pages/room/listen.rs @@ -1,4 +1,4 @@ -use std::{sync::Arc, time::Duration}; +use std::{convert::identity, sync::Arc, time::Duration}; use iced::{ futures::StreamExt, @@ -6,12 +6,14 @@ use iced::{ widget::{container, image::Handle, Column}, Element, Renderer, Subscription, }; +use image::imageops::blur; use url::Url; use crate::{ hass_client::MediaPlayerRepeat, oracle::{MediaPlayerSpeaker, MediaPlayerSpeakerState, Oracle, Room}, - subscriptions::download_image, + subscriptions::{download_image, find_fanart_urls, find_musicbrainz_artist, MaybePendingImage}, + theme::darken_image, widgets, }; @@ -20,7 +22,10 @@ pub struct Listen { room: Room, oracle: Arc, speaker: Option<(&'static str, MediaPlayerSpeaker)>, - now_playing_image: Option, + album_art_image: Option, + musicbrainz_artist_id: Option, + pub background: Option, + artist_logo: Option, } impl Listen { @@ -31,22 +36,27 @@ impl Listen { room: room.clone(), speaker, oracle, - now_playing_image: None, + album_art_image: None, + musicbrainz_artist_id: None, + background: None, + artist_logo: None, } } pub fn update(&mut self, event: Message) -> Option { match event { - Message::NowPlayingImageLoaded(url, handle) => { - if self - .speaker - .as_ref() - .and_then(|(_, v)| v.entity_picture.as_ref()) - == Some(&url) - { - self.now_playing_image = Some(handle); - } - + Message::AlbumArtImageLoaded(handle) => { + self.album_art_image = Some(handle); + None + } + Message::FanArtLoaded(logo, background) => { + self.background = background.map(MaybePendingImage::Loading); + self.artist_logo = logo.map(MaybePendingImage::Loading); + None + } + Message::MusicbrainzArtistLoaded(v) => { + eprintln!("musicbrainz artist {v}"); + self.musicbrainz_artist_id = Some(v); None } Message::UpdateSpeaker => { @@ -61,7 +71,21 @@ impl Listen { .as_ref() .and_then(|(_, v)| v.entity_picture.as_ref()) { - self.now_playing_image = None; + self.album_art_image = None; + self.artist_logo = None; + self.background = None; + } + + if self + .speaker + .as_ref() + .and_then(|(_, v)| v.media_artist.as_ref()) + != new + .as_ref() + .as_ref() + .and_then(|(_, v)| v.media_artist.as_ref()) + { + self.musicbrainz_artist_id = None; } self.speaker = new; @@ -106,15 +130,26 @@ impl Listen { speaker.shuffle = new; Some(Event::SetSpeakerShuffle(id, new)) } + Message::BackgroundDownloaded(handle) => { + self.background = Some(MaybePendingImage::Downloaded(handle)); + None + } + Message::ArtistLogoDownloaded(handle) => { + self.artist_logo = Some(MaybePendingImage::Downloaded(handle)); + None + } } } pub fn view(&self) -> Element<'_, Message, Renderer> { - let mut col = Column::new(); - if let Some((_, speaker)) = self.speaker.clone() { - col = col.push(container( - widgets::media_player::media_player(speaker, self.now_playing_image.clone()) + container( + widgets::media_player::media_player(speaker, self.album_art_image.clone()) + .with_artist_logo( + self.artist_logo + .as_ref() + .and_then(MaybePendingImage::handle), + ) .on_volume_change(Message::OnSpeakerVolumeChange) .on_mute_change(Message::OnSpeakerMuteChange) .on_repeat_change(Message::OnSpeakerRepeatChange) @@ -123,22 +158,59 @@ impl Listen { .on_next_track(Message::OnSpeakerNextTrack) .on_previous_track(Message::OnSpeakerPreviousTrack) .on_shuffle_change(Message::OnSpeakerShuffleChange), - )); + ) + .into() + } else { + Column::new().into() } - - col.into() } pub fn subscription(&self) -> Subscription { - let image_subscription = if let (Some(uri), None) = ( + let album_art_subscription = if let (Some(uri), None) = ( self.speaker .as_ref() .and_then(|(_, v)| v.entity_picture.as_ref()), - &self.now_playing_image, + &self.album_art_image, + ) { + download_image(uri.clone(), identity, Message::AlbumArtImageLoaded) + } else { + Subscription::none() + }; + + let musicbrainz_artist_id_subscription = if let (Some(artist), None) = ( + self.speaker + .as_ref() + .and_then(|(_, v)| v.media_artist.as_ref()), + &self.musicbrainz_artist_id, + ) { + find_musicbrainz_artist(artist.to_string(), Message::MusicbrainzArtistLoaded) + } else { + Subscription::none() + }; + + let fanart_subscription = if let (None, None, Some(musicbrainz_id)) = ( + &self.background, + &self.artist_logo, + &self.musicbrainz_artist_id, ) { - download_image("now-playing", uri.clone(), |_, url, handle| { - Message::NowPlayingImageLoaded(url, handle) - }) + find_fanart_urls(musicbrainz_id.clone(), Message::FanArtLoaded) + } else { + Subscription::none() + }; + + let background_subscription = + if let Some(MaybePendingImage::Loading(url)) = &self.background { + download_image( + url.clone(), + |image| blur(&darken_image(image, 0.3), 5.0), + Message::BackgroundDownloaded, + ) + } else { + Subscription::none() + }; + + let logo_subscription = if let Some(MaybePendingImage::Loading(url)) = &self.artist_logo { + download_image(url.clone(), identity, Message::ArtistLogoDownloaded) } else { Subscription::none() }; @@ -155,7 +227,14 @@ impl Listen { Subscription::none() }; - Subscription::batch([image_subscription, speaker_subscription]) + Subscription::batch([ + album_art_subscription, + speaker_subscription, + musicbrainz_artist_id_subscription, + background_subscription, + logo_subscription, + fanart_subscription, + ]) } } @@ -173,7 +252,11 @@ pub enum Event { #[derive(Clone, Debug)] pub enum Message { - NowPlayingImageLoaded(Url, Handle), + AlbumArtImageLoaded(Handle), + BackgroundDownloaded(Handle), + ArtistLogoDownloaded(Handle), + MusicbrainzArtistLoaded(String), + FanArtLoaded(Option, Option), UpdateSpeaker, OnSpeakerVolumeChange(f32), OnSpeakerPositionChange(Duration), diff --git a/shalom/src/subscriptions.rs b/shalom/src/subscriptions.rs index 36aef78..3a6c05a 100644 --- a/shalom/src/subscriptions.rs +++ b/shalom/src/subscriptions.rs @@ -1,24 +1,44 @@ -use std::{hash::Hash, num::NonZeroUsize}; +use std::num::NonZeroUsize; +use ::image::GenericImageView; use iced::{futures::stream, subscription, widget::image, Subscription}; use lru::LruCache; use once_cell::sync::Lazy; use parking_lot::Mutex; use url::Url; -pub fn download_image( - id: I, +use crate::config::FANART_PROJECT_KEY; + +#[derive(Debug)] +pub enum MaybePendingImage { + Downloaded(image::Handle), + Loading(Url), +} + +impl MaybePendingImage { + pub fn handle(&self) -> Option { + match self { + Self::Downloaded(h) => Some(h.clone()), + Self::Loading(_) => None, + } + } +} + +pub fn download_image( url: Url, - resp: fn(I, Url, image::Handle) -> M, + post_process: fn(::image::RgbaImage) -> ::image::RgbaImage, + resp: impl FnOnce(image::Handle) -> M + Send + 'static, ) -> Subscription { static CACHE: Lazy>> = Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap()))); subscription::run_with_id( - id, + url.to_string(), stream::once(async move { + eprintln!("{url} dl"); + if let Some(handle) = CACHE.lock().get(&url) { - return (resp)(id, url, handle.clone()); + return (resp)(handle.clone()); } let bytes = reqwest::get(url.clone()) @@ -27,11 +47,116 @@ pub fn download_image( .bytes() .await .unwrap(); - let handle = image::Handle::from_memory(bytes); + + let handle = tokio::task::spawn_blocking(move || { + eprintln!("parsing image"); + let img = ::image::load_from_memory(&bytes).unwrap(); + let (h, w) = img.dimensions(); + eprintln!("post processing"); + let data = post_process(img.into_rgba8()).into_raw(); + image::Handle::from_pixels(h, w, data) + }) + .await + .unwrap(); CACHE.lock().push(url.clone(), handle.clone()); - (resp)(id, url, handle) + (resp)(handle) + }), + ) +} + +pub fn find_musicbrainz_artist( + artist: String, + to_msg: fn(String) -> M, +) -> Subscription { + static CACHE: Lazy>> = + Lazy::new(|| Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap()))); + + subscription::run_with_id( + format!("musicbrainz-{artist}"), + stream::once(async move { + eprintln!("musicbrainz req"); + + if let Some(handle) = CACHE.lock().get(&artist) { + return (to_msg)(handle.to_string()); + } + + // TODO + let client = reqwest::Client::builder() + .user_agent(format!( + "{}/{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .build() + .unwrap(); + + let resp: serde_json::Value = client + .get(format!( + "https://musicbrainz.org/ws/2/artist/?query={artist}&fmt=json", + )) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); + + let id = resp + .get("artists") + .unwrap() + .get(0) + .unwrap() + .get("id") + .unwrap() + .as_str() + .unwrap() + .to_string(); + + CACHE.lock().push(artist, id.clone()); + + // TODO: typing + (to_msg)(id) + }), + ) +} + +pub fn find_fanart_urls( + musicbrainz_id: String, + to_msg: fn(Option, Option) -> M, +) -> Subscription { + subscription::run_with_id( + format!("fanart-{musicbrainz_id}"), + stream::once(async move { + eprintln!("fanart req"); + + let resp: serde_json::Value = reqwest::get(format!("http://webservice.fanart.tv/v3/music/{musicbrainz_id}?api_key={FANART_PROJECT_KEY}")) + .await + .unwrap() + .json() + .await + .unwrap(); + + // TODO: typing + let logo = resp + .get("hdmusiclogo") + .and_then(|v| v.get(0)) + .and_then(|v| v.get("url")) + .and_then(|v| v.as_str()) + .map(Url::parse) + .transpose() + .unwrap(); + let background = resp + .get("artistbackground") + .and_then(|v| v.get(0)) + .and_then(|v| v.get("url")) + .and_then(|v| v.as_str()) + .map(Url::parse) + .transpose() + .unwrap(); + + (to_msg)(logo, background) }), ) } diff --git a/shalom/src/theme.rs b/shalom/src/theme.rs index e2d00f1..06640be 100644 --- a/shalom/src/theme.rs +++ b/shalom/src/theme.rs @@ -1,4 +1,4 @@ -use ::image::GenericImageView; +use ::image::{GenericImageView, Pixel, RgbaImage}; use iced::{ advanced::svg::Handle, widget::{image, svg}, @@ -160,3 +160,14 @@ impl From for image::Handle { value.handle() } } + +pub fn darken_image(mut img: RgbaImage, factor: f32) -> RgbaImage { + for px in img.pixels_mut() { + #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] + px.apply_without_alpha(|v| (f32::from(v) * (1.0 - factor)).min(255.0) as u8); + } + + eprintln!("darkened"); + + img +} diff --git a/shalom/src/widgets/media_player.rs b/shalom/src/widgets/media_player.rs index f11189f..0b43774 100644 --- a/shalom/src/widgets/media_player.rs +++ b/shalom/src/widgets/media_player.rs @@ -5,6 +5,7 @@ use std::{ use iced::{ advanced::graphics::core::Element, + alignment::Horizontal, theme::{Container, Slider, Svg, Text}, widget::{ column as icolumn, component, container, image::Handle, row, slider, svg, text, Component, @@ -22,12 +23,13 @@ use crate::{ widgets::mouse_area::mouse_area, }; -pub fn media_player(device: MediaPlayerSpeaker, image: Option) -> MediaPlayer { +pub fn media_player(device: MediaPlayerSpeaker, album_art: Option) -> MediaPlayer { MediaPlayer { height: Length::Shrink, width: Length::Fill, device, - image, + album_art, + artist_logo: None, on_volume_change: None, on_position_change: None, on_state_change: None, @@ -44,7 +46,8 @@ pub struct MediaPlayer { height: Length, width: Length, device: MediaPlayerSpeaker, - image: Option, + album_art: Option, + artist_logo: Option, on_volume_change: Option M>, on_position_change: Option M>, on_state_change: Option M>, @@ -56,6 +59,11 @@ pub struct MediaPlayer { } impl MediaPlayer { + pub fn with_artist_logo(mut self, logo: Option) -> Self { + self.artist_logo = logo; + self + } + pub fn on_volume_change(mut self, f: fn(f32) -> M) -> Self { self.on_volume_change = Some(f); self @@ -153,132 +161,136 @@ impl Component for MediaPlayer { let volume = state.overridden_volume.unwrap_or(self.device.volume); - container( - row![ - container(crate::widgets::track_card::track_card( - self.device - .media_artist - .as_ref() - .map(ToString::to_string) - .unwrap_or_default(), - self.device - .media_title - .as_ref() - .map(ToString::to_string) - .unwrap_or_default(), - self.image.clone(), - ),) - .width(Length::FillPortion(8)), + let track_card = crate::widgets::track_card::track_card( + self.device.media_artist.as_deref().unwrap_or_default(), + self.device.media_title.as_deref().unwrap_or_default(), + self.album_art.clone(), + self.artist_logo.clone(), + ); + + let playback_controls = row![ + mouse_area( + svg(Icon::Shuffle) + .height(24) + .width(24) + .style(icon_style(self.device.shuffle)), + ) + .on_press(Event::ToggleShuffle), + mouse_area( + svg(Icon::Backward) + .height(28) + .width(28) + .style(icon_style(false)) + ) + .on_press(Event::PreviousTrack), + mouse_area( + svg(if self.device.state == MediaPlayerSpeakerState::Playing { + Icon::Pause + } else { + Icon::Play + }) + .height(42) + .width(42) + .style(icon_style(false)) + ) + .on_press(Event::TogglePlaying), + mouse_area( + svg(Icon::Forward) + .height(28) + .width(28) + .style(icon_style(false)) + ) + .on_press(Event::NextTrack), + mouse_area( + svg(match self.device.repeat { + MediaPlayerRepeat::Off | MediaPlayerRepeat::All => Icon::Repeat, + MediaPlayerRepeat::One => Icon::Repeat1, + }) + .height(28) + .width(28) + .style(icon_style(self.device.repeat != MediaPlayerRepeat::Off)), + ) + .on_press(Event::ToggleRepeat), + ] + .spacing(14) + .align_items(Alignment::Center); + + let volume_controls = row![ + mouse_area( + svg(if self.device.muted { + Icon::SpeakerMuted + } else { + Icon::Speaker + }) + .height(16) + .width(16) + .style(icon_style(false)), + ) + .on_press(Event::ToggleMute), + slider(0.0..=1.0, volume, Event::VolumeChange) + .width(128) + .step(0.01) + .on_release(Event::OnVolumeRelease) + .style(Slider::Custom(Box::new(SliderStyle))), + ] + .spacing(12) + .align_items(Alignment::Center); + + let scrubber = row![ + text(format_time(position)) + .style(Text::Color(SLATE_400)) + .size(12) + .width(Length::FillPortion(10)), + slider( + 0.0..=self.device.media_duration.unwrap_or_default().as_secs_f64(), + position.as_secs_f64(), + Event::PositionChange + ) + .on_release(Event::OnPositionRelease) + .style(Slider::Custom(Box::new(SliderStyle))) + .width(Length::FillPortion(80)), + text(format_time(self.device.media_duration.unwrap_or_default())) + .style(Text::Color(SLATE_400)) + .size(12) + .width(Length::FillPortion(10)) + .horizontal_alignment(iced::alignment::Horizontal::Right), + ] + .spacing(14) + .align_items(Alignment::Center); + + icolumn![ + container(track_card) + .width(Length::Fill) + .height(Length::Fill) + .padding([0, 40, 0, 40]) + .center_y(), + container( icolumn![ - // container( - // svg(Icon::Hamburger) - // .height(30) - // .width(30), - // ) - // .align_x(Horizontal::Right) - // .align_y(Vertical::Center) - // .width(Length::Fill), row![ - mouse_area( - svg(Icon::Shuffle) - .height(24) - .width(24) - .style(icon_style(self.device.shuffle)), - ) - .on_press(Event::ToggleShuffle), - mouse_area( - svg(Icon::Backward) - .height(28) - .width(28) - .style(icon_style(false)) - ) - .on_press(Event::PreviousTrack), - mouse_area( - svg(if self.device.state == MediaPlayerSpeakerState::Playing { - Icon::Pause - } else { - Icon::Play - }) - .height(42) - .width(42) - .style(icon_style(false)) - ) - .on_press(Event::TogglePlaying), - mouse_area( - svg(Icon::Forward) - .height(28) - .width(28) - .style(icon_style(false)) - ) - .on_press(Event::NextTrack), - mouse_area( - svg(match self.device.repeat { - MediaPlayerRepeat::Off | MediaPlayerRepeat::All => Icon::Repeat, - MediaPlayerRepeat::One => Icon::Repeat1, - }) - .height(28) - .width(28) - .style(icon_style(self.device.repeat != MediaPlayerRepeat::Off)), - ) - .on_press(Event::ToggleRepeat), + row![].width(Length::FillPortion(8)), + container(playback_controls) + .width(Length::FillPortion(20)) + .align_x(Horizontal::Center), + container(volume_controls) + .width(Length::FillPortion(8)) + .align_x(Horizontal::Right), ] - .spacing(14) - .align_items(Alignment::Center), - row![ - text(format_time(position)) - .style(Text::Color(SLATE_400)) - .size(12) - .width(Length::FillPortion(10)), - slider( - 0.0..=self.device.media_duration.unwrap_or_default().as_secs_f64(), - position.as_secs_f64(), - Event::PositionChange - ) - .on_release(Event::OnPositionRelease) - .style(Slider::Custom(Box::new(SliderStyle))) - .width(Length::FillPortion(80)), - text(format_time(self.device.media_duration.unwrap_or_default())) - .style(Text::Color(SLATE_400)) - .size(12) - .width(Length::FillPortion(10)), - ] - .spacing(14) - .align_items(Alignment::Center), - ] - .spacing(8) - .align_items(Alignment::Center) - .width(Length::FillPortion(12)), - row![ - mouse_area( - svg(if self.device.muted { - Icon::SpeakerMuted - } else { - Icon::Speaker - }) - .height(16) - .width(16) - .style(icon_style(false)), - ) - .on_press(Event::ToggleMute), - slider(0.0..=1.0, volume, Event::VolumeChange) - .width(128) - .step(0.01) - .on_release(Event::OnVolumeRelease) - .style(Slider::Custom(Box::new(SliderStyle))), + .spacing(8) + .align_items(Alignment::Center) + .width(Length::Fill), + scrubber, ] .align_items(Alignment::Center) - .width(Length::FillPortion(4)) - .spacing(12), - ] - .align_items(Alignment::Center) - .spacing(48), - ) - .height(self.height) - .width(self.width) - .center_x() - .center_y() - .style(Container::Custom(Box::new(Style::Inactive))) - .padding(20) + .spacing(24), + ) + .height(self.height) + .width(self.width) + .center_x() + .center_y() + .style(Container::Custom(Box::new(Style::Inactive))) + .padding([20, 40, 20, 40]) + ] + .spacing(30) .into() } } @@ -390,7 +402,7 @@ impl container::StyleSheet for Style { a: 0.8, ..Color::BLACK })), - border_radius: 10.0.into(), + border_radius: 0.0.into(), border_width: 0., border_color: Color::default(), } diff --git a/shalom/src/widgets/track_card.rs b/shalom/src/widgets/track_card.rs index d186b97..746b2d4 100644 --- a/shalom/src/widgets/track_card.rs +++ b/shalom/src/widgets/track_card.rs @@ -1,21 +1,28 @@ use iced::{ advanced::graphics::core::Element, + font::{Stretch, Weight}, theme::Text, widget::{ column as icolumn, component, container, image::{self, Image}, - row, text, vertical_space, Component, + text, vertical_space, Component, }, - Alignment, Background, Color, Renderer, Theme, + Background, Color, Font, Renderer, Theme, }; -use crate::theme::colours::{SLATE_200, SLATE_400}; +use crate::theme::colours::SLATE_200; -pub fn track_card(artist: String, song: String, image: Option) -> TrackCard { +pub fn track_card( + artist: &str, + song: &str, + image: Option, + artist_logo: Option, +) -> TrackCard { TrackCard { - artist, - song, + artist: artist.to_uppercase(), + song: format!("\"{}\"", song.to_uppercase()), image, + artist_logo, } } @@ -23,6 +30,7 @@ pub struct TrackCard { artist: String, song: String, image: Option, + artist_logo: Option, } impl Component for TrackCard { @@ -34,28 +42,41 @@ impl Component for TrackCard { } fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> { - let image = - if let Some(handle) = self.image.clone() { - Element::from(Image::new(handle).width(64).height(64)) - } else { - Element::from(container(vertical_space(0)).width(64).height(64).style( - |_t: &Theme| container::Appearance { - background: Some(Background::Color(SLATE_200)), - ..container::Appearance::default() - }, - )) - }; + let image = if let Some(handle) = self.image.clone() { + Element::from(Image::new(handle).width(192).height(192)) + } else { + Element::from(container(vertical_space(0)).width(192).height(192).style( + |_t: &Theme| container::Appearance { + background: Some(Background::Color(SLATE_200)), + ..container::Appearance::default() + }, + )) + }; + + let artist = if let Some(handle) = self.artist_logo.clone() { + Element::from(Image::new(handle).height(64)) + } else { + Element::from( + text(&self.artist) + .size(49) + .style(Text::Color(Color::WHITE)) + .font(Font { + weight: Weight::Bold, + stretch: Stretch::Condensed, + ..Font::with_name("Helvetica Neue") + }), + ) + }; + + let song = text(&self.song) + .size(24) + .style(Text::Color(Color::WHITE)) + .font(Font { + weight: Weight::Medium, + ..Font::with_name("Helvetica Neue") + }); - row![ - image, - icolumn![ - text(&self.song).size(14).style(Text::Color(Color::WHITE)), - text(&self.artist).style(Text::Color(SLATE_400)).size(14) - ] - ] - .align_items(Alignment::Center) - .spacing(10) - .into() + icolumn![icolumn![image, artist,].spacing(5), song,].into() } } -- libgit2 1.7.2