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(-)
@@ -1,2 +1,3 @@
/target
/config.toml
/.vscode
@@ -45,6 +45,120 @@
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 {
@@ -105,105 +219,11 @@
(Message::OmniEvent(e), ActivePage::Omni(r), _) => match r.update(e) {
Some(pages::omni::Event::OpenRoom(room)) => {
self.page = self.build_room_route(room);
Command::none()
}
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 {
@@ -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 @@
#[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 @@
pub fn update(&mut self, event: Message) -> Option<Event> {
match event {
Message::SetLightState(id, state) => {
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
@@ -160,64 +66,14 @@
..Font::with_name("Helvetica Neue")
})
.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 @@
}
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,
}
@@ -1,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) => {
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),
}
@@ -1,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,
}