From 7000c41cba743674f237979415a5a5f98042b180 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Wed, 1 Nov 2023 01:31:48 +0000 Subject: [PATCH] Read currently playing track from home assistant --- 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(-) create mode 100644 shalom/src/subscriptions.rs 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>, @@ -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>, #[serde(borrow)] pub configuration_url: Option>, - #[serde(borrow)] + #[serde(borrow, default)] pub config_entries: Vec>, #[serde(borrow)] - pub connections: Vec<(Cow<'a, str>, Cow<'a, str>)>, + pub connections: Vec>>, #[serde(borrow)] pub disabled_by: Option>, #[serde(borrow)] @@ -227,16 +232,16 @@ pub mod responses { pub hw_version: Option>, #[serde(borrow)] pub id: Cow<'a, str>, + #[serde(borrow, default)] + pub identifiers: Vec>>, #[serde(borrow)] - pub identifiers: Vec<(Cow<'a, str>, Cow<'a, str>)>, - #[serde(borrow)] - pub manufacturer: Cow<'a, str>, + pub manufacturer: Option>, #[serde(borrow)] pub model: Option>, #[serde(borrow)] pub name_by_user: Option>, #[serde(borrow)] - pub name: Cow<'a, str>, + pub name: Option>, #[serde(borrow)] pub sw_version: Option>, #[serde(borrow)] @@ -251,7 +256,7 @@ pub mod responses { #[serde(borrow)] pub area_id: Option>, #[serde(borrow)] - pub config_entry_id: Cow<'a, str>, + pub config_entry_id: Option>, #[serde(borrow)] pub device_id: Option>, #[serde(borrow)] @@ -270,9 +275,9 @@ pub mod responses { #[serde(borrow)] pub name: Option>, #[serde(borrow)] - pub original_name: Cow<'a, str>, + pub original_name: Option>, #[serde(borrow)] - pub platform: Cow<'a, str>, + pub platform: Option>, #[serde(borrow)] pub translation_key: Option>, #[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>, + pub source_list: Vec>, #[serde(borrow, default)] - group_members: Vec>, - volume_level: Option, - is_volume_muted: Option, + pub group_members: Vec>, + pub volume_level: Option, + pub is_volume_muted: Option, + #[serde(borrow)] + pub media_content_id: Option>, #[serde(borrow)] - media_content_id: Option>, + pub media_content_type: Option>, + pub media_duration: Option, + pub media_position: Option, + pub media_title: Option>, + pub media_artist: Option>, + pub media_album_name: Option>, #[serde(borrow)] - media_content_type: Option>, + pub source: Option>, + pub shuffle: Option, #[serde(borrow)] - source: Option>, - shuffle: Option, + pub repeat: Option>, + pub queue_position: Option, + pub queue_size: Option, #[serde(borrow)] - repeat: Option>, - queue_position: Option, - queue_size: Option, + pub device_class: Option>, #[serde(borrow)] - device_class: Option>, + pub friendly_name: Option>, #[serde(borrow)] - friendly_name: Option>, + pub entity_picture: Option>, + } + + #[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, - homepage: ActivePage, oracle: Option>, } @@ -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 { - 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> { 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 { + 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), 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, 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::>(HassRequestKind::AreaRegistry), + hass_client.request::>(HassRequestKind::DeviceRegistry), + hass_client.request::>(HassRequestKind::EntityRegistry), hass_client.request::>(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::>>(); + + 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::::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::::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 + '_ { - self.rooms.values() + pub fn rooms(&self) -> impl Iterator + '_ { + 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, + pub media_duration: Option, + pub media_position: Option, + pub media_title: Option>, + pub media_artist: Option>, + pub media_album_name: Option>, + pub shuffle: bool, + pub repeat: Box, + pub entity_picture: Option, +} + +#[derive(Debug)] +pub struct MediaPlayerTv {} + +#[derive(Debug, Clone)] pub struct Room { pub name: Intern, + pub entities: Vec>, + pub speaker_id: Option>, +} + +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 { +#[derive(Debug)] +pub struct Omni { oracle: Arc, - open_page: fn(ActivePage) -> M, } -impl Omni { - pub fn new(oracle: Arc, open_page: fn(ActivePage) -> M) -> Self { - Self { oracle, open_page } +impl Omni { + pub fn new(oracle: Arc) -> Self { + Self { oracle } } } -impl Component for Omni { - type State = State; - type Event = Event; - - fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option { +impl Omni { + #[allow( + clippy::unnecessary_wraps, + clippy::needless_pass_by_value, + clippy::unused_self + )] + pub fn update(&mut self, event: Message) -> Option { 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 Component for Omni { 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> for Element<'a, M, Renderer> -where - M: 'a + Clone, -{ - fn from(card: Omni) -> 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 { - name: &'static str, - open_context_menu: fn(ActiveContextMenu) -> M, +#[derive(Debug)] +pub struct Room { + room: crate::oracle::Room, + speaker: Option, + now_playing_image: Option, } -impl Room { - 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 Component for Room { - type State = State; - type Event = Event; - fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { + pub fn update(&mut self, event: Message) -> Option { 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 { + 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> for Element<'a, M, Renderer> -where - M: 'a + Clone, -{ - fn from(card: Room) -> 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( + id: I, + url: Url, + resp: fn(Url, image::Handle) -> M, +) -> Subscription { + static CACHE: Lazy>> = + 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 { pub on_click: Option, pub oracle: Arc, 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() -> MediaPlayer { - MediaPlayer::default() +pub fn media_player(device: MediaPlayerSpeaker, image: Option) -> MediaPlayer { + MediaPlayer { + height: Length::Shrink, + width: Length::Fill, + device, + image, + _on_something: None, + } } +#[derive(Clone)] pub struct MediaPlayer { height: Length, width: Length, - now_playing: NowPlaying, - on_something: Option, - track_length: Duration, -} - -impl Default for MediaPlayer { - 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, + _on_something: Option, } impl Component for MediaPlayer { @@ -80,9 +72,9 @@ impl Component for MediaPlayer { 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 Component for MediaPlayer { .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 Component for MediaPlayer { .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) -> TrackCard { TrackCard { artist, song, - loved, + image, } } pub struct TrackCard { artist: String, song: String, - loved: bool, + image: Option, } impl Component for TrackCard { @@ -34,10 +34,20 @@ impl Component 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) -- libgit2 1.7.2