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/subscriptions.rs | 36 ++++++++++++++++++++++++++++++++++++
shalom/src/pages/omni.rs | 48 ++++++++++++++++++++++++------------------------
shalom/src/pages/room.rs | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
shalom/src/widgets/media_player.rs | 72 ++++++++++++++++++++++++++++++++++++++----------------------------------
shalom/src/widgets/track_card.rs | 28 +++++++++++++++++++---------
shalom/src/widgets/cards/weather.rs | 1 +
11 files changed, 864 insertions(+), 209 deletions(-)
@@ -588,10 +588,29 @@
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"
@@ -652,6 +671,12 @@
dependencies = [
"instant",
]
[[package]]
name = "fastrand"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fdeflate"
@@ -909,7 +934,7 @@
dependencies = [
"cosmic-text",
"etagere",
"lru",
"lru 0.11.1",
"wgpu",
]
@@ -973,6 +998,25 @@
dependencies = [
"euclid",
"svg_fmt",
]
[[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]]
@@ -1046,6 +1090,17 @@
"bytes",
"fnv",
"itoa",
]
[[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]]
@@ -1053,8 +1108,51 @@
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"
@@ -1312,6 +1410,12 @@
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "ipnet"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "itertools"
@@ -1443,6 +1547,12 @@
version = "0.2.8"
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"
@@ -1465,6 +1575,15 @@
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21"
dependencies = [
"hashbrown 0.14.2",
]
[[package]]
name = "lru"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60"
dependencies = [
"hashbrown 0.14.2",
]
@@ -1542,6 +1661,12 @@
"log",
"objc",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
@@ -1613,6 +1738,24 @@
]
[[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"
@@ -1864,6 +2007,50 @@
version = "1.18.0"
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"
@@ -2277,6 +2464,44 @@
version = "1.0.0"
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"
@@ -2324,6 +2549,19 @@
version = "1.1.0"
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"
@@ -2369,6 +2607,15 @@
version = "1.0.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"
@@ -2393,9 +2640,32 @@
"memmap2 0.5.10",
"smithay-client-toolkit",
"tiny-skia 0.8.4",
]
[[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"
@@ -2431,7 +2701,19 @@
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
dependencies = [
"serde",
]
[[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",
]
@@ -2455,7 +2737,9 @@
"internment",
"itertools",
"keyframe",
"lru 0.12.0",
"once_cell",
"reqwest",
"serde",
"serde_json",
"strum",
@@ -2463,6 +2747,7 @@
"tokio",
"tokio-tungstenite",
"toml",
"url",
"yoke",
]
@@ -2538,6 +2823,16 @@
dependencies = [
"smithay-client-toolkit",
"wayland-client 0.29.5",
]
[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi",
]
[[package]]
@@ -2560,7 +2855,7 @@
"cfg_aliases",
"cocoa",
"core-graphics",
"fastrand",
"fastrand 1.9.0",
"foreign-types",
"log",
"nix 0.26.4",
@@ -2711,8 +3006,42 @@
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e801cf239ecd6ccd71f03d270d67dd53d13e90aab208bf4b8fe4ad957ea949b0"
dependencies = [
"libc",
]
[[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]]
@@ -2851,7 +3180,7 @@
"mio",
"num_cpus",
"pin-project-lite",
"socket2",
"socket2 0.5.5",
"tokio-macros",
"windows-sys 0.48.0",
]
@@ -2865,6 +3194,16 @@
"proc-macro2",
"quote",
"syn 2.0.38",
]
[[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]]
@@ -2877,6 +3216,20 @@
"log",
"tokio",
"tungstenite",
]
[[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]]
@@ -2922,7 +3275,38 @@
"serde_spanned",
"toml_datetime",
"winnow",
]
[[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"
@@ -3124,6 +3508,12 @@
version = "0.7.6"
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"
@@ -3136,6 +3526,15 @@
version = "0.9.4"
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"
@@ -3706,6 +4105,16 @@
checksum = "a3b801d0e0a6726477cc207f60162da452f3a95adb368399bef20a946e06f65c"
dependencies = [
"memchr",
]
[[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]]
@@ -12,6 +12,8 @@
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-tungstenite = "0.20"
toml = "0.8"
time = { version = "0.3", features = ["std"] }
url = "2.4.1"
yoke = { version = "0.7", features = ["derive"] }
@@ -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 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 @@
ready_recv.await.unwrap();
Client { sender }
Client {
base: Url::parse(&format!("http://{}/", config.uri)).unwrap(),
sender,
}
}
#[derive(Deserialize, Yokeable)]
@@ -215,10 +220,10 @@
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 hw_version: Option<Cow<'a, str>>,
#[serde(borrow)]
pub id: Cow<'a, str>,
#[serde(borrow)]
pub identifiers: Vec<(Cow<'a, str>, Cow<'a, str>)>,
#[serde(borrow, default)]
pub identifiers: Vec<Vec<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 @@
#[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 @@
#[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 @@
};
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 @@
}
#[derive(Deserialize, Debug)]
#[allow(clippy::large_enum_variant)]
pub enum StateAttributes<'a> {
Sun(StateSunAttributes),
MediaPlayer(#[serde(borrow)] StateMediaPlayerAttributes<'a>),
@@ -393,26 +399,40 @@
#[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 @@
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 @@
Windy,
WindyVariant,
Exceptional,
#[default]
#[serde(other)]
Unknown,
}
@@ -1,9 +1,10 @@
#![deny(clippy::pedantic)]
mod config;
mod hass_client;
mod oracle;
mod pages;
mod subscriptions;
mod theme;
mod widgets;
@@ -13,7 +14,8 @@
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 @@
pub struct Shalom {
page: ActivePage,
context_menu: Option<ActiveContextMenu>,
homepage: ActivePage,
oracle: Option<Arc<Oracle>>,
}
@@ -40,7 +41,6 @@
let this = Self {
page: ActivePage::Loading,
context_menu: None,
homepage: ActivePage::Room("Living Room"),
oracle: None,
};
@@ -67,20 +67,37 @@
}
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::OpenOmniPage, _) => {
self.page = ActivePage::Omni(pages::omni::Omni::new(self.oracle.clone().unwrap()));
}
Message::ChangePage(page) => {
self.page = page;
}
(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 @@
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),
ActivePage::Loading => (false, false),
ActivePage::Omni => (false, true),
ActivePage::Omni(_) => (false, true),
ActivePage::Room(_) => (true, true),
};
@@ -113,14 +125,14 @@
.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()));
);
let navigation = match (show_back, show_home) {
(true, true) => Some(Element::from(
@@ -172,6 +184,13 @@
.into()
} else {
content.into()
}
}
fn subscription(&self) -> Subscription<Self::Message> {
match &self.page {
ActivePage::Room(room) => room.subscription().map(Message::RoomEvent),
_ => Subscription::none(),
}
}
}
@@ -185,15 +204,17 @@
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)]
@@ -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| {
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 @@
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 @@
}
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
@@ -1,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)
}),
)
}
@@ -1,45 +1,47 @@
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))
};
@@ -47,7 +49,7 @@
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 @@
#[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),
}
@@ -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 crate::{theme::Icon, widgets, ActiveContextMenu};
use url::Url;
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) => {
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::OpenLightOptions(name) => Some(Event::OpenLightContextMenu(name)),
Message::UpdateLightAmount(_name, _v) => {
None
}
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))
};
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]),
);
}
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()
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)
}
}
@@ -1,13 +1,16 @@
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 @@
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 @@
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 @@
.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 @@
.width(self.width)
.center_x()
.center_y()
.into()
}
}
pub struct NowPlaying {
album_art: String,
song: String,
artist: String,
loved: bool,
}
#[derive(Default)]
pub struct State {
muted: bool,
@@ -218,6 +204,20 @@
Active,
Inactive,
}
impl svg::StyleSheet for Style {
type Style = Theme;
@@ -1,28 +1,28 @@
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 @@
}
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)
@@ -19,6 +19,7 @@
use crate::oracle::Oracle;
#[allow(clippy::module_name_repetitions)]
pub struct WeatherCard<M> {
pub on_click: Option<M>,
pub oracle: Arc<Oracle>,