🏡 index : ~doyle/shalom.git

author Jordan Doyle <jordan@doyle.la> 2023-11-01 1:31:48.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-11-01 1:31:48.0 +00:00:00
commit
7000c41cba743674f237979415a5a5f98042b180 [patch]
tree
0ad2fc78ce22b6cbea261b14b39fbf4a5808bf74
parent
32cf1a03ab5725b1efd5cba0993c75d5e388cfa6
download
7000c41cba743674f237979415a5a5f98042b180.tar.gz

Read currently playing track from home assistant



Diff

 Cargo.lock                          | 415 ++++++++++++++++++++++++++++++++++++-
 shalom/Cargo.toml                   |   3 +-
 shalom/src/hass_client.rs           |  73 ++++---
 shalom/src/main.rs                  |  79 ++++---
 shalom/src/oracle.rs                | 167 +++++++++++++--
 shalom/src/pages/omni.rs            |  48 +---
 shalom/src/pages/room.rs            | 149 +++++++------
 shalom/src/subscriptions.rs         |  36 +++-
 shalom/src/widgets/cards/weather.rs |   1 +-
 shalom/src/widgets/media_player.rs  |  72 +++---
 shalom/src/widgets/track_card.rs    |  28 +-
 11 files changed, 863 insertions(+), 208 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c2e1179..e4307e9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -588,12 +588,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"

[[package]]
name = "encoding_rs"
version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
dependencies = [
 "cfg-if",
]

