🏡 index : ~doyle/shalom.git

author Jordan Doyle <jordan@doyle.la> 2024-01-03 2:35:07.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2024-01-03 2:35:07.0 +00:00:00
commit
4a61bfcde4ab374d28da2ed77c0bf9bb5449105f [patch]
tree
02340c0eec872f8ee35adde0c35323bd1b638e2b
parent
7d8e5658665be613d0a29e9cfec09cc758c389f9
download
4a61bfcde4ab374d28da2ed77c0bf9bb5449105f.tar.gz

Split subviews from room view



Diff

 .gitignore                      |   1 +-
 shalom/src/main.rs              | 210 +++++++++++++++++++----------------
 shalom/src/pages/room.rs        | 246 ++++-------------------------------------
 shalom/src/pages/room/lights.rs | 103 +++++++++++++++++-
 shalom/src/pages/room/listen.rs | 186 +++++++++++++++++++++++++++++++-
 5 files changed, 429 insertions(+), 317 deletions(-)

diff --git a/.gitignore b/.gitignore
index fabfb87..0b3ec0b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/target
/config.toml
/.vscode
diff --git a/shalom/src/main.rs b/shalom/src/main.rs
index ef847f7..c826a2f 100644
--- a/shalom/src/main.rs
+++ b/shalom/src/main.rs
@@ -45,6 +45,120 @@ impl Shalom {
            self.oracle.as_ref().unwrap().clone(),
        ))
    }

    fn handle_room_event(&mut self, e: pages::room::Message) -> Command<Message> {
        let ActivePage::Room(r) = &mut self.page else {
            return Command::none();
        };

        match r.update(e) {
            Some(pages::room::Event::Lights(e)) => self.handle_light_event(e),
            Some(pages::room::Event::Listen(e)) => self.handle_listen_event(e),
            Some(pages::room::Event::Exit) => {
                self.page = self.build_omni_route();
                Command::none()
            }
            None => Command::none(),
        }
    }

    fn handle_light_event(&mut self, event: pages::room::lights::Event) -> Command<Message> {
        match event {
            pages::room::lights::Event::SetLightState(id, state) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.set_light_state(id, state).await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::lights::Event::OpenLightContextMenu(id) => {
                if let Some(light) = self.oracle.as_ref().and_then(|o| o.fetch_light(id)) {
                    self.context_menu = Some(ActiveContextMenu::LightControl(
                        context_menus::light_control::LightControl::new(id, light),
                    ));
                }

                Command::none()
            }
        }
    }

    fn handle_listen_event(&self, event: pages::room::listen::Event) -> Command<Message> {
        match event {
            pages::room::listen::Event::SetSpeakerVolume(id, new) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).set_volume(new).await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SetSpeakerPosition(id, new) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).seek(new).await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SetSpeakerPlaying(id, new) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move {
                        let speaker = oracle.speaker(id);
                        if new {
                            speaker.play().await;
                        } else {
                            speaker.pause().await;
                        }
                    },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SetSpeakerMuted(id, new) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).set_mute(new).await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SetSpeakerRepeat(id, new) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).set_repeat(new).await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SpeakerNextTrack(id) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).next().await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SpeakerPreviousTrack(id) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).previous().await },
                    Message::UpdateLightResult,
                )
            }
            pages::room::listen::Event::SetSpeakerShuffle(id, new) => {
                let oracle = self.oracle.as_ref().unwrap().clone();

                Command::perform(
                    async move { oracle.speaker(id).set_shuffle(new).await },
                    Message::UpdateLightResult,
                )
            }
        }
    }
}

