Redesign listen view
Diff
Cargo.lock | 1 +
shalom/Cargo.toml | 3 ++-
.github/screenshots/listen.webp | 4 ++--
shalom/src/config.rs | 6 ++++++
shalom/src/oracle.rs | 1 +
shalom/src/subscriptions.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
shalom/src/theme.rs | 13 ++++++++++++-
shalom/src/pages/omni.rs | 9 ++++++---
shalom/src/pages/room.rs | 44 +++++++++++++++++++++++++++++++-------------
shalom/src/widgets/media_player.rs | 262 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
shalom/src/widgets/track_card.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
shalom/src/pages/room/listen.rs | 141 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
12 files changed, 491 insertions(+), 211 deletions(-)
@@ -2907,6 +2907,7 @@
dependencies = [
"atomic",
"bytemuck",
"bytes",
"iced",
"image",
"internment",
@@ -8,6 +8,7 @@
[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 @@
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"] }
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:202171412da17ceb5126dde885259b93301fa22a652ad9c1dfb72dbd77fa5d64
size 56054
oid sha256:f7a117d25df6a310f93cd836f85622043164d6a24d5204723ae3ed490e492c32
size 57180
@@ -1,7 +1,13 @@
#![allow(clippy::module_name_repetitions)]
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 {
@@ -269,6 +269,7 @@
StateAttributes::MediaPlayer(attrs) => {
let entity_id =
Intern::<str>::from(state_changed.entity_id.as_ref()).as_ref();
eprintln!("{entity_id} updated");
let new_state = MediaPlayer::new(
attrs,
&state_changed.new_state.state,
@@ -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<I: Hash + Copy + Send + 'static, M: 'static>(
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<image::Handle> {
match self {
Self::Downloaded(h) => Some(h.clone()),
Self::Loading(_) => None,
}
}
}
pub fn download_image<M: 'static>(
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<M> {
static CACHE: Lazy<Mutex<LruCache<Url, image::Handle>>> =
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 @@
.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)(handle)
}),
)
}
pub fn find_musicbrainz_artist<M: 'static>(
artist: String,
to_msg: fn(String) -> M,
) -> Subscription<M> {
static CACHE: Lazy<Mutex<LruCache<String, String>>> =
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());
}
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());
(to_msg)(id)
}),
)
}
pub fn find_fanart_urls<M: 'static>(
musicbrainz_id: String,
to_msg: fn(Option<Url>, Option<Url>) -> M,
) -> Subscription<M> {
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();
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();
(resp)(id, url, handle)
(to_msg)(logo, background)
}),
)
}
@@ -1,4 +1,4 @@
use ::image::GenericImageView;
use ::image::{GenericImageView, Pixel, RgbaImage};
use iced::{
advanced::svg::Handle,
widget::{image, svg},
@@ -159,4 +159,15 @@
fn from(value: Image) -> Self {
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
}
@@ -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 @@
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
@@ -7,12 +7,13 @@
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 @@
}
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 mut col = Column::new().spacing(20).padding(40).push(header);
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).push(header);
col = col.push(match self.current_page {
Page::Climate => Element::from(row![]),
@@ -75,14 +79,26 @@
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)
@@ -5,6 +5,7 @@
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 @@
widgets::mouse_area::mouse_area,
};
pub fn media_player<M>(device: MediaPlayerSpeaker, image: Option<Handle>) -> MediaPlayer<M> {
pub fn media_player<M>(device: MediaPlayerSpeaker, album_art: Option<Handle>) -> MediaPlayer<M> {
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 @@
height: Length,
width: Length,
device: MediaPlayerSpeaker,
image: Option<Handle>,
album_art: Option<Handle>,
artist_logo: Option<Handle>,
on_volume_change: Option<fn(f32) -> M>,
on_position_change: Option<fn(Duration) -> M>,
on_state_change: Option<fn(bool) -> M>,
@@ -56,6 +59,11 @@
}
impl<M> MediaPlayer<M> {
pub fn with_artist_logo(mut self, logo: Option<Handle>) -> 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 @@
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![
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),
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)),
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),
]
.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 @@
a: 0.8,
..Color::BLACK
})),
border_radius: 10.0.into(),
border_radius: 0.0.into(),
border_width: 0.,
border_color: Color::default(),
}
@@ -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<image::Handle>) -> TrackCard {
pub fn track_card(
artist: &str,
song: &str,
image: Option<image::Handle>,
artist_logo: Option<image::Handle>,
) -> TrackCard {
TrackCard {
artist,
song,
artist: artist.to_uppercase(),
song: format!("\"{}\"", song.to_uppercase()),
image,
artist_logo,
}
}
@@ -23,6 +30,7 @@
artist: String,
song: String,
image: Option<image::Handle>,
artist_logo: Option<image::Handle>,
}
impl<M> Component<M, Renderer> for TrackCard {
@@ -34,28 +42,41 @@
}
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()
},
))
};
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()
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")
});
icolumn![icolumn![image, artist,].spacing(5), song,].into()
}
}
@@ -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 @@
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 @@
room: Room,
oracle: Arc<Oracle>,
speaker: Option<(&'static str, MediaPlayerSpeaker)>,
now_playing_image: Option<Handle>,
album_art_image: Option<Handle>,
musicbrainz_artist_id: Option<String>,
pub background: Option<MaybePendingImage>,
artist_logo: Option<MaybePendingImage>,
}
impl Listen {
@@ -31,22 +36,27 @@
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<Event> {
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,9 +71,23 @@
.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;
None
@@ -105,16 +129,27 @@
let (id, speaker) = self.speaker.as_mut()?;
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 @@
.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<Message> {
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 @@
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 @@
#[derive(Clone, Debug)]
pub enum Message {
NowPlayingImageLoaded(Url, Handle),
AlbumArtImageLoaded(Handle),
BackgroundDownloaded(Handle),
ArtistLogoDownloaded(Handle),
MusicbrainzArtistLoaded(String),
FanArtLoaded(Option<Url>, Option<Url>),
UpdateSpeaker,
OnSpeakerVolumeChange(f32),
OnSpeakerPositionChange(Duration),