[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"

[[package]]
name = "errno"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860"
dependencies = [
 "libc",
 "windows-sys 0.48.0",
]

[[package]]
name = "error-code"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -654,6 +673,12 @@ dependencies = [
]

[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"

[[package]]
name = "fdeflate"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -909,7 +934,7 @@ checksum = "5e87caa7459145f5e5f167bf34db4532901404c679e62339fb712a0e3ccf722a"
dependencies = [
 "cosmic-text",
 "etagere",
 "lru",
 "lru 0.11.1",
 "wgpu",
]

@@ -976,6 +1001,25 @@ dependencies = [
]

[[package]]
name = "h2"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
dependencies = [
 "bytes",
 "fnv",
 "futures-core",
 "futures-sink",
 "futures-util",
 "http",
 "indexmap 1.9.3",
 "slab",
 "tokio",
 "tokio-util",
 "tracing",
]

[[package]]
name = "half"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1049,12 +1093,66 @@ dependencies = [
]

[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
 "bytes",
 "http",
 "pin-project-lite",
]

[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"

[[package]]
name = "httpdate"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"

[[package]]
name = "hyper"
version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
 "bytes",
 "futures-channel",
 "futures-core",
 "futures-util",
 "h2",
 "http",
 "http-body",
 "httparse",
 "httpdate",
 "itoa",
 "pin-project-lite",
 "socket2 0.4.10",
 "tokio",
 "tower-service",
 "tracing",
 "want",
]

[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
 "bytes",
 "hyper",
 "native-tls",
 "tokio",
 "tokio-native-tls",
]

[[package]]
name = "iced"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1314,6 +1412,12 @@ dependencies = [
]

[[package]]
name = "ipnet"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"

[[package]]
name = "itertools"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1445,6 +1549,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"

[[package]]
name = "linux-raw-sys"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f"

[[package]]
name = "lock_api"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1470,6 +1580,15 @@ dependencies = [
]

[[package]]
name = "lru"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
dependencies = [
 "hashbrown 0.14.2",
]

[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1544,6 +1663,12 @@ dependencies = [
]

[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"

[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1613,6 +1738,24 @@ dependencies = [
]

[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
 "lazy_static",
 "libc",
 "log",
 "openssl",
 "openssl-probe",
 "openssl-sys",
 "schannel",
 "security-framework",
 "security-framework-sys",
 "tempfile",
]

[[package]]
name = "ndk"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1866,6 +2009,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"

[[package]]
name = "openssl"
version = "0.10.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
dependencies = [
 "bitflags 2.4.1",
 "cfg-if",
 "foreign-types",
 "libc",
 "once_cell",
 "openssl-macros",
 "openssl-sys",
]

[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
 "proc-macro2",
 "quote",
 "syn 2.0.38",
]

[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"

[[package]]
name = "openssl-sys"
version = "0.9.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d"
dependencies = [
 "cc",
 "libc",
 "pkg-config",
 "vcpkg",
]

[[package]]
name = "orbclient"
version = "0.3.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2279,6 +2466,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216080ab382b992234dda86873c18d4c48358f5cfcb70fd693d7f6f2131b628b"

[[package]]
name = "reqwest"
version = "0.11.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b"
dependencies = [
 "base64",
 "bytes",
 "encoding_rs",
 "futures-core",
 "futures-util",
 "h2",
 "http",
 "http-body",
 "hyper",
 "hyper-tls",
 "ipnet",
 "js-sys",
 "log",
 "mime",
 "native-tls",
 "once_cell",
 "percent-encoding",
 "pin-project-lite",
 "serde",
 "serde_json",
 "serde_urlencoded",
 "system-configuration",
 "tokio",
 "tokio-native-tls",
 "tower-service",
 "url",
 "wasm-bindgen",
 "wasm-bindgen-futures",
 "web-sys",
 "winreg",
]

[[package]]
name = "resvg"
version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2326,6 +2551,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"

[[package]]
name = "rustix"
version = "0.38.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
dependencies = [
 "bitflags 2.4.1",
 "errno",
 "libc",
 "linux-raw-sys",
 "windows-sys 0.48.0",
]

[[package]]
name = "rustversion"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2371,6 +2609,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"

[[package]]
name = "schannel"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
 "windows-sys 0.48.0",
]

[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2396,6 +2643,29 @@ dependencies = [
]

[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
 "bitflags 1.3.2",
 "core-foundation",
 "core-foundation-sys",
 "libc",
 "security-framework-sys",
]

[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
 "core-foundation-sys",
 "libc",
]

[[package]]
name = "serde"
version = "1.0.189"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2436,6 +2706,18 @@ dependencies = [
]

[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
 "form_urlencoded",
 "itoa",
 "ryu",
 "serde",
]

[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2455,7 +2737,9 @@ dependencies = [
 "internment",
 "itertools",
 "keyframe",
 "lru 0.12.0",
 "once_cell",
 "reqwest",
 "serde",
 "serde_json",
 "strum",
@@ -2463,6 +2747,7 @@ dependencies = [
 "tokio",
 "tokio-tungstenite",
 "toml",
 "url",
 "yoke",
]

@@ -2542,6 +2827,16 @@ dependencies = [

[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
 "libc",
 "winapi",
]

[[package]]
name = "socket2"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
@@ -2560,7 +2855,7 @@ dependencies = [
 "cfg_aliases",
 "cocoa",
 "core-graphics",
 "fastrand",
 "fastrand 1.9.0",
 "foreign-types",
 "log",
 "nix 0.26.4",
@@ -2716,6 +3011,40 @@ dependencies = [
]

[[package]]
name = "system-configuration"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
 "bitflags 1.3.2",
 "core-foundation",
 "system-configuration-sys",
]

[[package]]
name = "system-configuration-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9"
dependencies = [
 "core-foundation-sys",
 "libc",
]

[[package]]
name = "tempfile"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
 "cfg-if",
 "fastrand 2.0.1",
 "redox_syscall 0.4.1",
 "rustix",
 "windows-sys 0.48.0",
]

[[package]]
name = "termcolor"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2851,7 +3180,7 @@ dependencies = [
 "mio",
 "num_cpus",
 "pin-project-lite",
 "socket2",
 "socket2 0.5.5",
 "tokio-macros",
 "windows-sys 0.48.0",
]
@@ -2868,6 +3197,16 @@ dependencies = [
]

[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
 "native-tls",
 "tokio",
]

[[package]]
name = "tokio-tungstenite"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2880,6 +3219,20 @@ dependencies = [
]

[[package]]
name = "tokio-util"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
dependencies = [
 "bytes",
 "futures-core",
 "futures-sink",
 "pin-project-lite",
 "tokio",
 "tracing",
]

[[package]]
name = "toml"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2925,6 +3278,37 @@ dependencies = [
]

[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"

[[package]]
name = "tracing"
version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
 "pin-project-lite",
 "tracing-core",
]

[[package]]
name = "tracing-core"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
dependencies = [
 "once_cell",
]

[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"

[[package]]
name = "ttf-parser"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3126,6 +3510,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"

[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"

[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3138,6 +3528,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"

[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
 "try-lock",
]

[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3709,6 +4108,16 @@ dependencies = [
]

[[package]]
name = "winreg"
version = "0.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1"
dependencies = [
 "cfg-if",
 "windows-sys 0.48.0",
]

[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/shalom/Cargo.toml b/shalom/Cargo.toml
index 8276f50..88319fb 100644
--- a/shalom/Cargo.toml
+++ b/shalom/Cargo.toml
@@ -12,6 +12,8 @@ once_cell = "1.18"
internment = "0.7.4"
itertools = "0.11"
keyframe = "1.1"
lru = "0.12"
reqwest = "0.11.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
strum = { version = "0.25", features = ["derive"] }
@@ -19,4 +21,5 @@ tokio = { version = "1.33", features = ["net", "sync", "rt", "macros", "time", "
tokio-tungstenite = "0.20"
toml = "0.8"
time = { version = "0.3", features = ["std"] }
url = "2.4.1"
yoke = { version = "0.7", features = ["derive"] }
diff --git a/shalom/src/hass_client.rs b/shalom/src/hass_client.rs
index 8fd2d4a..fac6d2d 100644
--- a/shalom/src/hass_client.rs
+++ b/shalom/src/hass_client.rs
@@ -1,4 +1,4 @@
#![allow(clippy::forget_non_drop)]
#![allow(clippy::forget_non_drop, dead_code)]

use std::{collections::HashMap, time::Duration};

@@ -8,12 +8,14 @@ use serde_json::value::RawValue;
use time::OffsetDateTime;
use tokio::sync::{mpsc, oneshot};
use tokio_tungstenite::tungstenite::Message;
use url::Url;
use yoke::{Yoke, Yokeable};

use crate::config::HomeAssistantConfig;

#[derive(Clone, Debug)]
pub struct Client {
    pub base: url::Url,
    sender: mpsc::Sender<(
        HassRequestKind,
        oneshot::Sender<Yoke<&'static RawValue, String>>,
@@ -124,7 +126,10 @@ pub async fn create(config: HomeAssistantConfig) -> Client {

    ready_recv.await.unwrap();

    Client { sender }
    Client {
        base: Url::parse(&format!("http://{}/", config.uri)).unwrap(),
        sender,
    }
}

#[derive(Deserialize, Yokeable)]
@@ -215,10 +220,10 @@ pub mod responses {
        pub area_id: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub configuration_url: Option<Cow<'a, str>>,
        #[serde(borrow)]
        #[serde(borrow, default)]
        pub config_entries: Vec<Cow<'a, str>>,
        #[serde(borrow)]
        pub connections: Vec<(Cow<'a, str>, Cow<'a, str>)>,
        pub connections: Vec<Vec<Cow<'a, str>>>,
        #[serde(borrow)]
        pub disabled_by: Option<Cow<'a, str>>,
        #[serde(borrow)]
@@ -227,16 +232,16 @@ pub mod responses {
        pub hw_version: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub id: Cow<'a, str>,
        #[serde(borrow, default)]
        pub identifiers: Vec<Vec<Cow<'a, str>>>,
        #[serde(borrow)]
        pub identifiers: Vec<(Cow<'a, str>, Cow<'a, str>)>,
        #[serde(borrow)]
        pub manufacturer: Cow<'a, str>,
        pub manufacturer: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub model: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub name_by_user: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub name: Cow<'a, str>,
        pub name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub sw_version: Option<Cow<'a, str>>,
        #[serde(borrow)]
@@ -251,7 +256,7 @@ pub mod responses {
        #[serde(borrow)]
        pub area_id: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub config_entry_id: Cow<'a, str>,
        pub config_entry_id: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub device_id: Option<Cow<'a, str>>,
        #[serde(borrow)]
@@ -270,9 +275,9 @@ pub mod responses {
        #[serde(borrow)]
        pub name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub original_name: Cow<'a, str>,
        pub original_name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub platform: Cow<'a, str>,
        pub platform: Option<Cow<'a, str>>,
        #[serde(borrow)]
        pub translation_key: Option<Cow<'a, str>>,
        #[serde(borrow)]
@@ -345,7 +350,7 @@ pub mod responses {
            };

            let attributes = match kind {
                "sun" => StateAttributes::Light(serde_json::from_str(attributes.get()).unwrap()),
                "sun" => StateAttributes::Sun(serde_json::from_str(attributes.get()).unwrap()),
                "media_player" => {
                    StateAttributes::MediaPlayer(serde_json::from_str(attributes.get()).unwrap())
                }
@@ -368,6 +373,7 @@ pub mod responses {
    }

    #[derive(Deserialize, Debug)]
    #[allow(clippy::large_enum_variant)]
    pub enum StateAttributes<'a> {
        Sun(StateSunAttributes),
        MediaPlayer(#[serde(borrow)] StateMediaPlayerAttributes<'a>),
@@ -393,26 +399,40 @@ pub mod responses {
    #[derive(Deserialize, Debug)]
    pub struct StateMediaPlayerAttributes<'a> {
        #[serde(borrow, default)]
        source_list: Vec<Cow<'a, str>>,
        pub source_list: Vec<Cow<'a, str>>,
        #[serde(borrow, default)]
        group_members: Vec<Cow<'a, str>>,
        volume_level: Option<f32>,
        is_volume_muted: Option<bool>,
        pub group_members: Vec<Cow<'a, str>>,
        pub volume_level: Option<f32>,
        pub is_volume_muted: Option<bool>,
        #[serde(borrow)]
        pub media_content_id: Option<MediaContentId<'a>>,
        #[serde(borrow)]
        media_content_id: Option<Cow<'a, str>>,
        pub media_content_type: Option<Cow<'a, str>>,
        pub media_duration: Option<u64>,
        pub media_position: Option<u64>,
        pub media_title: Option<Cow<'a, str>>,
        pub media_artist: Option<Cow<'a, str>>,
        pub media_album_name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        media_content_type: Option<Cow<'a, str>>,
        pub source: Option<Cow<'a, str>>,
        pub shuffle: Option<bool>,
        #[serde(borrow)]
        source: Option<Cow<'a, str>>,
        shuffle: Option<bool>,
        pub repeat: Option<Cow<'a, str>>,
        pub queue_position: Option<u32>,
        pub queue_size: Option<u32>,
        #[serde(borrow)]
        repeat: Option<Cow<'a, str>>,
        queue_position: Option<u32>,
        queue_size: Option<u32>,
        pub device_class: Option<Cow<'a, str>>,
        #[serde(borrow)]
        device_class: Option<Cow<'a, str>>,
        pub friendly_name: Option<Cow<'a, str>>,
        #[serde(borrow)]
        friendly_name: Option<Cow<'a, str>>,
        pub entity_picture: Option<Cow<'a, str>>,
    }

    #[derive(Deserialize, Debug)]
    #[serde(untagged)]
    pub enum MediaContentId<'a> {
        Uri(#[serde(borrow)] Cow<'a, str>),
        Int(u32),
    }

    #[derive(Deserialize, Debug)]
@@ -433,7 +453,7 @@ pub mod responses {
        entity_picture: Cow<'a, str>,
    }

    #[derive(Deserialize, Debug, EnumString, Copy, Clone)]
    #[derive(Default, Deserialize, Debug, EnumString, Copy, Clone)]
    #[serde(rename_all = "kebab-case")]
    #[strum(serialize_all = "kebab-case")]
    pub enum WeatherCondition {
@@ -454,6 +474,7 @@ pub mod responses {
        Windy,
        WindyVariant,
        Exceptional,
        #[default]
        #[serde(other)]
        Unknown,
    }
diff --git a/shalom/src/main.rs b/shalom/src/main.rs
index 46254b2..6fb6935 100644
--- a/shalom/src/main.rs
+++ b/shalom/src/main.rs
@@ -4,6 +4,7 @@ mod config;
mod hass_client;
mod oracle;
mod pages;
mod subscriptions;
mod theme;
mod widgets;

@@ -13,7 +14,8 @@ use iced::{
    alignment::{Horizontal, Vertical},
    font::{Stretch, Weight},
    widget::{column, container, row, scrollable, svg, text, vertical_slider, Column},
    Alignment, Application, Command, ContentFit, Element, Font, Length, Renderer, Settings, Theme,
    Alignment, Application, Command, ContentFit, Element, Font, Length, Renderer, Settings,
    Subscription, Theme,
};

use crate::{
@@ -26,7 +28,6 @@ use crate::{
pub struct Shalom {
    page: ActivePage,
    context_menu: Option<ActiveContextMenu>,
    homepage: ActivePage,
    oracle: Option<Arc<Oracle>>,
}

@@ -40,7 +41,6 @@ impl Application for Shalom {
        let this = Self {
            page: ActivePage::Loading,
            context_menu: None,
            homepage: ActivePage::Room("Living Room"),
            oracle: None,
        };

@@ -67,20 +67,37 @@ impl Application for Shalom {
    }

    fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
        match message {
            Message::Loaded(oracle) => {
                self.page = self.homepage.clone();
        #[allow(clippy::single_match)]
        match (message, &mut self.page) {
            (Message::Loaded(oracle), _) => {
                self.oracle = Some(oracle);
                self.page = ActivePage::Room(pages::room::Room::new(
                    "living_room",
                    self.oracle.as_deref().unwrap(),
                ));
            }
            Message::CloseContextMenu => {
            (Message::CloseContextMenu, _) => {
                self.context_menu = None;
            }
            Message::OpenContextMenu(menu) => {
                self.context_menu = Some(menu);
            }
            Message::ChangePage(page) => {
                self.page = page;
            (Message::OpenOmniPage, _) => {
                self.page = ActivePage::Omni(pages::omni::Omni::new(self.oracle.clone().unwrap()));
            }
            (Message::OmniEvent(e), ActivePage::Omni(r)) => match r.update(e) {
                Some(pages::omni::Event::OpenRoom(room)) => {
                    self.page = ActivePage::Room(pages::room::Room::new(
                        room,
                        self.oracle.as_deref().unwrap(),
                    ));
                }
                None => {}
            },
            (Message::RoomEvent(e), ActivePage::Room(r)) => match r.update(e) {
                Some(pages::room::Event::OpenLightContextMenu(light)) => {
                    self.context_menu = Some(ActiveContextMenu::LightOptions(light));
                }
                None => {}
            },
            _ => {}
        }

        Command::none()
@@ -89,21 +106,16 @@ impl Application for Shalom {
    fn view(&self) -> Element<'_, Self::Message, Renderer<Self::Theme>> {
        let page_content = match &self.page {
            ActivePage::Loading => Element::from(column!["Loading...",].spacing(20)),
            ActivePage::Room(room) => {
                Element::from(pages::room::Room::new(room, Message::OpenContextMenu))
            }
            ActivePage::Omni => Element::from(pages::omni::Omni::new(
                self.oracle.clone().unwrap(),
                Message::ChangePage,
            )),
            ActivePage::Room(room) => room.view().map(Message::RoomEvent),
            ActivePage::Omni(omni) => omni.view().map(Message::OmniEvent),
        };

        let mut content = Column::new().push(scrollable(page_content));

        let (show_back, show_home) = match &self.page {
            _ if self.page == self.homepage => (true, false),
            // _ if self.page == self.homepage => (true, false),
            ActivePage::Loading => (false, false),
            ActivePage::Omni => (false, true),
            ActivePage::Omni(_) => (false, true),
            ActivePage::Room(_) => (true, true),
        };

@@ -113,14 +125,14 @@ impl Application for Shalom {
                .width(32)
                .content_fit(ContentFit::None),
        )
        .on_press(Message::ChangePage(ActivePage::Omni));
        .on_press(Message::OpenOmniPage);
        let home = mouse_area(
            svg(Icon::Home)
                .height(32)
                .width(32)
                .content_fit(ContentFit::None),
        )
        .on_press(Message::ChangePage(self.homepage.clone()));
        );
        // .on_press(Message::ChangePage(self.homepage.clone()));

        let navigation = match (show_back, show_home) {
            (true, true) => Some(Element::from(
@@ -174,6 +186,13 @@ impl Application for Shalom {
            content.into()
        }
    }

    fn subscription(&self) -> Subscription<Self::Message> {
        match &self.page {
            ActivePage::Room(room) => room.subscription().map(Message::RoomEvent),
            _ => Subscription::none(),
        }
    }
}

async fn load_config() -> Config {
@@ -185,15 +204,17 @@ async fn load_config() -> Config {
pub enum Message {
    Loaded(Arc<Oracle>),
    CloseContextMenu,
    ChangePage(ActivePage),
    OpenContextMenu(ActiveContextMenu),
    OpenOmniPage,
    OmniEvent(pages::omni::Message),
    RoomEvent(pages::room::Message),
}

#[derive(Debug, Clone, PartialEq)]
#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum ActivePage {
    Loading,
    Room(&'static str),
    Omni,
    Room(pages::room::Room),
    Omni(pages::omni::Omni),
}

#[derive(Clone, Debug)]
diff --git a/shalom/src/oracle.rs b/shalom/src/oracle.rs
index d1acf95..9420c1f 100644
--- a/shalom/src/oracle.rs
+++ b/shalom/src/oracle.rs
@@ -1,39 +1,128 @@
use std::{collections::BTreeMap, str::FromStr};
use std::{
    collections::{BTreeMap, HashMap},
    str::FromStr,
    time::Duration,
};

use internment::Intern;
use url::Url;

use crate::hass_client::{
    responses::{AreaRegistryList, StateAttributes, StatesList, WeatherCondition},
    responses::{
        AreaRegistryList, DeviceRegistryList, EntityRegistryList, StateAttributes, StatesList,
        WeatherCondition,
    },
    HassRequestKind,
};

#[allow(dead_code)]
#[derive(Debug)]
pub struct Oracle {
    client: crate::hass_client::Client,
    rooms: BTreeMap<Intern<str>, Room>,
    rooms: BTreeMap<&'static str, Room>,
    pub weather: Weather,
    pub media_players: BTreeMap<&'static str, MediaPlayer>,
}

impl Oracle {
    pub async fn new(hass_client: crate::hass_client::Client) -> Self {
        let (rooms, states) = tokio::join!(
        let (rooms, devices, entities, states) = tokio::join!(
            hass_client.request::<AreaRegistryList<'_>>(HassRequestKind::AreaRegistry),
            hass_client.request::<DeviceRegistryList<'_>>(HassRequestKind::DeviceRegistry),
            hass_client.request::<EntityRegistryList<'_>>(HassRequestKind::EntityRegistry),
            hass_client.request::<StatesList<'_>>(HassRequestKind::GetStates),
        );

        let rooms = &rooms.get().0;
        let states = states.get();
        let devices = &devices.get().0;
        let entities = &entities.get().0;

        let all_entities = entities
            .iter()
            .fold(HashMap::<_, Vec<_>>::new(), |mut acc, curr| {
                if let Some(device_id) = curr.device_id.as_deref() {
                    acc.entry(device_id).or_default().push(curr);
                }

                acc
            });

        let room_devices = devices
            .iter()
            .fold(HashMap::<_, Vec<_>>::new(), |mut acc, curr| {
                if let (Some(area_id), Some(entity)) =
                    (curr.area_id.as_deref(), all_entities.get(curr.id.as_ref()))
                {
                    acc.entry(area_id).or_default().push(entity);
                }

                acc
            });

        let rooms = rooms
            .get()
            .0
            .iter()
            .map(|room| {
                (
                    Intern::from(room.area_id.as_ref()),
                    Room {
                        name: Intern::from(room.name.as_ref()),
                    },
                )
                let entities = room_devices
                    .get(room.area_id.as_ref())
                    .iter()
                    .flat_map(|v| v.iter())
                    .flat_map(|v| v.iter())
                    .map(|v| Intern::from(v.entity_id.as_ref()))
                    .collect::<Vec<Intern<str>>>();

                let speaker_id = entities
                    .iter()
                    .filter(|v| {
                        // TODO: support multiple media players in one room
                        v.as_ref() != "media_player.lg_webos_smart_tv"
                    })
                    .find(|v| v.starts_with("media_player."))
                    .copied();

                let area = Intern::<str>::from(room.area_id.as_ref()).as_ref();
                let room = Room {
                    name: Intern::from(room.name.as_ref()),
                    entities,
                    speaker_id,
                };

                (area, room)
            })
            .collect();

        eprintln!("{rooms:#?}");

        let media_players = states
            .0
            .iter()
            .filter_map(|state| {
                if let StateAttributes::MediaPlayer(attr) = &state.attributes {
                    let kind = if attr.volume_level.is_some() {
                        MediaPlayer::Speaker(MediaPlayerSpeaker {
                            volume: attr.volume_level.unwrap(),
                            muted: attr.is_volume_muted.unwrap(),
                            source: Box::from(attr.source.as_deref().unwrap_or("")),
                            media_duration: attr.media_duration.map(Duration::from_secs),
                            media_position: attr.media_position.map(Duration::from_secs),
                            media_title: attr.media_title.as_deref().map(Box::from),
                            media_artist: attr.media_artist.as_deref().map(Box::from),
                            media_album_name: attr.media_album_name.as_deref().map(Box::from),
                            shuffle: attr.shuffle.unwrap_or(false),
                            repeat: Box::from(attr.repeat.as_deref().unwrap_or("")),
                            entity_picture: attr
                                .entity_picture
                                .as_deref()
                                .map(|path| hass_client.base.join(path).unwrap()),
                        })
                    } else {
                        MediaPlayer::Tv(MediaPlayerTv {})
                    };

                    Some((Intern::<str>::from(state.entity_id.as_ref()).as_ref(), kind))
                } else {
                    None
                }
            })
            .collect();

@@ -41,17 +130,61 @@ impl Oracle {
            client: hass_client,
            rooms,
            weather: Weather::parse_from_states(states),
            media_players,
        }
    }

    pub fn rooms(&self) -> impl Iterator<Item = &'_ Room> + '_ {
        self.rooms.values()
    pub fn rooms(&self) -> impl Iterator<Item = (&'static str, &'_ Room)> + '_ {
        self.rooms.iter().map(|(k, v)| (*k, v))
    }

    pub fn room(&self, id: &str) -> &Room {
        self.rooms.get(id).unwrap()
    }
}

#[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum MediaPlayer {
    Speaker(MediaPlayerSpeaker),
    Tv(MediaPlayerTv),
}

#[derive(Debug, Clone)]
pub struct MediaPlayerSpeaker {
    pub volume: f32,
    pub muted: bool,
    pub source: Box<str>,
    pub media_duration: Option<Duration>,
    pub media_position: Option<Duration>,
    pub media_title: Option<Box<str>>,
    pub media_artist: Option<Box<str>>,
    pub media_album_name: Option<Box<str>>,
    pub shuffle: bool,
    pub repeat: Box<str>,
    pub entity_picture: Option<Url>,
}

#[derive(Debug)]
pub struct MediaPlayerTv {}

#[derive(Debug, Clone)]
pub struct Room {
    pub name: Intern<str>,
    pub entities: Vec<Intern<str>>,
    pub speaker_id: Option<Intern<str>>,
}

impl Room {
    pub fn speaker<'a>(&self, oracle: &'a Oracle) -> Option<&'a MediaPlayerSpeaker> {
        match self
            .speaker_id
            .and_then(|v| oracle.media_players.get(v.as_ref()))?
        {
            MediaPlayer::Speaker(v) => Some(v),
            MediaPlayer::Tv(_) => None,
        }
    }
}

#[derive(Debug)]
@@ -63,18 +196,18 @@ pub struct Weather {
}

impl Weather {
    #[allow(clippy::cast_possible_truncation)]
    fn parse_from_states(states: &StatesList) -> Self {
        let (state, weather) = states
            .0
            .iter()
            .filter_map(|v| match &v.attributes {
            .find_map(|v| match &v.attributes {
                StateAttributes::Weather(attr) => Some((&v.state, attr)),
                _ => None,
            })
            .next()
            .unwrap();

        let condition = WeatherCondition::from_str(&state).unwrap_or(WeatherCondition::Unknown);
        let condition = WeatherCondition::from_str(state).unwrap_or_default();

        let (high, low) =
            weather
diff --git a/shalom/src/pages/omni.rs b/shalom/src/pages/omni.rs
index ed9b8fc..846a210 100644
--- a/shalom/src/pages/omni.rs
+++ b/shalom/src/pages/omni.rs
@@ -3,43 +3,45 @@ use std::sync::Arc;
use iced::{
    advanced::graphics::core::Element,
    font::{Stretch, Weight},
    widget::{column, component, scrollable, text, Column, Component, Row},
    widget::{column, scrollable, text, Column, Row},
    Font, Renderer,
};
use itertools::Itertools;

use crate::{oracle::Oracle, theme::Image, widgets::image_card, ActivePage};
use crate::{oracle::Oracle, theme::Image, widgets::image_card};

pub struct Omni<M> {
#[derive(Debug)]
pub struct Omni {
    oracle: Arc<Oracle>,
    open_page: fn(ActivePage) -> M,
}

impl<M> Omni<M> {
    pub fn new(oracle: Arc<Oracle>, open_page: fn(ActivePage) -> M) -> Self {
        Self { oracle, open_page }
impl Omni {
    pub fn new(oracle: Arc<Oracle>) -> Self {
        Self { oracle }
    }
}

impl<M: Clone> Component<M, Renderer> for Omni<M> {
    type State = State;
    type Event = Event;

    fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option<M> {
impl Omni {
    #[allow(
        clippy::unnecessary_wraps,
        clippy::needless_pass_by_value,
        clippy::unused_self
    )]
    pub fn update(&mut self, event: Message) -> Option<Event> {
        match event {
            Event::OpenRoom(room) => Some((self.open_page)(ActivePage::Room(room))),
            Message::OpenRoom(room) => Some(Event::OpenRoom(room)),
        }
    }

    fn view(&self, _state: &Self::State) -> Element<'_, Self::Event, Renderer> {
    pub fn view(&self) -> Element<'_, Message, Renderer> {
        let greeting = text("Good Evening").size(60).font(Font {
            weight: Weight::Bold,
            stretch: Stretch::Condensed,
            ..Font::with_name("Helvetica Neue")
        });

        let room = |room, image| {
            image_card::image_card(image, room).on_press(Event::OpenRoom(room))
        let room = |id, room, image| {
            image_card::image_card(image, room).on_press(Message::OpenRoom(id))
            // .height(Length::Fixed(128.0))
            // .width(Length::FillPortion(1))
        };
@@ -47,7 +49,7 @@ impl<M: Clone> Component<M, Renderer> for Omni<M> {
        let rooms = self
            .oracle
            .rooms()
            .map(|r| room(r.name.as_ref(), determine_image(&r.name)))
            .map(|(id, r)| room(id, r.name.as_ref(), determine_image(&r.name)))
            .chunks(2)
            .into_iter()
            .map(|children| children.into_iter().fold(Row::new().spacing(10), Row::push))
@@ -79,16 +81,12 @@ fn determine_image(name: &str) -> Image {
#[derive(Default, Hash)]
pub struct State {}

#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum Event {
    OpenRoom(&'static str),
}

impl<'a, M> From<Omni<M>> for Element<'a, M, Renderer>
where
    M: 'a + Clone,
{
    fn from(card: Omni<M>) -> Self {
        component(card)
    }
#[derive(Clone, Debug)]
pub enum Message {
    OpenRoom(&'static str),
}
diff --git a/shalom/src/pages/room.rs b/shalom/src/pages/room.rs
index 0cff432..0969069 100644
--- a/shalom/src/pages/room.rs
+++ b/shalom/src/pages/room.rs
@@ -1,100 +1,123 @@
use std::collections::HashMap;

use iced::{
    advanced::graphics::core::Element,
    font::{Stretch, Weight},
    widget::{column, component, container, row, text, Component},
    Font, Renderer,
    widget::{container, image::Handle, row, text, Column},
    Font, Renderer, Subscription,
};
use url::Url;

use crate::{theme::Icon, widgets, ActiveContextMenu};
use crate::{
    oracle::{MediaPlayerSpeaker, Oracle},
    subscriptions::download_image,
    theme::Icon,
    widgets,
};

pub struct Room<M> {
    name: &'static str,
    open_context_menu: fn(ActiveContextMenu) -> M,
#[derive(Debug)]
pub struct Room {
    room: crate::oracle::Room,
    speaker: Option<MediaPlayerSpeaker>,
    now_playing_image: Option<Handle>,
}

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

        Self {
            name,
            open_context_menu,
            room,
            speaker,
            now_playing_image: None,
        }
    }
}

impl<M: Clone> Component<M, Renderer> for Room<M> {
    type State = State;
    type Event = Event;

    fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<M> {
    pub fn update(&mut self, event: Message) -> Option<Event> {
        match event {
            Event::LightToggle(name) => {
                let x = state.lights.entry(name).or_default();
                if *x == 0 {
                    *x = 1;
                } else {
                    *x = 0;
                }

            Message::LightToggle(_name) => {
                // let x = state.lights.entry(name).or_default();
                // if *x == 0 {
                //     *x = 1;
                // } else {
                //     *x = 0;
                // }
                //
                None
            }
            Message::OpenLightOptions(name) => Some(Event::OpenLightContextMenu(name)),
            Message::UpdateLightAmount(_name, _v) => {
                // let x = state.lights.entry(name).or_default();
                // *x = v;
                None
            }
            Event::OpenLightOptions(name) => Some((self.open_context_menu)(
                ActiveContextMenu::LightOptions(name),
            )),
            Event::UpdateLightAmount(name, v) => {
                let x = state.lights.entry(name).or_default();
                *x = v;
            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
            }
        }
    }

    fn view(&self, state: &Self::State) -> Element<'_, Self::Event, Renderer> {
        let header = text(self.name).size(60).font(Font {
    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")
        });

        let light = |name| {
            widgets::toggle_card::toggle_card(
                name,
                state.lights.get(name).copied().unwrap_or_default() > 0,
            )
            .icon(Icon::Bulb)
            .on_press(Event::LightToggle(name))
            .on_long_press(Event::OpenLightOptions(name))
            widgets::toggle_card::toggle_card(name, false)
                .icon(Icon::Bulb)
                .on_press(Message::LightToggle(name))
                .on_long_press(Message::OpenLightOptions(name))
        };

        column![
            header,
            container(widgets::media_player::media_player()).padding([12, 0, 24, 0]),
            row![light("Main"), light("Lamp"), light("TV")].spacing(10),
        ]
        .spacing(20)
        .padding(40)
        .into()
        let mut col = Column::new().spacing(20).padding(40).push(header);

        if let Some(speaker) = self.speaker.clone() {
            col = col.push(
                container(widgets::media_player::media_player(
                    speaker,
                    self.now_playing_image.clone(),
                ))
                .padding([12, 0, 24, 0]),
            );
        }

        col = col.push(row![light("Main"), light("Lamp"), light("TV")].spacing(10));

        col.into()
    }
}

#[derive(Default)]
pub struct State {
    lights: HashMap<&'static str, u8>,
    pub fn subscription(&self) -> Subscription<Message> {
        if let (Some(uri), None) = (
            self.speaker
                .as_ref()
                .and_then(|v| v.entity_picture.as_ref()),
            &self.now_playing_image,
        ) {
            download_image(uri.clone(), uri.clone(), Message::NowPlayingImageLoaded)
        } else {
            Subscription::none()
        }
    }
}

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

#[derive(Clone, Debug)]
pub enum Message {
    NowPlayingImageLoaded(Url, Handle),
    LightToggle(&'static str),
    OpenLightOptions(&'static str),
    UpdateLightAmount(&'static str, u8),
}

impl<'a, M> From<Room<M>> for Element<'a, M, Renderer>
where
    M: 'a + Clone,
{
    fn from(card: Room<M>) -> Self {
        component(card)
    }
}
diff --git a/shalom/src/subscriptions.rs b/shalom/src/subscriptions.rs
new file mode 100644
index 0000000..79ba7aa
--- /dev/null
+++ b/shalom/src/subscriptions.rs
@@ -0,0 +1,36 @@
use std::{hash::Hash, num::NonZeroUsize, sync::Mutex};

use iced::{futures::stream, subscription, widget::image, Subscription};
use lru::LruCache;
use once_cell::sync::Lazy;
use url::Url;

pub fn download_image<I: Hash + 'static, M: 'static>(
    id: I,
    url: Url,
    resp: fn(Url, image::Handle) -> M,
) -> 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,
        stream::once(async move {
            if let Some(handle) = CACHE.lock().unwrap().get(&url) {
                return (resp)(url, handle.clone());
            }

            let bytes = reqwest::get(url.clone())
                .await
                .unwrap()
                .bytes()
                .await
                .unwrap();
            let handle = image::Handle::from_memory(bytes);

            CACHE.lock().unwrap().push(url.clone(), handle.clone());

            (resp)(url, handle)
        }),
    )
}
diff --git a/shalom/src/widgets/cards/weather.rs b/shalom/src/widgets/cards/weather.rs
index 473f4eb..43443d4 100644
--- a/shalom/src/widgets/cards/weather.rs
+++ b/shalom/src/widgets/cards/weather.rs
@@ -19,6 +19,7 @@ use iced::{

use crate::oracle::Oracle;

#[allow(clippy::module_name_repetitions)]
pub struct WeatherCard<M> {
    pub on_click: Option<M>,
    pub oracle: Arc<Oracle>,
diff --git a/shalom/src/widgets/media_player.rs b/shalom/src/widgets/media_player.rs
index d31a863..c512fd9 100644
--- a/shalom/src/widgets/media_player.rs
+++ b/shalom/src/widgets/media_player.rs
@@ -3,11 +3,14 @@ use std::{fmt::Display, time::Duration};
use iced::{
    advanced::graphics::core::Element,
    theme::{Svg, Text},
    widget::{column as icolumn, component, container, row, slider, svg, text, Component},
    widget::{
        column as icolumn, component, container, image::Handle, row, slider, svg, text, Component,
    },
    Alignment, Length, Renderer, Theme,
};

use crate::{
    oracle::MediaPlayerSpeaker,
    theme::{
        colours::{SKY_500, SLATE_400, SLATE_600},
        Icon,
@@ -15,34 +18,23 @@ use crate::{
    widgets::mouse_area::mouse_area,
};

pub fn media_player<M>() -> MediaPlayer<M> {
    MediaPlayer::default()
pub fn media_player<M>(device: MediaPlayerSpeaker, image: Option<Handle>) -> MediaPlayer<M> {
    MediaPlayer {
        height: Length::Shrink,
        width: Length::Fill,
        device,
        image,
        _on_something: None,
    }
}

#[derive(Clone)]
pub struct MediaPlayer<M> {
    height: Length,
    width: Length,
    now_playing: NowPlaying,
    on_something: Option<M>,
    track_length: Duration,
}

impl<M> Default for MediaPlayer<M> {
    fn default() -> Self {
        Self {
            height: Length::Shrink,
            width: Length::Fill,
            now_playing: NowPlaying {
                album_art: "https://i.scdn.co/image/ab67616d00004851d771166c366eff01950de570"
                    .to_string(),
                song: "Almost Had to Start a Fight/In and Out of Patience".to_string(),
                artist: "Parquet Court".to_string(),
                loved: true,
            },
            on_something: None,
            track_length: Duration::from_secs(194),
        }
    }
    device: MediaPlayerSpeaker,
    image: Option<Handle>,
    _on_something: Option<M>,
}

impl<M> Component<M, Renderer> for MediaPlayer<M> {
@@ -80,9 +72,9 @@ impl<M> Component<M, Renderer> for MediaPlayer<M> {
        container(
            row![
                container(crate::widgets::track_card::track_card(
                    self.now_playing.artist.clone(),
                    self.now_playing.song.clone(),
                    false,
                    self.device.media_artist.as_ref().unwrap().to_string(),
                    self.device.media_title.as_ref().unwrap().to_string(),
                    self.image.clone(),
                ),)
                .width(Length::FillPortion(8)),
                icolumn![
@@ -128,11 +120,11 @@ impl<M> Component<M, Renderer> for MediaPlayer<M> {
                            .style(Text::Color(SLATE_400))
                            .size(12),
                        slider(
                            0.0..=self.track_length.as_secs_f64(),
                            0.0..=self.device.media_duration.unwrap().as_secs_f64(),
                            state.track_position.as_secs_f64(),
                            Event::PositionChange
                        ),
                        text(format_time(self.track_length))
                        text(format_time(self.device.media_duration.unwrap()))
                            .style(Text::Color(SLATE_400))
                            .size(12),
                    ]
@@ -167,17 +159,11 @@ impl<M> Component<M, Renderer> for MediaPlayer<M> {
        .width(self.width)
        .center_x()
        .center_y()
        // .style(Container::Custom(Box::new(Style::Inactive)))
        .into()
    }
}

pub struct NowPlaying {
    album_art: String,
    song: String,
    artist: String,
    loved: bool,
}

#[derive(Default)]
pub struct State {
    muted: bool,
@@ -219,6 +205,20 @@ pub enum Style {
    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(SLATE_200)),
//             border_radius: Default::default(),
//             border_width: 0.0,
//             border_color: Default::default(),
//         }
//     }
// }

impl svg::StyleSheet for Style {
    type Style = Theme;

diff --git a/shalom/src/widgets/track_card.rs b/shalom/src/widgets/track_card.rs
index aaad3e2..fcda827 100644
--- a/shalom/src/widgets/track_card.rs
+++ b/shalom/src/widgets/track_card.rs
@@ -2,27 +2,27 @@ use iced::{
    advanced::graphics::core::Element,
    theme::Text,
    widget::{
        column as icolumn, component,
        column as icolumn, component, container,
        image::{self, Image},
        row, text, Component,
        row, text, vertical_space, Component,
    },
    Alignment, Renderer,
    Alignment, Background, Renderer, Theme,
};

use crate::theme::colours::SLATE_400;

pub fn track_card(artist: String, song: String, loved: bool) -> TrackCard {
pub fn track_card(artist: String, song: String, image: Option<image::Handle>) -> TrackCard {
    TrackCard {
        artist,
        song,
        loved,
        image,
    }
}

pub struct TrackCard {
    artist: String,
    song: String,
    loved: bool,
    image: Option<image::Handle>,
}

impl<M> Component<M, Renderer> for TrackCard {
@@ -34,10 +34,20 @@ impl<M> Component<M, Renderer> 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_400)),
                        ..container::Appearance::default()
                    },
                ))
            };

        row![
            Image::new(image::Handle::from_path("/tmp/tmp.jpg"))
                .width(64)
                .height(64),
            image,
            icolumn![
                text(&self.song).size(14),
                text(&self.artist).style(Text::Color(SLATE_400)).size(14)