use std::{
fmt::Display,
time::{Duration, Instant},
};
use iced::{
advanced::graphics::core::Element,
alignment::Horizontal,
theme::{Container, Slider, Svg, Text},
widget::{
column as icolumn, component, container, image::Handle, mouse_area, row, slider, svg, text,
Component,
},
Alignment, Background, Color, Length, Renderer, Theme,
};
use crate::{
hass_client::MediaPlayerRepeat,
oracle::{MediaPlayerSpeaker, MediaPlayerSpeakerState},
theme::{
colours::{SKY_500, SLATE_400},
Icon,
},
};
pub fn media_player<M>(device: MediaPlayerSpeaker, album_art: Option<Handle>) -> MediaPlayer<M> {
MediaPlayer {
height: Length::Shrink,
width: Length::Fill,
device,
album_art,
artist_logo: None,
on_volume_change: None,
on_position_change: None,
on_state_change: None,
on_mute_change: None,
on_repeat_change: None,
on_next_track: None,
on_previous_track: None,
on_shuffle_change: None,
on_search: None,
}
}
#[derive(Clone)]
pub struct MediaPlayer<M> {
height: Length,
width: Length,
device: MediaPlayerSpeaker,
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>,
on_mute_change: Option<fn(bool) -> M>,
on_repeat_change: Option<fn(MediaPlayerRepeat) -> M>,
on_next_track: Option<M>,
on_previous_track: Option<M>,
on_shuffle_change: Option<fn(bool) -> M>,
on_search: Option<M>,
}
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
}
pub fn on_search(mut self, m: M) -> Self {
self.on_search = Some(m);
self
}
pub fn on_position_change(mut self, f: fn(Duration) -> M) -> Self {
self.on_position_change = Some(f);
self
}
pub fn on_state_change(mut self, f: fn(bool) -> M) -> Self {
self.on_state_change = Some(f);
self
}
pub fn on_mute_change(mut self, f: fn(bool) -> M) -> Self {
self.on_mute_change = Some(f);
self
}
pub fn on_repeat_change(mut self, f: fn(MediaPlayerRepeat) -> M) -> Self {
self.on_repeat_change = Some(f);
self
}
pub fn on_next_track(mut self, msg: M) -> Self {
self.on_next_track = Some(msg);
self
}
pub fn on_previous_track(mut self, msg: M) -> Self {
self.on_previous_track = Some(msg);
self
}
pub fn on_shuffle_change(mut self, f: fn(bool) -> M) -> Self {
self.on_shuffle_change = Some(f);
self
}
}
impl<M: Clone> Component<M, Renderer> for MediaPlayer<M> {
type State = State;
type Event = Event;
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
match event {
Event::VolumeChange(new) => {
state.overridden_volume = Some(new);
None
}
Event::PositionChange(new) => {
state.overridden_position = Some(Duration::from_secs_f64(new));
None
}
Event::TogglePlaying => self
.on_state_change
.map(|f| f(!self.device.state.is_playing())),
Event::ToggleMute => self.on_mute_change.map(|f| f(!self.device.muted)),
Event::ToggleRepeat => self.on_repeat_change.map(|f| f(self.device.repeat.next())),
Event::OnVolumeRelease => self
.on_volume_change
.zip(state.overridden_volume.take())
.map(|(f, vol)| f(vol)),
Event::OnPositionRelease => self
.on_position_change
.zip(state.overridden_position.take())
.map(|(f, pos)| f(pos)),
Event::PreviousTrack => {
let last_press = state
.last_previous_click
.as_ref()
.map_or(Duration::MAX, Instant::elapsed);
state.last_previous_click = Some(Instant::now());
if last_press > Duration::from_secs(2) {
self.on_position_change.map(|f| f(Duration::ZERO))
} else {
self.on_previous_track.clone()
}
}
Event::NextTrack => self.on_next_track.clone(),
Event::ToggleShuffle => self.on_shuffle_change.map(|f| f(!self.device.shuffle)),
}
}
#[allow(clippy::too_many_lines)]
fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer> {
let icon_style = |v| Svg::Custom(Box::new(if v { Style::Active } else { Style::Inactive }));
let position = state
.overridden_position
.or(self.device.actual_media_position)
.unwrap_or_default();
let volume = state.overridden_volume.unwrap_or(self.device.volume);
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![
container(row![])
.width(Length::FillPortion(8))
.align_x(Horizontal::Left),
container(playback_controls)
.width(Length::FillPortion(20))
.align_x(Horizontal::Center),
container(volume_controls)
.width(Length::FillPortion(8))
.align_x(Horizontal::Right),
]
.spacing(8)
.align_items(Alignment::Center)
.width(Length::Fill),
scrubber,
]
.align_items(Alignment::Center)
.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()
}
}
#[derive(Copy, Clone, Debug, Default)]
pub struct State {
overridden_position: Option<Duration>,
overridden_volume: Option<f32>,
last_previous_click: Option<Instant>,
}
#[derive(Clone)]
pub enum Event {
TogglePlaying,
ToggleMute,
ToggleRepeat,
ToggleShuffle,
VolumeChange(f32),
PositionChange(f64),
OnVolumeRelease,
OnPositionRelease,
PreviousTrack,
NextTrack,
}
impl<'a, M> From<MediaPlayer<M>> for Element<'a, M, Renderer>
where
M: 'a + Clone,
{
fn from(card: MediaPlayer<M>) -> Self {
component(card)
}
}
fn format_time(duration: Duration) -> impl Display {
let secs = duration.as_secs();
let minutes = secs / 60;
let seconds = secs % 60;
format!("{minutes:02}:{seconds:02}")
}
struct SliderStyle;
impl slider::StyleSheet for SliderStyle {
type Style = Theme;
fn active(&self, style: &Self::Style) -> slider::Appearance {
let palette = style.extended_palette();
let handle = slider::Handle {
shape: slider::HandleShape::Rectangle {
width: 0,
border_radius: 0.0.into(),
},
color: Color::TRANSPARENT,
border_color: Color::TRANSPARENT,
border_width: 0.0,
};
slider::Appearance {
rail: slider::Rail {
colors: (palette.primary.base.color, palette.secondary.base.color),
width: 4.0,
border_radius: 4.0.into(),
},
handle,
}
}
fn hovered(&self, style: &Self::Style) -> slider::Appearance {
let palette = style.extended_palette();
let handle = slider::Handle {
shape: slider::HandleShape::Circle { radius: 6.0 },
color: palette.background.base.color,
border_color: palette.primary.base.color,
border_width: 1.0,
};
slider::Appearance {
rail: slider::Rail {
colors: (palette.primary.base.color, palette.secondary.base.color),
width: 4.0,
border_radius: 4.0.into(),
},
handle,
}
}
fn dragging(&self, style: &Self::Style) -> slider::Appearance {
self.hovered(style)
}
}
#[derive(Copy, Clone)]
pub enum Style {
Active,
Inactive,
}
impl container::StyleSheet for Style {
type Style = Theme;
fn appearance(&self, _style: &Self::Style) -> container::Appearance {
container::Appearance {
text_color: None,
background: Some(Background::Color(Color {
a: 0.8,
..Color::BLACK
})),
border_radius: 0.0.into(),
border_width: 0.,
border_color: Color::default(),
}
}
}
impl svg::StyleSheet for Style {
type Style = Theme;
fn appearance(&self, _style: &Self::Style) -> svg::Appearance {
let color = match self {
Self::Active => SKY_500,
Self::Inactive => Color::WHITE,
};
svg::Appearance { color: Some(color) }
}
}