impl Application for Shalom {
@@ -109,101 +223,7 @@ impl Application for Shalom {
                }
                None => Command::none(),
            },
            (Message::RoomEvent(e), ActivePage::Room(r), _) => match r.update(e) {
                Some(pages::room::Event::OpenLightContextMenu(id)) => {
                    if let Some(light) = self.oracle.as_ref().and_then(|o| o.fetch_light(id)) {
                        self.context_menu = Some(ActiveContextMenu::LightControl(
                            context_menus::light_control::LightControl::new(id, light),
                        ));
                    }

                    Command::none()
                }
                Some(pages::room::Event::SetLightState(id, state)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.set_light_state(id, state).await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SetSpeakerVolume(id, new)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).set_volume(new).await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SetSpeakerPosition(id, new)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).seek(new).await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SetSpeakerPlaying(id, new)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move {
                            let speaker = oracle.speaker(id);
                            if new {
                                speaker.play().await;
                            } else {
                                speaker.pause().await;
                            }
                        },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SetSpeakerMuted(id, new)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).set_mute(new).await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SetSpeakerRepeat(id, new)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).set_repeat(new).await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SpeakerNextTrack(id)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).next().await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SpeakerPreviousTrack(id)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).previous().await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::SetSpeakerShuffle(id, new)) => {
                    let oracle = self.oracle.as_ref().unwrap().clone();

                    Command::perform(
                        async move { oracle.speaker(id).set_shuffle(new).await },
                        Message::UpdateLightResult,
                    )
                }
                Some(pages::room::Event::Exit) => {
                    self.page = self.build_omni_route();
                    Command::none()
                }
                None => Command::none(),
            },
            (Message::RoomEvent(e), _, _) => self.handle_room_event(e),
            (Message::LightControlMenu(e), _, Some(ActiveContextMenu::LightControl(menu))) => {
                match menu.update(e) {
                    Some(context_menus::light_control::Event::UpdateLightColour {
diff --git a/shalom/src/pages/room.rs b/shalom/src/pages/room.rs
index ce26d64..233dd18 100644
--- a/shalom/src/pages/room.rs
+++ b/shalom/src/pages/room.rs
@@ -1,23 +1,19 @@
use std::{collections::BTreeMap, sync::Arc, time::Duration};
pub mod lights;
pub mod listen;

use std::sync::Arc;

use iced::{
    advanced::graphics::core::Element,
    font::{Stretch, Weight},
    futures::StreamExt,
    subscription, theme,
    widget::{container, image::Handle, row, text, Column, Row},
    theme,
    widget::{row, text, Column},
    Color, Font, Length, Renderer, Subscription,
};
use url::Url;

use crate::{
    hass_client::MediaPlayerRepeat,
    oracle::{Light, MediaPlayerSpeaker, MediaPlayerSpeakerState, Oracle},
    subscriptions::download_image,
    theme::Icon,
    widgets,
    oracle::Oracle,
    widgets::{
        colour_picker::colour_from_hsb,
        image_background::image_background,
        room_navigation::{Page, RoomNavigation},
    },
@@ -26,28 +22,21 @@ use crate::{
#[derive(Debug)]
pub struct Room {
    id: &'static str,
    oracle: Arc<Oracle>,
    room: crate::oracle::Room,
    speaker: Option<(&'static str, MediaPlayerSpeaker)>,
    now_playing_image: Option<Handle>,
    lights: BTreeMap<&'static str, Light>,
    lights: lights::Lights,
    listen: listen::Listen,
    current_page: Page,
}

impl Room {
    pub fn new(id: &'static str, oracle: Arc<Oracle>) -> Self {
        let room = oracle.room(id).clone();
        let speaker = room.speaker(&oracle);

        let lights = room.lights(&oracle);

        Self {
            id,
            oracle,
            listen: listen::Listen::new(oracle.clone(), &room),
            lights: lights::Lights::new(oracle, &room),
            room,
            speaker,
            now_playing_image: None,
            lights,
            current_page: Page::Listen,
        }
    }
@@ -58,91 +47,8 @@ impl Room {

    pub fn update(&mut self, event: Message) -> Option<Event> {
        match event {
            Message::SetLightState(id, state) => {
                // give instant feedback before we get the event back from hass
                if let Some(light) = self.lights.get_mut(id) {
                    light.on = Some(state);
                }

                Some(Event::SetLightState(id, state))
            }
            Message::OpenLightOptions(id) => Some(Event::OpenLightContextMenu(id)),
            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);
                }

                None
            }
            Message::UpdateSpeaker => {
                let new = self.room.speaker(&self.oracle);

                if self
                    .speaker
                    .as_ref()
                    .and_then(|(_, v)| v.entity_picture.as_ref())
                    != new
                        .as_ref()
                        .as_ref()
                        .and_then(|(_, v)| v.entity_picture.as_ref())
                {
                    self.now_playing_image = None;
                }

                self.speaker = new;

                None
            }
            Message::UpdateLight(entity_id) => {
                if let Some(light) = self.oracle.fetch_light(entity_id) {
                    self.lights.insert(entity_id, light);
                }

                None
            }
            Message::OnSpeakerVolumeChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.volume = new;
                Some(Event::SetSpeakerVolume(id, new))
            }
            Message::OnSpeakerPositionChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.actual_media_position = Some(new);
                Some(Event::SetSpeakerPosition(id, new))
            }
            Message::OnSpeakerStateChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.state = if new {
                    MediaPlayerSpeakerState::Playing
                } else {
                    MediaPlayerSpeakerState::Paused
                };
                Some(Event::SetSpeakerPlaying(id, new))
            }
            Message::OnSpeakerMuteChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.muted = new;
                Some(Event::SetSpeakerMuted(id, new))
            }
            Message::OnSpeakerRepeatChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.repeat = new;
                Some(Event::SetSpeakerRepeat(id, new))
            }
            Message::OnSpeakerNextTrack => Some(Event::SpeakerNextTrack(self.speaker.as_ref()?.0)),
            Message::OnSpeakerPreviousTrack => {
                Some(Event::SpeakerPreviousTrack(self.speaker.as_ref()?.0))
            }
            Message::OnSpeakerShuffleChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.shuffle = new;
                Some(Event::SetSpeakerShuffle(id, new))
            }
            Message::Lights(v) => self.lights.update(v).map(Event::Lights),
            Message::Listen(v) => self.listen.update(v).map(Event::Listen),
            Message::ChangePage(page) => {
                self.current_page = page;
                None
@@ -161,63 +67,13 @@ impl Room {
            })
            .style(theme::Text::Color(Color::WHITE));

        let light = |id, light: &Light| {
            let mut toggle_card = widgets::toggle_card::toggle_card(
                &light.friendly_name,
                light.on.unwrap_or_default(),
                light.on.is_none(),
            )
            .icon(Icon::Bulb)
            .active_icon_colour(
                light
                    .hs_color
                    .zip(light.brightness)
                    .map(|((h, s), b)| colour_from_hsb(h, s, b / 255.)),
            );

            if let Some(state) = light.on {
                toggle_card = toggle_card
                    .on_press(Message::SetLightState(id, !state))
                    .on_long_press(Message::OpenLightOptions(id));
            }

            toggle_card
        };

        let mut col = Column::new().spacing(20).padding(40).push(header);

        match self.current_page {
            Page::Climate => {}
            Page::Listen => {
                if let Some((_, speaker)) = self.speaker.clone() {
                    col = col.push(container(
                        widgets::media_player::media_player(
                            speaker,
                            self.now_playing_image.clone(),
                        )
                        .on_volume_change(Message::OnSpeakerVolumeChange)
                        .on_mute_change(Message::OnSpeakerMuteChange)
                        .on_repeat_change(Message::OnSpeakerRepeatChange)
                        .on_state_change(Message::OnSpeakerStateChange)
                        .on_position_change(Message::OnSpeakerPositionChange)
                        .on_next_track(Message::OnSpeakerNextTrack)
                        .on_previous_track(Message::OnSpeakerPreviousTrack)
                        .on_shuffle_change(Message::OnSpeakerShuffleChange),
                    ));
                }
            }
            Page::Lights => {
                let lights = Row::with_children(
                    self.lights
                        .iter()
                        .map(|(id, item)| light(*id, item))
                        .map(Element::from)
                        .collect::<Vec<_>>(),
                )
                .spacing(10);
                col = col.push(lights);
            }
        }
        col = col.push(match self.current_page {
            Page::Climate => Element::from(row![]),
            Page::Listen => self.listen.view().map(Message::Listen),
            Page::Lights => self.lights.view().map(Message::Lights),
        });

        row![
            RoomNavigation::new(self.current_page)
@@ -234,77 +90,23 @@ impl Room {
    }

    pub fn subscription(&self) -> Subscription<Message> {
        let image_subscription = if let (Some(uri), None) = (
            self.speaker
                .as_ref()
                .and_then(|(_, v)| v.entity_picture.as_ref()),
            &self.now_playing_image,
        ) {
            download_image("now-playing", uri.clone(), |_, url, handle| {
                Message::NowPlayingImageLoaded(url, handle)
            })
        } else {
            Subscription::none()
        };

        let speaker_subscription = if let Some(speaker_id) = self.speaker.as_ref().map(|(k, _)| *k)
        {
            subscription::run_with_id(
                speaker_id,
                self.oracle
                    .subscribe_id(speaker_id)
                    .map(|()| Message::UpdateSpeaker),
            )
        } else {
            Subscription::none()
        };

        let light_subscriptions = Subscription::batch(self.lights.keys().copied().map(|key| {
            subscription::run_with_id(
                key,
                self.oracle
                    .subscribe_id(key)
                    .map(|()| Message::UpdateLight(key)),
            )
        }));

        Subscription::batch([
            image_subscription,
            speaker_subscription,
            light_subscriptions,
            self.listen.subscription().map(Message::Listen),
            self.lights.subscription().map(Message::Lights),
        ])
    }
}

pub enum Event {
    OpenLightContextMenu(&'static str),
    SetLightState(&'static str, bool),
    SetSpeakerVolume(&'static str, f32),
    SetSpeakerPosition(&'static str, Duration),
    SetSpeakerPlaying(&'static str, bool),
    SetSpeakerMuted(&'static str, bool),
    SetSpeakerShuffle(&'static str, bool),
    SetSpeakerRepeat(&'static str, MediaPlayerRepeat),
    SpeakerNextTrack(&'static str),
    SpeakerPreviousTrack(&'static str),
    Lights(lights::Event),
    Listen(listen::Event),
    Exit,
}

#[derive(Clone, Debug)]
pub enum Message {
    NowPlayingImageLoaded(Url, Handle),
    SetLightState(&'static str, bool),
    OpenLightOptions(&'static str),
    UpdateSpeaker,
    UpdateLight(&'static str),
    OnSpeakerVolumeChange(f32),
    OnSpeakerPositionChange(Duration),
    OnSpeakerStateChange(bool),
    OnSpeakerMuteChange(bool),
    OnSpeakerShuffleChange(bool),
    OnSpeakerRepeatChange(MediaPlayerRepeat),
    OnSpeakerNextTrack,
    OnSpeakerPreviousTrack,
    Lights(lights::Message),
    Listen(listen::Message),
    ChangePage(Page),
    Exit,
}
diff --git a/shalom/src/pages/room/lights.rs b/shalom/src/pages/room/lights.rs
new file mode 100644
index 0000000..4eebeb4
--- /dev/null
+++ b/shalom/src/pages/room/lights.rs
@@ -0,0 +1,103 @@
use std::{collections::BTreeMap, sync::Arc};

use iced::{futures::StreamExt, subscription, widget::Row, Element, Renderer, Subscription};

use crate::{
    oracle::{Light, Oracle, Room},
    theme::Icon,
    widgets::{self, colour_picker::colour_from_hsb},
};

#[derive(Debug)]
pub struct Lights {
    lights: BTreeMap<&'static str, Light>,
    oracle: Arc<Oracle>,
}

impl Lights {
    pub fn new(oracle: Arc<Oracle>, room: &Room) -> Self {
        let lights = room.lights(&oracle);

        Self { lights, oracle }
    }

    pub fn update(&mut self, event: Message) -> Option<Event> {
        match event {
            Message::SetLightState(id, state) => {
                // give instant feedback before we get the event back from hass
                if let Some(light) = self.lights.get_mut(id) {
                    light.on = Some(state);
                }

                Some(Event::SetLightState(id, state))
            }
            Message::OpenLightOptions(id) => Some(Event::OpenLightContextMenu(id)),
            Message::UpdateLight(entity_id) => {
                if let Some(light) = self.oracle.fetch_light(entity_id) {
                    self.lights.insert(entity_id, light);
                }

                None
            }
        }
    }

    pub fn view(&self) -> Element<'_, Message, Renderer> {
        let light = |id, light: &Light| {
            let mut toggle_card = widgets::toggle_card::toggle_card(
                &light.friendly_name,
                light.on.unwrap_or_default(),
                light.on.is_none(),
            )
            .icon(Icon::Bulb)
            .active_icon_colour(
                light
                    .hs_color
                    .zip(light.brightness)
                    .map(|((h, s), b)| colour_from_hsb(h, s, b / 255.)),
            );

            if let Some(state) = light.on {
                toggle_card = toggle_card
                    .on_press(Message::SetLightState(id, !state))
                    .on_long_press(Message::OpenLightOptions(id));
            }

            toggle_card
        };

        Row::with_children(
            self.lights
                .iter()
                .map(|(id, item)| light(*id, item))
                .map(Element::from)
                .collect::<Vec<_>>(),
        )
        .spacing(10)
        .into()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        Subscription::batch(self.lights.keys().copied().map(|key| {
            subscription::run_with_id(
                key,
                self.oracle
                    .subscribe_id(key)
                    .map(|()| Message::UpdateLight(key)),
            )
        }))
    }
}

#[derive(Copy, Clone)]
pub enum Event {
    OpenLightContextMenu(&'static str),
    SetLightState(&'static str, bool),
}

#[derive(Clone, Debug, Copy)]
pub enum Message {
    SetLightState(&'static str, bool),
    UpdateLight(&'static str),
    OpenLightOptions(&'static str),
}
diff --git a/shalom/src/pages/room/listen.rs b/shalom/src/pages/room/listen.rs
new file mode 100644
index 0000000..981cca9
--- /dev/null
+++ b/shalom/src/pages/room/listen.rs
@@ -0,0 +1,186 @@
use std::{sync::Arc, time::Duration};

use iced::{
    futures::StreamExt,
    subscription,
    widget::{container, image::Handle, Column},
    Element, Renderer, Subscription,
};
use url::Url;

use crate::{
    hass_client::MediaPlayerRepeat,
    oracle::{MediaPlayerSpeaker, MediaPlayerSpeakerState, Oracle, Room},
    subscriptions::download_image,
    widgets,
};

#[derive(Debug)]
pub struct Listen {
    room: Room,
    oracle: Arc<Oracle>,
    speaker: Option<(&'static str, MediaPlayerSpeaker)>,
    now_playing_image: Option<Handle>,
}

impl Listen {
    pub fn new(oracle: Arc<Oracle>, room: &Room) -> Self {
        let speaker = room.speaker(&oracle);

        Self {
            room: room.clone(),
            speaker,
            oracle,
            now_playing_image: 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);
                }

                None
            }
            Message::UpdateSpeaker => {
                let new = self.room.speaker(&self.oracle);

                if self
                    .speaker
                    .as_ref()
                    .and_then(|(_, v)| v.entity_picture.as_ref())
                    != new
                        .as_ref()
                        .as_ref()
                        .and_then(|(_, v)| v.entity_picture.as_ref())
                {
                    self.now_playing_image = None;
                }

                self.speaker = new;

                None
            }
            Message::OnSpeakerVolumeChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.volume = new;
                Some(Event::SetSpeakerVolume(id, new))
            }
            Message::OnSpeakerPositionChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.actual_media_position = Some(new);
                Some(Event::SetSpeakerPosition(id, new))
            }
            Message::OnSpeakerStateChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.state = if new {
                    MediaPlayerSpeakerState::Playing
                } else {
                    MediaPlayerSpeakerState::Paused
                };
                Some(Event::SetSpeakerPlaying(id, new))
            }
            Message::OnSpeakerMuteChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.muted = new;
                Some(Event::SetSpeakerMuted(id, new))
            }
            Message::OnSpeakerRepeatChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.repeat = new;
                Some(Event::SetSpeakerRepeat(id, new))
            }
            Message::OnSpeakerNextTrack => Some(Event::SpeakerNextTrack(self.speaker.as_ref()?.0)),
            Message::OnSpeakerPreviousTrack => {
                Some(Event::SpeakerPreviousTrack(self.speaker.as_ref()?.0))
            }
            Message::OnSpeakerShuffleChange(new) => {
                let (id, speaker) = self.speaker.as_mut()?;
                speaker.shuffle = new;
                Some(Event::SetSpeakerShuffle(id, new))
            }
        }
    }

    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())
                    .on_volume_change(Message::OnSpeakerVolumeChange)
                    .on_mute_change(Message::OnSpeakerMuteChange)
                    .on_repeat_change(Message::OnSpeakerRepeatChange)
                    .on_state_change(Message::OnSpeakerStateChange)
                    .on_position_change(Message::OnSpeakerPositionChange)
                    .on_next_track(Message::OnSpeakerNextTrack)
                    .on_previous_track(Message::OnSpeakerPreviousTrack)
                    .on_shuffle_change(Message::OnSpeakerShuffleChange),
            ));
        }

        col.into()
    }

    pub fn subscription(&self) -> Subscription<Message> {
        let image_subscription = if let (Some(uri), None) = (
            self.speaker
                .as_ref()
                .and_then(|(_, v)| v.entity_picture.as_ref()),
            &self.now_playing_image,
        ) {
            download_image("now-playing", uri.clone(), |_, url, handle| {
                Message::NowPlayingImageLoaded(url, handle)
            })
        } else {
            Subscription::none()
        };

        let speaker_subscription = if let Some(speaker_id) = self.speaker.as_ref().map(|(k, _)| *k)
        {
            subscription::run_with_id(
                speaker_id,
                self.oracle
                    .subscribe_id(speaker_id)
                    .map(|()| Message::UpdateSpeaker),
            )
        } else {
            Subscription::none()
        };

        Subscription::batch([image_subscription, speaker_subscription])
    }
}

#[derive(Copy, Clone)]
pub enum Event {
    SetSpeakerVolume(&'static str, f32),
    SetSpeakerPosition(&'static str, Duration),
    SetSpeakerPlaying(&'static str, bool),
    SetSpeakerMuted(&'static str, bool),
    SetSpeakerShuffle(&'static str, bool),
    SetSpeakerRepeat(&'static str, MediaPlayerRepeat),
    SpeakerNextTrack(&'static str),
    SpeakerPreviousTrack(&'static str),
}

#[derive(Clone, Debug)]
pub enum Message {
    NowPlayingImageLoaded(Url, Handle),
    UpdateSpeaker,
    OnSpeakerVolumeChange(f32),
    OnSpeakerPositionChange(Duration),
    OnSpeakerStateChange(bool),
    OnSpeakerMuteChange(bool),
    OnSpeakerShuffleChange(bool),
    OnSpeakerRepeatChange(MediaPlayerRepeat),
    OnSpeakerNextTrack,
    OnSpeakerPreviousTrack,
}