1786105678
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
|
*.old
|
||||||
|
|||||||
220
Cargo.lock
generated
@@ -614,6 +614,7 @@ dependencies = [
|
|||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"serde",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
@@ -702,6 +703,35 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"document-features",
|
||||||
|
"idna",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "copypasta"
|
name = "copypasta"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -898,6 +928,7 @@ dependencies = [
|
|||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"embedded-graphics",
|
"embedded-graphics",
|
||||||
"embedded-graphics-core",
|
"embedded-graphics-core",
|
||||||
@@ -905,10 +936,12 @@ dependencies = [
|
|||||||
"framebuffer",
|
"framebuffer",
|
||||||
"hass-rs",
|
"hass-rs",
|
||||||
"linfb",
|
"linfb",
|
||||||
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"slint",
|
"slint",
|
||||||
"slint-build",
|
"slint-build",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"ureq",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -923,6 +956,12 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_builder"
|
name = "derive_builder"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -1028,6 +1067,15 @@ dependencies = [
|
|||||||
"libloading",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dotenvy"
|
name = "dotenvy"
|
||||||
version = "0.15.7"
|
version = "0.15.7"
|
||||||
@@ -1593,6 +1641,17 @@ dependencies = [
|
|||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -2679,6 +2738,12 @@ version = "0.8.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@@ -2983,6 +3048,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -3654,6 +3725,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -4054,6 +4131,20 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if 1.0.4",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rowan"
|
name = "rowan"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -4135,6 +4226,41 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -4571,6 +4697,12 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "svgtypes"
|
name = "svgtypes"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
@@ -4751,6 +4883,36 @@ dependencies = [
|
|||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
@@ -5082,12 +5244,49 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unty"
|
name = "unty"
|
||||||
version = "0.0.4"
|
version = "0.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"cookie_store",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ureq-proto",
|
||||||
|
"utf8-zero",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq-proto"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -5128,6 +5327,12 @@ dependencies = [
|
|||||||
"xmlwriter",
|
"xmlwriter",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-zero"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -5307,6 +5512,15 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -5949,6 +6163,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
chrono = { version = "0.4.45", features = ["serde"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
embedded-graphics = "0.8.2"
|
embedded-graphics = "0.8.2"
|
||||||
embedded-graphics-core = "0.4.1"
|
embedded-graphics-core = "0.4.1"
|
||||||
@@ -11,9 +12,11 @@ evdev = "0.13.2"
|
|||||||
framebuffer = "0.3.1"
|
framebuffer = "0.3.1"
|
||||||
hass-rs = "0.5.0"
|
hass-rs = "0.5.0"
|
||||||
linfb = { version = "0.2.1", default-features = false }
|
linfb = { version = "0.2.1", default-features = false }
|
||||||
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
serde_json = "1.0.151"
|
serde_json = "1.0.151"
|
||||||
slint = { version = "1.17.1", default-features = false, features = ["std", "compat-1-2", "libm", "renderer-software", "unsafe-single-threaded"] }
|
slint = { version = "1.17.1", default-features = false, features = ["std", "compat-1-2", "libm", "renderer-software", "unsafe-single-threaded"] }
|
||||||
tokio = { version = "1.53.1", features = ["full"] }
|
tokio = { version = "1.53.1", features = ["full"] }
|
||||||
|
ureq = { version = "3.3.0", default-features = false, features = ["json", "rustls"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
dev = ["slint/backend-winit-x11"]
|
dev = ["slint/backend-winit-x11"]
|
||||||
|
|||||||
412
src/ha.rs
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
use std::{collections::HashMap, time::Duration};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local, NaiveDate};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
use crate::state::{DashboardSnapshot, LIGHTS, THERMOSTATS};
|
||||||
|
|
||||||
|
const REQUEST_TIMEOUT: Duration = Duration::from_secs(12);
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct HaClient {
|
||||||
|
pub agent: ureq::Agent,
|
||||||
|
pub base_url: String,
|
||||||
|
pub token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaState {
|
||||||
|
pub entity_id: String,
|
||||||
|
pub state: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub attributes: HaAttributes,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
pub struct HaAttributes {
|
||||||
|
pub current_temperature: Option<f64>,
|
||||||
|
pub temperature: Option<f64>,
|
||||||
|
pub hvac_action: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub hvac_modes: Vec<String>,
|
||||||
|
pub fan_mode: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub fan_modes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaForecastServiceResponse {
|
||||||
|
pub service_response: HashMap<String, HaForecast>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaForecast {
|
||||||
|
#[serde(default)]
|
||||||
|
pub forecast: Vec<HaForecastItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaForecastItem {
|
||||||
|
pub temperature: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaCalendar {
|
||||||
|
pub entity_id: String,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaCalendarEvent {
|
||||||
|
pub summary: String,
|
||||||
|
pub start: HaCalendarTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct HaCalendarTime {
|
||||||
|
#[serde(rename = "dateTime")]
|
||||||
|
pub date_time: Option<String>,
|
||||||
|
pub date: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct ClimateState {
|
||||||
|
pub entity_id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub current: Option<f64>,
|
||||||
|
pub target: Option<f64>,
|
||||||
|
pub mode: String,
|
||||||
|
pub action: String,
|
||||||
|
pub available: bool,
|
||||||
|
pub supports_heat: bool,
|
||||||
|
pub supports_cool: bool,
|
||||||
|
pub fan_mode: String,
|
||||||
|
pub fan_modes: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct LightState {
|
||||||
|
pub entity_id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub on: bool,
|
||||||
|
pub available: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct CalendarEvent {
|
||||||
|
pub summary: String,
|
||||||
|
pub date: String,
|
||||||
|
pub time: String,
|
||||||
|
pub calendar: String,
|
||||||
|
pub sort_epoch: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct BatteryState {
|
||||||
|
pub level: u8,
|
||||||
|
pub kind: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HaClient {
|
||||||
|
pub fn from_environment() -> Result<Self, String> {
|
||||||
|
let base_url = std::env::var("HASS_URL")
|
||||||
|
.map_err(|_| "HASS_URL is not configured".to_string())?
|
||||||
|
.trim_end_matches('/')
|
||||||
|
.to_string();
|
||||||
|
let token =
|
||||||
|
std::env::var("HASS_TOKEN").map_err(|_| "HASS_TOKEN is not configured".to_string())?;
|
||||||
|
let agent = ureq::Agent::new_with_config(
|
||||||
|
ureq::config::Config::builder()
|
||||||
|
.timeout_send_request(Some(REQUEST_TIMEOUT))
|
||||||
|
.timeout_recv_response(Some(REQUEST_TIMEOUT))
|
||||||
|
.timeout_send_body(Some(REQUEST_TIMEOUT))
|
||||||
|
.build(),
|
||||||
|
);
|
||||||
|
Ok(Self {
|
||||||
|
agent,
|
||||||
|
base_url,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_states(&self) -> Result<Vec<HaState>, String> {
|
||||||
|
self.agent
|
||||||
|
.get(&format!("{}/api/states", self.base_url))
|
||||||
|
.header("Authorization", &format!("Bearer {}", self.token))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.call()
|
||||||
|
.map_err(|error| format!("states: {error}"))?
|
||||||
|
.body_mut()
|
||||||
|
.read_json()
|
||||||
|
.map_err(|error| format!("states: invalid response: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_calendars(&self) -> Result<Vec<HaCalendar>, String> {
|
||||||
|
self.agent
|
||||||
|
.get(&format!("{}/api/calendars", self.base_url))
|
||||||
|
.header("Authorization", &format!("Bearer {}", self.token))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.call()
|
||||||
|
.map_err(|error| format!("calendars: {error}"))?
|
||||||
|
.body_mut()
|
||||||
|
.read_json()
|
||||||
|
.map_err(|error| format!("calendars: invalid response: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_calendar_events(
|
||||||
|
&self,
|
||||||
|
entity_id: &str,
|
||||||
|
start: &str,
|
||||||
|
end: &str,
|
||||||
|
) -> Result<Vec<HaCalendarEvent>, String> {
|
||||||
|
self.agent
|
||||||
|
.get(&format!("{}/api/calendars/{entity_id}", self.base_url))
|
||||||
|
.query("start", start)
|
||||||
|
.query("end", end)
|
||||||
|
.header("Authorization", &format!("Bearer {}", self.token))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.call()
|
||||||
|
.map_err(|error| format!("{entity_id}: {error}"))?
|
||||||
|
.body_mut()
|
||||||
|
.read_json()
|
||||||
|
.map_err(|error| format!("{entity_id}: invalid response: {error}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_calendar_events(&self) -> Result<Vec<CalendarEvent>, String> {
|
||||||
|
let now = Local::now();
|
||||||
|
let start = now.to_rfc3339();
|
||||||
|
let end = (now + chrono::Duration::days(30)).to_rfc3339();
|
||||||
|
let mut upcoming = Vec::new();
|
||||||
|
|
||||||
|
for calendar in self.get_calendars()? {
|
||||||
|
let Ok(events) = self.get_calendar_events(&calendar.entity_id, &start, &end) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for event in events {
|
||||||
|
let Some((sort_epoch, date, time)) = format_calendar_time(&event.start) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
upcoming.push(CalendarEvent {
|
||||||
|
summary: event.summary,
|
||||||
|
date,
|
||||||
|
time,
|
||||||
|
calendar: calendar.name.clone(),
|
||||||
|
sort_epoch,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
upcoming.sort_by_key(|event| event.sort_epoch);
|
||||||
|
upcoming.truncate(7);
|
||||||
|
Ok(upcoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn call_service(
|
||||||
|
&self,
|
||||||
|
domain: &str,
|
||||||
|
service: &str,
|
||||||
|
body: serde_json::Value,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
self.agent
|
||||||
|
.post(&format!(
|
||||||
|
"{}/api/services/{domain}/{service}",
|
||||||
|
self.base_url
|
||||||
|
))
|
||||||
|
.header("Authorization", &format!("Bearer {}", self.token))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send_json(body)
|
||||||
|
.map_err(|error| format!("{domain}.{service}: {error}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_daily_high(&self, entity_id: &str) -> Result<Option<f64>, String> {
|
||||||
|
let response: HaForecastServiceResponse = self
|
||||||
|
.agent
|
||||||
|
.post(&format!(
|
||||||
|
"{}/api/services/weather/get_forecasts?return_response",
|
||||||
|
self.base_url
|
||||||
|
))
|
||||||
|
.header("Authorization", &format!("Bearer {}", self.token))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.send_json(json!({ "entity_id": entity_id, "type": "daily" }))
|
||||||
|
.map_err(|error| format!("weather.get_forecasts: {error}"))?
|
||||||
|
.body_mut()
|
||||||
|
.read_json()
|
||||||
|
.map_err(|error| format!("weather.get_forecasts: invalid response: {error}"))?;
|
||||||
|
|
||||||
|
Ok(response
|
||||||
|
.service_response
|
||||||
|
.get(entity_id)
|
||||||
|
.and_then(|weather| weather.forecast.first())
|
||||||
|
.and_then(|forecast| forecast.temperature))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_temperature(&self, entity_id: &str, temperature: f64) -> Result<(), String> {
|
||||||
|
self.call_service(
|
||||||
|
"climate",
|
||||||
|
"set_temperature",
|
||||||
|
json!({ "entity_id": entity_id, "temperature": temperature }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_mode(&self, entity_id: &str, mode: &str) -> Result<(), String> {
|
||||||
|
self.call_service(
|
||||||
|
"climate",
|
||||||
|
"set_hvac_mode",
|
||||||
|
json!({ "entity_id": entity_id, "hvac_mode": mode }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_fan_mode(&self, entity_id: &str, fan_mode: &str) -> Result<(), String> {
|
||||||
|
self.call_service(
|
||||||
|
"climate",
|
||||||
|
"set_fan_mode",
|
||||||
|
json!({ "entity_id": entity_id, "fan_mode": fan_mode }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_light(&self, entity_id: &str, on: bool) -> Result<(), String> {
|
||||||
|
self.call_service(
|
||||||
|
"light",
|
||||||
|
if on { "turn_on" } else { "turn_off" },
|
||||||
|
json!({ "entity_id": entity_id }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fetch_snapshot(&self) -> Result<DashboardSnapshot, String> {
|
||||||
|
let states: HashMap<String, HaState> = self
|
||||||
|
.get_all_states()?
|
||||||
|
.into_iter()
|
||||||
|
.map(|state| (state.entity_id.clone(), state))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let weather = states
|
||||||
|
.get("weather.forecast_home")
|
||||||
|
.or_else(|| states.get("weather.kmco"))
|
||||||
|
.ok_or_else(|| "weather entity unavailable".to_string())?;
|
||||||
|
let is_night = states
|
||||||
|
.get("sun.sun")
|
||||||
|
.is_some_and(|sun| sun.state == "below_horizon");
|
||||||
|
let outside_high = self
|
||||||
|
.get_daily_high(&weather.entity_id)
|
||||||
|
.unwrap_or_else(|error| {
|
||||||
|
eprintln!("Daily forecast refresh failed: {error}");
|
||||||
|
None
|
||||||
|
});
|
||||||
|
|
||||||
|
let thermostats = THERMOSTATS
|
||||||
|
.iter()
|
||||||
|
.map(|(entity_id, name)| match states.get(*entity_id) {
|
||||||
|
Some(state) if state.state != "unavailable" && state.state != "unknown" => {
|
||||||
|
let action = state
|
||||||
|
.attributes
|
||||||
|
.hvac_action
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| state.state.clone());
|
||||||
|
ClimateState {
|
||||||
|
entity_id: (*entity_id).into(),
|
||||||
|
name: (*name).into(),
|
||||||
|
current: state.attributes.current_temperature,
|
||||||
|
target: state.attributes.temperature,
|
||||||
|
mode: state.state.clone(),
|
||||||
|
action,
|
||||||
|
available: true,
|
||||||
|
supports_heat: state
|
||||||
|
.attributes
|
||||||
|
.hvac_modes
|
||||||
|
.iter()
|
||||||
|
.any(|mode| mode == "heat"),
|
||||||
|
supports_cool: state
|
||||||
|
.attributes
|
||||||
|
.hvac_modes
|
||||||
|
.iter()
|
||||||
|
.any(|mode| mode == "cool"),
|
||||||
|
fan_mode: state.attributes.fan_mode.clone().unwrap_or_default(),
|
||||||
|
fan_modes: state.attributes.fan_modes.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ClimateState {
|
||||||
|
entity_id: (*entity_id).into(),
|
||||||
|
name: (*name).into(),
|
||||||
|
current: None,
|
||||||
|
target: None,
|
||||||
|
mode: "unavailable".into(),
|
||||||
|
action: "unavailable".into(),
|
||||||
|
available: false,
|
||||||
|
supports_heat: false,
|
||||||
|
supports_cool: false,
|
||||||
|
fan_mode: String::new(),
|
||||||
|
fan_modes: Vec::new(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let lights = LIGHTS
|
||||||
|
.iter()
|
||||||
|
.map(|(entity_id, name)| match states.get(*entity_id) {
|
||||||
|
Some(state) if state.state != "unavailable" && state.state != "unknown" => {
|
||||||
|
LightState {
|
||||||
|
entity_id: (*entity_id).into(),
|
||||||
|
name: (*name).into(),
|
||||||
|
on: state.state == "on",
|
||||||
|
available: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => LightState {
|
||||||
|
entity_id: (*entity_id).into(),
|
||||||
|
name: (*name).into(),
|
||||||
|
on: false,
|
||||||
|
available: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let calendar_events = self.fetch_calendar_events().unwrap_or_else(|error| {
|
||||||
|
eprintln!("Calendar refresh failed: {error}");
|
||||||
|
Vec::new()
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(DashboardSnapshot {
|
||||||
|
outside_temperature: weather.attributes.temperature,
|
||||||
|
outside_high,
|
||||||
|
outside_condition: weather.state.clone(),
|
||||||
|
weather_kind: weather_kind(&weather.state, is_night).into(),
|
||||||
|
thermostats,
|
||||||
|
lights,
|
||||||
|
calendar_events,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_calendar_time(value: &HaCalendarTime) -> Option<(i64, String, String)> {
|
||||||
|
if let Some(date_time) = value.date_time.as_deref() {
|
||||||
|
let parsed = DateTime::parse_from_rfc3339(date_time).ok()?;
|
||||||
|
return Some((
|
||||||
|
parsed.timestamp(),
|
||||||
|
parsed.format("%a %b %-d").to_string().to_uppercase(),
|
||||||
|
parsed.format("%-I:%M %p").to_string().to_uppercase(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let date = NaiveDate::parse_from_str(value.date.as_deref()?, "%Y-%m-%d").ok()?;
|
||||||
|
let sort_epoch = date.and_hms_opt(0, 0, 0)?.and_utc().timestamp();
|
||||||
|
Some((
|
||||||
|
sort_epoch,
|
||||||
|
date.format("%a %b %-d").to_string().to_uppercase(),
|
||||||
|
"ALL DAY".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn weather_kind(condition: &str, is_night: bool) -> &'static str {
|
||||||
|
match condition {
|
||||||
|
"sunny" => "sun",
|
||||||
|
"clear-night" => "moon",
|
||||||
|
"partlycloudy" if is_night => "cloud-moon",
|
||||||
|
"partlycloudy" => "cloud-sun",
|
||||||
|
"rainy" => "cloud-rain",
|
||||||
|
"pouring" | "windy" | "windy-variant" => "cloud-rain-wind",
|
||||||
|
"lightning" | "lightning-rainy" => "cloud-lightning",
|
||||||
|
"snowy" | "snowy-rainy" | "hail" => "snowflake",
|
||||||
|
"fog" => "cloud-fog",
|
||||||
|
_ => "cloud",
|
||||||
|
}
|
||||||
|
}
|
||||||
479
src/main.rs
@@ -1,193 +1,338 @@
|
|||||||
use hass_rs::{HassClient, HassEntity};
|
|
||||||
use slint::{ModelRc, VecModel};
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
env::var,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
thread::sleep,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
slint::include_modules!();
|
slint::include_modules!();
|
||||||
|
|
||||||
mod rgba;
|
mod ha;
|
||||||
#[cfg(not(feature = "dev"))]
|
mod state;
|
||||||
mod trekstor;
|
mod trekstor;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
// #[derive(Clone, Debug, PartialEq)]
|
use std::sync::{Arc, Mutex};
|
||||||
// struct LightState {
|
|
||||||
// entity_id: String,
|
|
||||||
// name: String,
|
|
||||||
// on: bool,
|
|
||||||
// available: bool,
|
|
||||||
// }
|
|
||||||
|
|
||||||
struct Database {
|
use chrono::Local;
|
||||||
client: HassClient,
|
|
||||||
entities: HashMap<String, HassEntity>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Database {
|
use crate::{ha::HaClient, state::DashboardSnapshot, utils::*};
|
||||||
pub async fn update_light(&mut self, light: &str, checked: bool) {
|
|
||||||
self.client
|
|
||||||
.call_service(
|
|
||||||
"light".to_string(),
|
|
||||||
if checked {
|
|
||||||
"turn_on".to_string()
|
|
||||||
} else {
|
|
||||||
"turn_off".to_string()
|
|
||||||
},
|
|
||||||
Some(serde_json::json!({
|
|
||||||
"entity_id": light.to_string()
|
|
||||||
})),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_lights(&self) -> Vec<LightData> {
|
|
||||||
let mut hidden = vec![];
|
|
||||||
let lights = self
|
|
||||||
.entities
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(id, e)| {
|
|
||||||
if id.starts_with("light.") {
|
|
||||||
if let Some(children) = e.attributes["entity_id"].as_array() {
|
|
||||||
for child in children {
|
|
||||||
hidden.push(child.as_str().unwrap().to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Some(LightData {
|
|
||||||
available: e.state != "unavailable",
|
|
||||||
entity_id: e.entity_id.clone().into(),
|
|
||||||
name: e.attributes["friendly_name"].as_str().unwrap().into(),
|
|
||||||
on: e.state == "on",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut lights = lights
|
|
||||||
.iter()
|
|
||||||
.filter(|e| {
|
|
||||||
!e.entity_id.ends_with("screen") && !hidden.contains(&e.entity_id.to_string())
|
|
||||||
})
|
|
||||||
.map(|e| e.to_owned())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
lights.sort_by(|a, b| a.name.cmp(&b.name));
|
|
||||||
lights
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type DB = Arc<Mutex<Database>>;
|
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), slint::PlatformError> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
#[cfg(not(feature = "dev"))]
|
#[cfg(not(feature = "dev"))]
|
||||||
slint::platform::set_platform(Box::new(crate::trekstor::Trekstor::new()))
|
slint::platform::set_platform(Box::new(crate::trekstor::Trekstor::new()))
|
||||||
.expect("set platform");
|
.expect("set platform");
|
||||||
|
|
||||||
let main_window = MainWindow::new()?;
|
let app = AppWindow::new()?;
|
||||||
|
app.on_quit(|| std::process::exit(0));
|
||||||
|
|
||||||
// start home_assistant webscoket
|
let state = Arc::new(Mutex::new(DashboardSnapshot::loading()));
|
||||||
let db = home_assisstant_websocket_client().await;
|
apply_snapshot(&app, &state.lock().expect("dashboard state poisoned"), None);
|
||||||
|
|
||||||
let db_weak = db.clone();
|
app.on_select_room({
|
||||||
let weak_window = main_window.as_weak();
|
let weak = app.as_weak();
|
||||||
main_window.on_toggle_light(move |light, checked| {
|
let state = state.clone();
|
||||||
let db_weaker = db_weak.clone();
|
move |index| {
|
||||||
slint::spawn_local(async move {
|
let Some(app) = weak.upgrade() else { return };
|
||||||
db_weaker
|
app.set_selected_index(index);
|
||||||
.lock()
|
let snapshot = state.lock().expect("dashboard state poisoned");
|
||||||
.unwrap()
|
apply_selected(&app, &snapshot);
|
||||||
.update_light(&light.entity_id.to_string(), checked)
|
app.set_action_status("USE - / + OR SELECT A MODE".into());
|
||||||
.await;
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let lights = db_weak.lock().unwrap().get_lights();
|
|
||||||
weak_window
|
|
||||||
.upgrade_in_event_loop(move |ui| {
|
|
||||||
ui.set_lights(ModelRc::new(VecModel::from(lights)));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
// println!("{} {}", entity_id, checked);
|
|
||||||
});
|
|
||||||
|
|
||||||
let weak_window = main_window.as_weak();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let lights = db.lock().unwrap().get_lights();
|
|
||||||
weak_window
|
|
||||||
.upgrade_in_event_loop(move |ui| {
|
|
||||||
ui.set_lights(ModelRc::new(VecModel::from(lights)));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
sleep(Duration::from_secs(1));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
main_window.run()
|
match HaClient::from_environment() {
|
||||||
|
Ok(client) => {
|
||||||
|
app.on_change_target({
|
||||||
|
let weak = app.as_weak();
|
||||||
|
let state = state.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
move |delta| {
|
||||||
|
let Some(app) = weak.upgrade() else { return };
|
||||||
|
let index = app.get_selected_index().max(0) as usize;
|
||||||
|
let (entity_id, target, snapshot) = {
|
||||||
|
let mut snapshot = state.lock().expect("dashboard state poisoned");
|
||||||
|
let Some(thermostat) = snapshot.thermostats.get_mut(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !thermostat.available {
|
||||||
|
app.set_action_status("THERMOSTAT UNAVAILABLE".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = thermostat.target.or(thermostat.current).unwrap_or(68.0)
|
||||||
|
+ f64::from(delta);
|
||||||
|
thermostat.target = Some(target);
|
||||||
|
(thermostat.entity_id.clone(), target, snapshot.clone())
|
||||||
|
};
|
||||||
|
apply_snapshot(&app, &snapshot, None);
|
||||||
|
app.set_action_status("SAVING SET POINT".into());
|
||||||
|
|
||||||
|
let weak = weak.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
std::thread::spawn(move || match client.set_temperature(&entity_id, target) {
|
||||||
|
Ok(()) => set_action_status(&weak, "SET POINT SAVED".into()),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Set temperature failed: {error}");
|
||||||
|
set_action_status(&weak, "SET POINT FAILED".into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on_set_mode({
|
||||||
|
let weak = app.as_weak();
|
||||||
|
let state = state.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
move |mode| {
|
||||||
|
let Some(app) = weak.upgrade() else { return };
|
||||||
|
let index = app.get_selected_index().max(0) as usize;
|
||||||
|
let (entity_id, mode_text, snapshot) = {
|
||||||
|
let mut snapshot = state.lock().expect("dashboard state poisoned");
|
||||||
|
let Some(thermostat) = snapshot.thermostats.get_mut(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !thermostat.available {
|
||||||
|
app.set_action_status("THERMOSTAT UNAVAILABLE".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mode_text = mode.to_string();
|
||||||
|
let supported = mode_text == "off"
|
||||||
|
|| (mode_text == "heat" && thermostat.supports_heat)
|
||||||
|
|| (mode_text == "cool" && thermostat.supports_cool);
|
||||||
|
if !supported {
|
||||||
|
app.set_action_status("MODE NOT SUPPORTED".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thermostat.mode = mode_text.clone();
|
||||||
|
thermostat.action = mode_text.clone();
|
||||||
|
(thermostat.entity_id.clone(), mode_text, snapshot.clone())
|
||||||
|
};
|
||||||
|
apply_snapshot(&app, &snapshot, None);
|
||||||
|
app.set_action_status("SAVING MODE".into());
|
||||||
|
|
||||||
|
let weak = weak.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
std::thread::spawn(move || match client.set_mode(&entity_id, &mode_text) {
|
||||||
|
Ok(()) => set_action_status(&weak, "MODE SAVED".into()),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Set mode failed: {error}");
|
||||||
|
set_action_status(&weak, "MODE FAILED".into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on_set_fan_mode({
|
||||||
|
let weak = app.as_weak();
|
||||||
|
let state = state.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
move |requested_mode| {
|
||||||
|
let Some(app) = weak.upgrade() else { return };
|
||||||
|
let index = app.get_selected_index().max(0) as usize;
|
||||||
|
let (entity_id, fan_mode, snapshot) = {
|
||||||
|
let mut snapshot = state.lock().expect("dashboard state poisoned");
|
||||||
|
let Some(thermostat) = snapshot.thermostats.get_mut(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !thermostat.available {
|
||||||
|
app.set_action_status("THERMOSTAT UNAVAILABLE".into());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some(fan_mode) = thermostat
|
||||||
|
.fan_modes
|
||||||
|
.iter()
|
||||||
|
.find(|mode| mode.eq_ignore_ascii_case(requested_mode.as_str()))
|
||||||
|
.cloned()
|
||||||
|
else {
|
||||||
|
app.set_action_status("FAN SPEED NOT SUPPORTED".into());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
thermostat.fan_mode = fan_mode.clone();
|
||||||
|
(thermostat.entity_id.clone(), fan_mode, snapshot.clone())
|
||||||
|
};
|
||||||
|
apply_snapshot(&app, &snapshot, None);
|
||||||
|
app.set_action_status("SAVING FAN SPEED".into());
|
||||||
|
|
||||||
|
let weak = weak.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
std::thread::spawn(move || match client.set_fan_mode(&entity_id, &fan_mode) {
|
||||||
|
Ok(()) => set_action_status(&weak, "FAN SPEED SAVED".into()),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Set fan mode failed: {error}");
|
||||||
|
set_action_status(&weak, "FAN SPEED FAILED".into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on_toggle_light({
|
||||||
|
let weak = app.as_weak();
|
||||||
|
let state = state.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
move |index| {
|
||||||
|
let index = index.max(0) as usize;
|
||||||
|
let (entity_id, turn_on, snapshot) = {
|
||||||
|
let mut snapshot = state.lock().expect("dashboard state poisoned");
|
||||||
|
let Some(light) = snapshot.lights.get_mut(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !light.available {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
light.on = !light.on;
|
||||||
|
(light.entity_id.clone(), light.on, snapshot.clone())
|
||||||
|
};
|
||||||
|
if let Some(app) = weak.upgrade() {
|
||||||
|
apply_snapshot(&app, &snapshot, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let weak = weak.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
std::thread::spawn(move || match client.set_light(&entity_id, turn_on) {
|
||||||
|
Ok(()) => set_action_status(&weak, "LIGHT UPDATED".into()),
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Set light failed: {error}");
|
||||||
|
set_action_status(&weak, "LIGHT UPDATE FAILED".into());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on_refresh({
|
||||||
|
let weak = app.as_weak();
|
||||||
|
let state = state.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
move || {
|
||||||
|
let Some(app) = weak.upgrade() else { return };
|
||||||
|
app.set_action_status("REFRESHING".into());
|
||||||
|
let weak = weak.clone();
|
||||||
|
let state = state.clone();
|
||||||
|
let client = client.clone();
|
||||||
|
std::thread::spawn(move || fetch_once(&client, &state, &weak));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
spawn_poll_loop(client, state, app.as_weak());
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("Home Assistant configuration error: {error}");
|
||||||
|
app.set_action_status("HOME ASSISTANT CONFIG MISSING".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.run()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo: add channel to send actions to HA, maybe on the db object? prolly not, right?
|
fn spawn_poll_loop(
|
||||||
async fn home_assisstant_websocket_client() -> DB {
|
client: HaClient,
|
||||||
let hass_ws_url = var("HASS_WS_URL").expect("HASS_WS_URL not set in environment");
|
state: Arc<Mutex<DashboardSnapshot>>,
|
||||||
let hass_token = var("HASS_TOKEN").expect("HA_TOKEN not set in environment");
|
weak: slint::Weak<AppWindow>,
|
||||||
|
) {
|
||||||
let mut client = HassClient::new(&hass_ws_url)
|
std::thread::spawn(move || {
|
||||||
.await
|
loop {
|
||||||
.expect("Failed to connect");
|
fetch_once(&client, &state, &weak);
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
client
|
}
|
||||||
.auth_with_longlivedtoken(&hass_token)
|
|
||||||
.await
|
|
||||||
.expect("Not able to authenticate");
|
|
||||||
|
|
||||||
let db = Arc::new(Mutex::new(Database {
|
|
||||||
client,
|
|
||||||
entities: Default::default(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
let mut event_receiver = db
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.client
|
|
||||||
.subscribe_event("state_changed")
|
|
||||||
.await
|
|
||||||
.expect("Failed to subscribe");
|
|
||||||
|
|
||||||
let states = db
|
|
||||||
.lock()
|
|
||||||
.unwrap()
|
|
||||||
.client
|
|
||||||
.get_states()
|
|
||||||
.await
|
|
||||||
.expect("unable to get states");
|
|
||||||
states.iter().for_each(|entity| {
|
|
||||||
db.lock()
|
|
||||||
.expect("Database poisoned")
|
|
||||||
.entities
|
|
||||||
.insert(entity.entity_id.clone(), entity.clone());
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let db_weak = db.clone();
|
fn fetch_once(
|
||||||
tokio::spawn(async move {
|
client: &HaClient,
|
||||||
while let Some(message) = event_receiver.recv().await {
|
state: &Arc<Mutex<DashboardSnapshot>>,
|
||||||
if let Some(entity) = &message.event.data.new_state {
|
weak: &slint::Weak<AppWindow>,
|
||||||
println!("{}", entity);
|
) {
|
||||||
db_weak
|
match client.fetch_snapshot() {
|
||||||
.lock()
|
Ok(snapshot) => {
|
||||||
.expect("Database poisoned")
|
let changed = {
|
||||||
.entities
|
let mut current = state.lock().expect("dashboard state poisoned");
|
||||||
.insert(entity.entity_id.clone(), entity.clone());
|
if *current == snapshot {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
*current = snapshot.clone();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if changed {
|
||||||
|
let weak = weak.clone();
|
||||||
|
let updated = format!("UPDATED {}", Local::now().format("%-I:%M %p"));
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(app) = weak.upgrade() {
|
||||||
|
apply_snapshot(&app, &snapshot, Some(updated));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
Err(error) => {
|
||||||
|
eprintln!("Home Assistant refresh failed: {error}");
|
||||||
db
|
set_action_status(weak, "HOME ASSISTANT UNAVAILABLE".into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_selected(app: &AppWindow, snapshot: &DashboardSnapshot) {
|
||||||
|
let index = app.get_selected_index().max(0) as usize;
|
||||||
|
let Some(thermostat) = snapshot.thermostats.get(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
app.set_selected_name(thermostat.name.as_str().into());
|
||||||
|
app.set_selected_target(format_temperature(thermostat.target).into());
|
||||||
|
app.set_selected_mode(pretty_label(&thermostat.mode).to_uppercase().into());
|
||||||
|
app.set_selected_supports_heat(thermostat.supports_heat);
|
||||||
|
app.set_selected_supports_cool(thermostat.supports_cool);
|
||||||
|
app.set_selected_supports_fan(!thermostat.fan_modes.is_empty());
|
||||||
|
app.set_selected_fan_mode(pretty_label(&thermostat.fan_mode).to_uppercase().into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_snapshot(app: &AppWindow, snapshot: &DashboardSnapshot, updated_text: Option<String>) {
|
||||||
|
app.set_outside_temperature(format_temperature(snapshot.outside_temperature).into());
|
||||||
|
app.set_outside_high(
|
||||||
|
snapshot
|
||||||
|
.outside_high
|
||||||
|
.map(|high| format!("HIGH {}", format_temperature(Some(high))))
|
||||||
|
.unwrap_or_else(|| "HIGH --".into())
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
app.set_outside_condition(
|
||||||
|
pretty_label(&snapshot.outside_condition)
|
||||||
|
.to_uppercase()
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
app.set_weather_kind(snapshot.weather_kind.as_str().into());
|
||||||
|
// app.set_living_fan_on(snapshot.living_fan.on);
|
||||||
|
// app.set_living_fan_available(snapshot.living_fan.available);
|
||||||
|
if let Some(event) = snapshot.calendar_events.first() {
|
||||||
|
app.set_next_event_label(
|
||||||
|
format!("NEXT / {} / {}", event.date, event.calendar.to_uppercase()).into(),
|
||||||
|
);
|
||||||
|
app.set_next_event_title(event.summary.as_str().into());
|
||||||
|
app.set_next_event_time(event.time.as_str().into());
|
||||||
|
} else {
|
||||||
|
app.set_next_event_label("NEXT EVENT".into());
|
||||||
|
app.set_next_event_title("NO UPCOMING EVENTS".into());
|
||||||
|
app.set_next_event_time("".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
app.set_thermostats(snapshot.into());
|
||||||
|
app.set_lights(snapshot.into());
|
||||||
|
app.set_calendar_events(snapshot.into());
|
||||||
|
let on_count = snapshot.lights.iter().filter(|light| light.on).count();
|
||||||
|
let available = snapshot
|
||||||
|
.lights
|
||||||
|
.iter()
|
||||||
|
.filter(|light| light.available)
|
||||||
|
.count();
|
||||||
|
app.set_lights_summary(format!("{on_count} ON / {available} AVAILABLE").into());
|
||||||
|
apply_selected(app, snapshot);
|
||||||
|
|
||||||
|
if let Some(updated_text) = updated_text {
|
||||||
|
app.set_updated_text(updated_text.clone().into());
|
||||||
|
app.set_action_status(updated_text.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_action_status(weak: &slint::Weak<AppWindow>, message: String) {
|
||||||
|
let weak = weak.clone();
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(app) = weak.upgrade() {
|
||||||
|
app.set_action_status(message.into());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
112
src/state.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use slint::{ModelRc, SharedString, VecModel};
|
||||||
|
|
||||||
|
use crate::{CalendarData, LightData, ThermostatData, ha::*, utils::*};
|
||||||
|
|
||||||
|
pub const THERMOSTATS: [(&str, &str); 1] = [("climate.thermostat", "Thermostat")];
|
||||||
|
|
||||||
|
pub const LIGHTS: [(&str, &str); 8] = [
|
||||||
|
("light.armory_light", "Armory"),
|
||||||
|
("light.bedroom_light", "Bedroom"),
|
||||||
|
("light.hallway_light", "Hallway"),
|
||||||
|
("light.kitchen_light", "Kitchen"),
|
||||||
|
("light.living_room", "Living Room"),
|
||||||
|
("light.shower_light", "Shower"),
|
||||||
|
("light.utilities", "Utilities"),
|
||||||
|
("light.light.wc_light", "WC"),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct DashboardSnapshot {
|
||||||
|
pub outside_temperature: Option<f64>,
|
||||||
|
pub outside_high: Option<f64>,
|
||||||
|
pub outside_condition: String,
|
||||||
|
pub weather_kind: String,
|
||||||
|
pub thermostats: Vec<ClimateState>,
|
||||||
|
pub lights: Vec<LightState>,
|
||||||
|
pub calendar_events: Vec<CalendarEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DashboardSnapshot {
|
||||||
|
pub fn loading() -> Self {
|
||||||
|
Self {
|
||||||
|
outside_temperature: None,
|
||||||
|
outside_high: None,
|
||||||
|
outside_condition: "Connecting".into(),
|
||||||
|
weather_kind: "cloud".into(),
|
||||||
|
thermostats: THERMOSTATS
|
||||||
|
.iter()
|
||||||
|
.map(|(entity_id, name)| ClimateState {
|
||||||
|
entity_id: (*entity_id).into(),
|
||||||
|
name: (*name).into(),
|
||||||
|
current: None,
|
||||||
|
target: None,
|
||||||
|
mode: "unknown".into(),
|
||||||
|
action: "unknown".into(),
|
||||||
|
available: false,
|
||||||
|
supports_heat: false,
|
||||||
|
supports_cool: false,
|
||||||
|
fan_mode: String::new(),
|
||||||
|
fan_modes: Vec::new(),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
lights: LIGHTS
|
||||||
|
.iter()
|
||||||
|
.map(|(entity_id, name)| LightState {
|
||||||
|
entity_id: (*entity_id).into(),
|
||||||
|
name: (*name).into(),
|
||||||
|
on: false,
|
||||||
|
available: false,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
calendar_events: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&DashboardSnapshot> for ModelRc<ThermostatData> {
|
||||||
|
fn from(value: &DashboardSnapshot) -> Self {
|
||||||
|
let rows = value
|
||||||
|
.thermostats
|
||||||
|
.iter()
|
||||||
|
.map(|thermostat| ThermostatData {
|
||||||
|
name: SharedString::from(thermostat.name.as_str()),
|
||||||
|
current: SharedString::from(format_temperature(thermostat.current)),
|
||||||
|
target: SharedString::from(format_temperature(thermostat.target)),
|
||||||
|
state: SharedString::from(pretty_label(&thermostat.action).to_uppercase()),
|
||||||
|
available: thermostat.available,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ModelRc::new(VecModel::from(rows))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&DashboardSnapshot> for ModelRc<LightData> {
|
||||||
|
fn from(value: &DashboardSnapshot) -> Self {
|
||||||
|
let rows = value
|
||||||
|
.lights
|
||||||
|
.iter()
|
||||||
|
.map(|light| LightData {
|
||||||
|
name: SharedString::from(light.name.as_str()),
|
||||||
|
on: light.on,
|
||||||
|
available: light.available,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ModelRc::new(VecModel::from(rows))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&DashboardSnapshot> for ModelRc<CalendarData> {
|
||||||
|
fn from(value: &DashboardSnapshot) -> Self {
|
||||||
|
let rows = value
|
||||||
|
.calendar_events
|
||||||
|
.iter()
|
||||||
|
.map(|event| CalendarData {
|
||||||
|
date: SharedString::from(event.date.as_str()),
|
||||||
|
time: SharedString::from(event.time.as_str()),
|
||||||
|
summary: SharedString::from(event.summary.as_str()),
|
||||||
|
calendar: SharedString::from(event.calendar.as_str()),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
ModelRc::new(VecModel::from(rows))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
mod input;
|
||||||
|
mod rgba;
|
||||||
|
mod screen;
|
||||||
|
|
||||||
|
pub use rgba::Rgba;
|
||||||
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
@@ -11,9 +17,6 @@ use slint::{EventLoopError, LogicalPosition, PhysicalSize, Rgb8Pixel, WindowSize
|
|||||||
use self::input::InputState;
|
use self::input::InputState;
|
||||||
use self::screen::Screen;
|
use self::screen::Screen;
|
||||||
|
|
||||||
mod input;
|
|
||||||
mod screen;
|
|
||||||
|
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub screen: Screen,
|
pub screen: Screen,
|
||||||
pub input: InputState,
|
pub input: InputState,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle};
|
use embedded_graphics::{pixelcolor::Rgb888, prelude::*, primitives::Rectangle};
|
||||||
use linfb::{Framebuffer, shape::Color};
|
use linfb::{Framebuffer, shape::Color};
|
||||||
|
|
||||||
use crate::rgba::Rgba;
|
use crate::trekstor::Rgba;
|
||||||
|
|
||||||
pub struct Screen {
|
pub struct Screen {
|
||||||
pub framebuffer: Framebuffer,
|
pub framebuffer: Framebuffer,
|
||||||
|
|||||||
24
src/utils.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
pub fn pretty_label(value: &str) -> String {
|
||||||
|
let value = match value {
|
||||||
|
"partlycloudy" => "partly cloudy",
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
value
|
||||||
|
.split(['_', '-', ' '])
|
||||||
|
.filter(|word| !word.is_empty())
|
||||||
|
.map(|word| {
|
||||||
|
let mut chars = word.chars();
|
||||||
|
match chars.next() {
|
||||||
|
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format_temperature(value: Option<f64>) -> String {
|
||||||
|
value
|
||||||
|
.map(|temperature| format!("{temperature:.0}°"))
|
||||||
|
.unwrap_or_else(|| "--".into())
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import {
|
|
||||||
MaterialText,
|
|
||||||
OutlinedCard,
|
|
||||||
MaterialTypography,
|
|
||||||
MaterialStyleMetrics
|
|
||||||
} from "../material.slint";
|
|
||||||
|
|
||||||
export component ComponentCard {
|
|
||||||
in property <string> title;
|
|
||||||
|
|
||||||
HorizontalLayout {
|
|
||||||
alignment: center;
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
spacing: 4px;
|
|
||||||
alignment: start;
|
|
||||||
|
|
||||||
MaterialText {
|
|
||||||
horizontal_alignment: center;
|
|
||||||
text: root.title;
|
|
||||||
style: MaterialTypography.title_medium;
|
|
||||||
}
|
|
||||||
|
|
||||||
OutlinedCard {
|
|
||||||
min-width: 360px;
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
padding_left: 2px;
|
|
||||||
padding_right: self.padding_left;
|
|
||||||
padding_top: MaterialStyleMetrics.padding_8;
|
|
||||||
padding_bottom: self.padding_top;
|
|
||||||
spacing: MaterialStyleMetrics.spacing_8;
|
|
||||||
|
|
||||||
@children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import {
|
|
||||||
MaterialText,
|
|
||||||
MaterialPalette,
|
|
||||||
MaterialTypography
|
|
||||||
} from "../material.slint";
|
|
||||||
|
|
||||||
export component Group {
|
|
||||||
in property <string> title;
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
border_radius: 12px;
|
|
||||||
background: MaterialPalette.surface_bright;
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
alignment: start;
|
|
||||||
padding: 8px;
|
|
||||||
spacing: 16px;
|
|
||||||
|
|
||||||
MaterialText {
|
|
||||||
horizontal_alignment: center;
|
|
||||||
text: root.title;
|
|
||||||
style: MaterialTypography.headline_small;
|
|
||||||
}
|
|
||||||
|
|
||||||
@children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
MaterialText
|
|
||||||
} from "../material.slint";
|
|
||||||
|
|
||||||
export component TextIconButton {
|
|
||||||
in property <image> icon <=> button.icon;
|
|
||||||
in property <string> text <=> label.text;
|
|
||||||
|
|
||||||
VerticalLayout {
|
|
||||||
HorizontalLayout {
|
|
||||||
alignment: center;
|
|
||||||
|
|
||||||
button := IconButton {}
|
|
||||||
}
|
|
||||||
|
|
||||||
label := MaterialText {
|
|
||||||
horizontal_alignment: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
SPDX-FileCopyrightText: Google Inc. <https://fonts.google.com/specimen/Roboto/about>
|
|
||||||
|
|
||||||
SPDX-License-Identifier: Apache-2.0
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
|
||||||
// SPDX-License-Identifier: MIT
|
|
||||||
|
|
||||||
export global FilledIcons {
|
|
||||||
out property <image> account_box: @image-url("./icons/filled/account_box.svg");
|
|
||||||
out property <image> calendar_month: @image-url("./icons/filled/calendar_month.svg");
|
|
||||||
out property <image> close: @image-url("./icons/filled/close.svg");
|
|
||||||
out property <image> delete: @image-url("./icons/filled/delete.svg");
|
|
||||||
out property <image> favorite: @image-url("./icons/filled/favorite.svg");
|
|
||||||
out property <image> inbox: @image-url("./icons/filled/inbox.svg");
|
|
||||||
out property <image> music_note: @image-url("./icons/filled/music_note.svg");
|
|
||||||
out property <image> play_arrow: @image-url("./icons/filled/play_arrow.svg");
|
|
||||||
out property <image> today: @image-url("./icons/filled/today.svg");
|
|
||||||
out property <image> add: @image-url("./icons/filled/add.svg");
|
|
||||||
out property <image> chat_bubble: @image-url("./icons/filled/chat_bubble.svg");
|
|
||||||
out property <image> edit: @image-url("./icons/filled/edit.svg");
|
|
||||||
out property <image> group: @image-url("./icons/filled/group.svg");
|
|
||||||
out property <image> mail: @image-url("./icons/filled/mail.svg");
|
|
||||||
out property <image> outbox: @image-url("./icons/filled/outbox.svg");
|
|
||||||
out property <image> search: @image-url("./icons/filled/search.svg");
|
|
||||||
out property <image> video_chat: @image-url("./icons/filled/video_call.svg");
|
|
||||||
out property <image> bookmark: @image-url("./icons/filled/bookmark.svg");
|
|
||||||
out property <image> check: @image-url("./icons/filled/check.svg");
|
|
||||||
out property <image> explore: @image-url("./icons/filled/explore.svg");
|
|
||||||
out property <image> image: @image-url("./icons/filled/image.svg");
|
|
||||||
out property <image> more_vert: @image-url("./icons/filled/more_vert.svg");
|
|
||||||
out property <image> pets: @image-url("./icons/filled/pets.svg");
|
|
||||||
out property <image> settings: @image-url("./icons/filled/settings.svg");
|
|
||||||
out property <image> view_week: @image-url("./icons/filled/view_week.svg");
|
|
||||||
out property <image> light_mode: @image-url("./icons/filled/light_mode.svg");
|
|
||||||
out property <image> dark_mode: @image-url("./icons/filled/dark_mode.svg");
|
|
||||||
out property <image> dashboard: @image-url("./icons/filled/dashboard.svg");
|
|
||||||
out property <image> pause: @image-url("./icons/filled/pause.svg");
|
|
||||||
out property <image> archive: @image-url("./icons/filled/archive.svg");
|
|
||||||
out property <image> share: @image-url("./icons/filled/share.svg");
|
|
||||||
out property <image> menu: @image-url("./icons/filled/menu.svg");
|
|
||||||
out property <image> account_circle: @image-url("./icons/filled/account_circle.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
export global OutlinedIcons {
|
|
||||||
out property <image> account_box: @image-url("./icons/outlined/account_box.svg");
|
|
||||||
out property <image> calendar_month: @image-url("./icons/outlined/calendar_month.svg");
|
|
||||||
out property <image> delete: @image-url("./icons/outlined/delete.svg");
|
|
||||||
out property <image> favorite: @image-url("./icons/outlined/favorite.svg");
|
|
||||||
out property <image> inbox: @image-url("./icons/outlined/inbox.svg");
|
|
||||||
out property <image> music_note: @image-url("./icons/outlined/music_note.svg");
|
|
||||||
out property <image> play_arrow: @image-url("./icons/outlined/play_arrow.svg");
|
|
||||||
out property <image> today: @image-url("./icons/outlined/today.svg");
|
|
||||||
out property <image> add: @image-url("./icons/outlined/add.svg");
|
|
||||||
out property <image> chat_bubble: @image-url("./icons/outlined/chat_bubble.svg");
|
|
||||||
out property <image> edit: @image-url("./icons/outlined/edit.svg");
|
|
||||||
out property <image> group: @image-url("./icons/outlined/group.svg");
|
|
||||||
out property <image> mail: @image-url("./icons/outlined/mail.svg");
|
|
||||||
out property <image> outbox: @image-url("./icons/outlined/outbox.svg");
|
|
||||||
out property <image> search: @image-url("./icons/outlined/search.svg");
|
|
||||||
out property <image> video_chat: @image-url("./icons/outlined/video_chat.svg");
|
|
||||||
out property <image> bookmark: @image-url("./icons/outlined/bookmark.svg");
|
|
||||||
out property <image> check: @image-url("./icons/outlined/check.svg");
|
|
||||||
out property <image> explore: @image-url("./icons/outlined/explore.svg");
|
|
||||||
out property <image> image: @image-url("./icons/outlined/image.svg");
|
|
||||||
out property <image> more_vert: @image-url("./icons/outlined/more_vert.svg");
|
|
||||||
out property <image> pets: @image-url("./icons/outlined/pets.svg");
|
|
||||||
out property <image> settings: @image-url("./icons/outlined/settings.svg");
|
|
||||||
out property <image> view_week: @image-url("./icons/outlined/view_week.svg");
|
|
||||||
out property <image> light_mode: @image-url("./icons/outlined/light_mode.svg");
|
|
||||||
out property <image> dark_mode: @image-url("./icons/outlined/dark_mode.svg");
|
|
||||||
out property <image> dashboard: @image-url("./icons/outlined/dashboard.svg");
|
|
||||||
out property <image> pause: @image-url("./icons/outlined/pause.svg");
|
|
||||||
out property <image> archive: @image-url("./icons/outlined/archive.svg");
|
|
||||||
out property <image> share: @image-url("./icons/outlined/share.svg");
|
|
||||||
out property <image> schedule: @image-url("./icons/outlined/schedule.svg");
|
|
||||||
out property <image> palette: @image-url("./icons/outlined/palette.svg");
|
|
||||||
}
|
|
||||||
1
ui/icons/NOTICE.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Icons are from Lucide (https://lucide.dev), licensed under the ISC License.
|
||||||
6
ui/icons/battery-charging.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m11 7-3 5h4l-3 5"/>
|
||||||
|
<path d="M14.856 6H16a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.935"/>
|
||||||
|
<path d="M22 14v-4"/>
|
||||||
|
<path d="M5.14 18H4a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h2.936"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
7
ui/icons/battery-full.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 10v4"/>
|
||||||
|
<path d="M14 10v4"/>
|
||||||
|
<path d="M22 14v-4"/>
|
||||||
|
<path d="M6 10v4"/>
|
||||||
|
<rect x="2" y="6" width="16" height="12" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 302 B |
5
ui/icons/battery-low.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 14v-4"/>
|
||||||
|
<path d="M6 14v-4"/>
|
||||||
|
<rect x="2" y="6" width="16" height="12" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 257 B |
6
ui/icons/battery-medium.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M10 14v-4"/>
|
||||||
|
<path d="M22 14v-4"/>
|
||||||
|
<path d="M6 14v-4"/>
|
||||||
|
<rect x="2" y="6" width="16" height="12" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 281 B |
4
ui/icons/battery.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M22 14v-4"/>
|
||||||
|
<rect x="2" y="6" width="16" height="12" rx="2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 234 B |
3
ui/icons/cloud-fog.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M16 17H7"/><path d="M17 21H9"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 269 B |
3
ui/icons/cloud-lightning.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M6 16.326A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 .5 8.973"/><path d="m13 12-3 5h4l-3 5"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 257 B |
3
ui/icons/cloud-moon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M13 16a3 3 0 0 1 0 6H7a5 5 0 1 1 4.9-6z"/><path d="M18.376 14.512a6 6 0 0 0 3.461-4.127c.148-.625-.659-.97-1.248-.714a4 4 0 0 1-5.259-5.26c.255-.589-.09-1.395-.716-1.248a6 6 0 0 0-4.594 5.36"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 363 B |
3
ui/icons/cloud-rain-wind.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="m9.2 22 3-7"/><path d="m9 13-3 7"/><path d="m17 13-3 7"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 295 B |
3
ui/icons/cloud-rain.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M4 14.899A7 7 0 1 1 15.71 8h1.79a4.5 4.5 0 0 1 2.5 8.242"/><path d="M16 14v6"/><path d="M8 14v6"/><path d="M12 16v6"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 288 B |
3
ui/icons/cloud-sun.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 2v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="M20 12h2"/><path d="m19.07 4.93-1.41 1.41"/><path d="M15.947 12.65a4 4 0 0 0-5.925-4.128"/><path d="M13 22H7a5 5 0 1 1 4.9-6H13a3 3 0 0 1 0 6Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 366 B |
3
ui/icons/cloud.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 224 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-7 3c1.93 0 3.5 1.57 3.5 3.5S13.93 13 12 13s-3.5-1.57-3.5-3.5S10.07 6 12 6zm7 13H5v-.23c0-.62.28-1.2.76-1.58C7.47 15.82 9.64 15 12 15s4.53.82 6.24 2.19c.48.38.76.97.76 1.58V19z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 354 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 4c1.93 0 3.5 1.57 3.5 3.5S13.93 13 12 13s-3.5-1.57-3.5-3.5S10.07 6 12 6zm0 14c-2.03 0-4.43-.82-6.14-2.88a9.947 9.947 0 0 1 12.28 0C16.43 19.18 14.03 20 12 20z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 329 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 136 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m20.54 5.23-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5 6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 332 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 160 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2zm-8 4H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 310 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 167 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 150 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 203 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 220 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 162 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 178 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a.996.996 0 0 0 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 246 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 10.9c-.61 0-1.1.49-1.1 1.1s.49 1.1 1.1 1.1c.61 0 1.1-.49 1.1-1.1s-.49-1.1-1.1-1.1zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm2.19 12.19L6 18l3.81-8.19L18 6l-3.81 8.19z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 296 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m12 21.35-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 274 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5c-1.66 0-3 1.34-3 3s1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5C6.34 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 390 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 221 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H4.99c-1.11 0-1.98.89-1.98 2L3 19c0 1.1.88 2 1.99 2H19c1.1 0 2-.9 2-2V5a2 2 0 0 0-2-2zm0 12h-4c0 1.66-1.35 3-3 3s-3-1.34-3-3H4.99V5H19v10z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 244 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 878 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4-8 5-8-5V6l8 5 8-5v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 209 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 147 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 246 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 186 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H4.99c-1.11 0-1.98.9-1.98 2L3 19c0 1.1.88 2 1.99 2H19c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 12h-4c0 1.66-1.35 3-3 3s-3-1.34-3-3H4.99V5H19v10zM8 11h2v3h4v-3h2l-4-4-4 4z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 270 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 132 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 609 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 114 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 303 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.488.488 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 793 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 460 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 231 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4zM14 13h-3v3H9v-3H6v-2h3V8h2v3h3v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 236 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5.33 20H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2h1.33c1.1 0 2 .9 2 2v12a2 2 0 0 1-2 2zM22 18V6c0-1.1-.9-2-2-2h-1.33c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2H20a2 2 0 0 0 2-2zm-7.33 0V6c0-1.1-.9-2-2-2h-1.33c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h1.33c1.1 0 2-.9 2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 346 B |
3
ui/icons/moon.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 283 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 16H7v-.24C8.42 17.62 10.16 17 12 17s3.58.62 5 1.76V19zm2-1.14C17.2 16.09 14.73 15 12 15s-5.2 1.09-7 2.86V5h14v12.86zM12 13c1.93 0 3.5-1.57 3.5-3.5S13.93 6 12 6 8.5 7.57 8.5 9.5 10.07 13 12 13zm0-5c.83 0 1.5.67 1.5 1.5S12.83 11 12 11s-1.5-.67-1.5-1.5S11.17 8 12 8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 444 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 136 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m20.54 5.23-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM6.24 5h11.52l.81.97H5.44l.8-.97zM5 19V8h14v11H5zm8.45-9h-2.9v3H8l4 4 4-4h-2.55z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 346 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 153 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2zM9 14H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2zm-8 4H7v-2h2v2zm4 0h-2v-2h2v2zm4 0h-2v-2h2v2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 325 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 167 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9 16.17 4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 158 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9.37 5.51A7.35 7.35 0 0 0 9.1 7.5c0 4.08 3.32 7.4 7.4 7.4.68 0 1.35-.09 1.99-.27A7.014 7.014 0 0 1 12 19c-3.86 0-7-3.14-7-7 0-2.93 1.81-5.45 4.37-6.49zM12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.389 5.389 0 0 1-4.4 2.26 5.403 5.403 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 372 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 5v2h-4V5h4M9 5v6H5V5h4m10 8v6h-4v-6h4M9 17v2H5v-2h4M21 3h-8v6h8V3zM11 3H3v10h8V3zm10 8h-8v10h8V11zm-10 4H3v6h8v-6z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 219 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 9v10H8V9h8m-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1zM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 194 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06M17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 307 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-5.5-2.5 7.51-3.49L17.5 6.5 9.99 9.99 6.5 17.5zm5.5-6.6c.61 0 1.1.49 1.1 1.1s-.49 1.1-1.1 1.1-1.1-.49-1.1-1.1.49-1.1 1.1-1.1z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 351 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m12 21.35-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 274 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9 13.75c-2.34 0-7 1.17-7 3.5V19h14v-1.75c0-2.33-4.66-3.5-7-3.5zM4.34 17c.84-.58 2.87-1.25 4.66-1.25s3.82.67 4.66 1.25H4.34zM9 12c1.93 0 3.5-1.57 3.5-3.5S10.93 5 9 5 5.5 6.57 5.5 8.5 7.07 12 9 12zm0-5c.83 0 1.5.67 1.5 1.5S9.83 10 9 10s-1.5-.67-1.5-1.5S8.17 7 9 7zm7.04 6.81c1.16.84 1.96 1.96 1.96 3.44V19h4v-1.75c0-2.02-3.5-3.17-5.96-3.44zM15 12c1.93 0 3.5-1.57 3.5-3.5S16.93 5 15 5c-.54 0-1.04.13-1.5.35.63.89 1 1.98 1 3.15s-.37 2.26-1 3.15c.46.22.96.35 1.5.35z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 563 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-4.86 8.86-3 3.87L9 13.14 6 17h12l-3.86-5.14z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 237 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5v-3h3.56c.69 1.19 1.97 2 3.45 2s2.75-.81 3.45-2H19v3zm0-5h-4.99c0 1.1-.9 2-2 2s-2-.9-2-2H5V5h14v9z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 282 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 9c1.65 0 3 1.35 3 3s-1.35 3-3 3-3-1.35-3-3 1.35-3 3-3m0-2c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37a.996.996 0 0 0-1.41 0 .996.996 0 0 0 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0a.996.996 0 0 0 0-1.41l-1.06-1.06zm1.06-10.96a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36a.996.996 0 0 0 0-1.41.996.996 0 0 0-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 934 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M22 6c0-1.1-.9-2-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6zm-2 0-8 4.99L4 6h16zm0 12H4V8l8 5 8-5v10z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 218 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 246 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="m12 3 .01 10.55c-.59-.34-1.27-.55-2-.55a4.001 4.001 0 1 0 0 8c2.22 0 3.99-1.79 3.99-4V7h4V3h-6zm-1.99 16c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 249 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M11 9.83V14h2V9.83l1.59 1.58L16 10l-4-4-4 4 1.41 1.41z"/><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5v-3h3.02c.91 1.21 2.35 2 3.98 2s3.06-.79 3.98-2H19v3zm0-5h-4.18c-.41 1.16-1.51 2-2.82 2s-2.4-.84-2.82-2H5V5h14v9z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 363 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 32.5-156t88-127Q256-817 330-848.5T488-880q80 0 151 27.5t124.5 76q53.5 48.5 85 115T880-518q0 115-70 176.5T640-280h-74q-9 0-12.5 5t-3.5 11q0 12 15 34.5t15 51.5q0 50-27.5 74T480-80Zm0-400Zm-220 40q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm120-160q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm200 0q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17Zm120 160q26 0 43-17t17-43q0-26-17-43t-43-17q-26 0-43 17t-17 43q0 26 17 43t43 17ZM480-160q9 0 14.5-5t5.5-13q0-14-15-33t-15-57q0-42 29-67t71-25h70q66 0 113-38.5T800-518q0-121-92.5-201.5T488-800q-136 0-232 93t-96 227q0 133 93.5 226.5T480-160Z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 850 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 132 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><circle cx="4.5" cy="9.5" r="2.5"/><circle cx="9" cy="5.5" r="2.5"/><circle cx="15" cy="5.5" r="2.5"/><circle cx="19.5" cy="9.5" r="2.5"/><path d="M17.34 14.86c-.87-1.02-1.6-1.89-2.48-2.91-.46-.54-1.05-1.08-1.75-1.32-.11-.04-.22-.07-.33-.09-.25-.04-.52-.04-.78-.04s-.53 0-.79.05c-.11.02-.22.05-.33.09-.7.24-1.28.78-1.75 1.32-.87 1.02-1.6 1.89-2.48 2.91-1.31 1.31-2.92 2.76-2.62 4.79.29 1.02 1.02 2.03 2.33 2.32.73.15 3.06-.44 5.54-.44h.18c2.48 0 4.81.58 5.54.44 1.31-.29 2.04-1.31 2.33-2.32.31-2.04-1.3-3.49-2.61-4.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 609 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 8.64 15.27 12 10 15.36V8.64M8 5v14l11-7L8 5z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 149 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e3e3e3"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 342 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27A6.471 6.471 0 0 0 16 9.5 6.5 6.5 0 1 0 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 303 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19.43 12.98c.04-.32.07-.64.07-.98 0-.34-.03-.66-.07-.98l2.11-1.65c.19-.15.24-.42.12-.64l-2-3.46a.5.5 0 0 0-.61-.22l-2.49 1c-.52-.4-1.08-.73-1.69-.98l-.38-2.65A.488.488 0 0 0 14 2h-4c-.25 0-.46.18-.49.42l-.38 2.65c-.61.25-1.17.59-1.69.98l-2.49-1a.566.566 0 0 0-.18-.03c-.17 0-.34.09-.43.25l-2 3.46c-.13.22-.07.49.12.64l2.11 1.65c-.04.32-.07.65-.07.98 0 .33.03.66.07.98l-2.11 1.65c-.19.15-.24.42-.12.64l2 3.46a.5.5 0 0 0 .61.22l2.49-1c.52.4 1.08.73 1.69.98l.38 2.65c.03.24.24.42.49.42h4c.25 0 .46-.18.49-.42l.38-2.65c.61-.25 1.17-.59 1.69-.98l2.49 1c.06.02.12.03.18.03.17 0 .34-.09.43-.25l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.65zm-1.98-1.71c.04.31.05.52.05.73 0 .21-.02.43-.05.73l-.14 1.13.89.7 1.08.84-.7 1.21-1.27-.51-1.04-.42-.9.68c-.43.32-.84.56-1.25.73l-1.06.43-.16 1.13-.2 1.35h-1.4l-.19-1.35-.16-1.13-1.06-.43c-.43-.18-.83-.41-1.23-.71l-.91-.7-1.06.43-1.27.51-.7-1.21 1.08-.84.89-.7-.14-1.13c-.03-.31-.05-.54-.05-.74s.02-.43.05-.73l.14-1.13-.89-.7-1.08-.84.7-1.21 1.27.51 1.04.42.9-.68c.43-.32.84-.56 1.25-.73l1.06-.43.16-1.13.2-1.35h1.39l.19 1.35.16 1.13 1.06.43c.43.18.83.41 1.23.71l.91.7 1.06-.43 1.27-.51.7 1.21-1.07.85-.89.7.14 1.13zM12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm0 6c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92s2.92-1.31 2.92-2.92c0-1.61-1.31-2.92-2.92-2.92zM18 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM6 13c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1zm12 7.02c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 621 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 3h-1V1h-2v2H8V1H6v2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V9h14v10zm0-12H5V5h14v2zM7 11h5v5H7z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 236 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.17L4 17.17V4h16v12z"/><path d="M8 14h6c.55 0 1-.45 1-1v-1.99L17 13V7l-2 1.99V7c0-.55-.45-1-1-1H8c-.55 0-1 .45-1 1v6c0 .55.45 1 1 1z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 313 B |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM8 18H4V6h4v12zm6 0h-4V6h4v12zm6 0h-4V6h4v12z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 222 B |
6
ui/icons/plug.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 22v-5"/>
|
||||||
|
<path d="M15 8V2"/>
|
||||||
|
<path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/>
|
||||||
|
<path d="M9 8V2"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 309 B |
7
ui/icons/refresh-ccw-dot.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||||
|
<path d="M3 3v5h5"/>
|
||||||
|
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
|
||||||
|
<path d="M16 16h5v5"/>
|
||||||
|
<circle cx="12" cy="12" r="1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 371 B |
3
ui/icons/snowflake.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m10 20-1.25-2.5L6 18"/><path d="M10 4 8.75 6.5 6 6"/><path d="m14 20 1.25-2.5L18 18"/><path d="m14 4 1.25 2.5L18 6"/><path d="m17 21-3-6h-4"/><path d="m17 3-3 6 1.5 3"/><path d="M2 12h6.5L10 9"/><path d="m20 10-1.5 2 1.5 2"/><path d="M22 12h-6.5L14 15"/><path d="m4 10 1.5 2L4 14"/><path d="m7 21 3-6-1.5-3"/><path d="m7 3 3 6h4"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 501 B |
3
ui/icons/sun.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 402 B |
627
ui/main.slint
@@ -1,56 +1,333 @@
|
|||||||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
export struct ThermostatData {
|
||||||
// SPDX-License-Identifier: MIT
|
name: string,
|
||||||
|
current: string,
|
||||||
import { LineEdit } from "std-widgets.slint";
|
target: string,
|
||||||
import { ComponentCard } from "./components/component_card.slint";
|
state: string,
|
||||||
|
available: bool,
|
||||||
import { Horizontal, Vertical, Switch } from "./material.slint";
|
}
|
||||||
|
|
||||||
import "./fonts/Roboto-VariableFont.ttf";
|
|
||||||
import { FilledIcons, OutlinedIcons } from "./icons.slint";
|
|
||||||
|
|
||||||
export struct LightData {
|
export struct LightData {
|
||||||
name: string,
|
name: string,
|
||||||
entity_id: string,
|
|
||||||
on: bool,
|
on: bool,
|
||||||
available: bool,
|
available: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
component LightSwitch inherits Horizontal {
|
export struct CalendarData {
|
||||||
in property <LightData> light;
|
date: string,
|
||||||
callback toggled(LightData, bool);
|
time: string,
|
||||||
|
summary: string,
|
||||||
|
calendar: string,
|
||||||
|
}
|
||||||
|
|
||||||
alignment: end;
|
component ControlButton inherits Rectangle {
|
||||||
label := Text {
|
in property <string> label;
|
||||||
text: root.light.name;
|
in property <bool> active: false;
|
||||||
vertical-alignment: center;
|
in property <bool> enabled: true;
|
||||||
horizontal-alignment: right;
|
callback activated();
|
||||||
font-size: 24px;
|
|
||||||
font-weight: root.light.on ? 700 : 500;
|
border-width: 2px;
|
||||||
|
border-color: black;
|
||||||
|
background: root.enabled && (touch.pressed || root.active) ? black : white;
|
||||||
|
|
||||||
|
touch := TouchArea {
|
||||||
|
clicked => { if root.enabled { root.activated(); } }
|
||||||
}
|
}
|
||||||
switch := Switch {
|
|
||||||
enabled: root.light.available;
|
Text {
|
||||||
checked: root.light.on;
|
text: root.label;
|
||||||
checked_state_changed(checked) => { root.toggled(root.light, checked) }
|
color: root.enabled && (touch.pressed || root.active) ? white : black;
|
||||||
|
font-size: root.label == "−" || root.label == "+" ? 48px : 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component NavigationButton inherits Rectangle {
|
||||||
|
in property <string> label;
|
||||||
|
in property <bool> active: false;
|
||||||
|
callback activated();
|
||||||
|
|
||||||
// The window the user interacts with. Edits to the inputs propagate to
|
border-width: 2px;
|
||||||
// the SystemTrayIcon instance via the host language's callbacks.
|
border-color: black;
|
||||||
export component MainWindow inherits Window {
|
background: root.active || touch.pressed ? black : white;
|
||||||
title: "Dashboard";
|
|
||||||
preferred-width: 1024px;
|
|
||||||
preferred-height: 600px;
|
|
||||||
default-font-family: "Roboto";
|
|
||||||
|
|
||||||
|
touch := TouchArea {
|
||||||
|
clicked => { root.activated(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.label;
|
||||||
|
color: root.active || touch.pressed ? white : black;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ScreenRefreshButton inherits Rectangle {
|
||||||
|
callback activated();
|
||||||
|
|
||||||
|
border-width: touch.pressed ? 4px : 2px;
|
||||||
|
border-color: black;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
touch := TouchArea {
|
||||||
|
clicked => { root.activated(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Image {
|
||||||
|
x: 12px;
|
||||||
|
y: 14px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
source: @image-url("icons/refresh-ccw-dot.svg");
|
||||||
|
image-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 48px;
|
||||||
|
width: parent.width - 54px;
|
||||||
|
height: parent.height;
|
||||||
|
text: "CLEAR";
|
||||||
|
color: black;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component ThermostatRow inherits Rectangle {
|
||||||
|
in property <ThermostatData> room;
|
||||||
|
in property <int> row-index;
|
||||||
|
in property <bool> selected: false;
|
||||||
|
callback row-activated(int);
|
||||||
|
|
||||||
|
background: root.selected ? black : white;
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => { root.row-activated(root.row-index); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: 0;
|
||||||
|
y: parent.height - 1px;
|
||||||
|
width: parent.width;
|
||||||
|
height: 1px;
|
||||||
|
background: root.selected ? white : black;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 16px;
|
||||||
|
y: 8px;
|
||||||
|
width: 330px;
|
||||||
|
height: 34px;
|
||||||
|
text: root.room.name;
|
||||||
|
color: root.selected ? white : black;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 17px;
|
||||||
|
y: 46px;
|
||||||
|
width: 330px;
|
||||||
|
height: 20px;
|
||||||
|
text: root.room.available ? root.room.state : "UNAVAILABLE";
|
||||||
|
color: root.selected ? white : black;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 365px;
|
||||||
|
y: 2px;
|
||||||
|
width: 135px;
|
||||||
|
height: 70px;
|
||||||
|
text: root.room.current;
|
||||||
|
color: root.selected ? white : black;
|
||||||
|
font-size: 52px;
|
||||||
|
font-weight: 700;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 530px;
|
||||||
|
y: 5px;
|
||||||
|
width: 136px;
|
||||||
|
height: 66px;
|
||||||
|
text: root.room.target;
|
||||||
|
color: root.selected ? white : black;
|
||||||
|
font-size: 39px;
|
||||||
|
font-weight: 700;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component LightTile inherits Rectangle {
|
||||||
|
in property <LightData> light;
|
||||||
|
in property <int> light-index;
|
||||||
|
callback toggled(int);
|
||||||
|
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: black;
|
||||||
|
background: root.light.on ? black : white;
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => { if root.light.available { root.toggled(root.light-index); } }
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 14px;
|
||||||
|
y: 7px;
|
||||||
|
width: parent.width - 88px;
|
||||||
|
height: 29px;
|
||||||
|
text: root.light.name;
|
||||||
|
color: root.light.on ? white : black;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow: elide;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 14px;
|
||||||
|
y: 37px;
|
||||||
|
width: parent.width - 88px;
|
||||||
|
height: 21px;
|
||||||
|
text: !root.light.available ? "UNAVAILABLE" : root.light.on ? "ON" : "OFF";
|
||||||
|
color: root.light.on ? white : black;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: parent.width - 56px;
|
||||||
|
y: 8px;
|
||||||
|
width: 38px;
|
||||||
|
height: parent.height - 16px;
|
||||||
|
text: root.light.on ? "●" : "○";
|
||||||
|
color: root.light.on ? white : black;
|
||||||
|
font-size: 25px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component CalendarRow inherits Rectangle {
|
||||||
|
in property <CalendarData> event;
|
||||||
|
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: 0;
|
||||||
|
y: parent.height - 2px;
|
||||||
|
width: parent.width;
|
||||||
|
height: 2px;
|
||||||
|
background: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 0;
|
||||||
|
y: 14px;
|
||||||
|
width: 150px;
|
||||||
|
height: 27px;
|
||||||
|
text: root.event.date;
|
||||||
|
color: black;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 0;
|
||||||
|
y: 47px;
|
||||||
|
width: 150px;
|
||||||
|
height: 24px;
|
||||||
|
text: root.event.time;
|
||||||
|
color: black;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 175px;
|
||||||
|
y: 11px;
|
||||||
|
width: parent.width - 175px;
|
||||||
|
height: 39px;
|
||||||
|
text: root.event.summary;
|
||||||
|
color: black;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
overflow: elide;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 175px;
|
||||||
|
y: 55px;
|
||||||
|
width: parent.width - 175px;
|
||||||
|
height: 23px;
|
||||||
|
text: root.event.calendar;
|
||||||
|
color: black;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export component AppWindow inherits Window {
|
||||||
|
width: 1024px;
|
||||||
|
height: 600px;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
in property <[ThermostatData]> thermostats;
|
||||||
in property <[LightData]> lights;
|
in property <[LightData]> lights;
|
||||||
callback toggle-light(LightData, bool);
|
in property <[CalendarData]> calendar-events;
|
||||||
|
|
||||||
in-out property <int> current-page: 0;
|
in-out property <int> current-page: 0;
|
||||||
|
in-out property <int> selected-index: 0;
|
||||||
|
in property <string> selected-name: "--";
|
||||||
|
in property <string> selected-target: "--";
|
||||||
|
in property <string> selected-mode: "--";
|
||||||
|
in property <bool> selected-supports-heat: false;
|
||||||
|
in property <bool> selected-supports-cool: false;
|
||||||
|
in property <bool> selected-supports-fan: false;
|
||||||
|
in property <string> selected-fan-mode: "--";
|
||||||
|
in property <string> outside-temperature: "--";
|
||||||
|
in property <string> outside-high: "HIGH --";
|
||||||
|
in property <string> outside-condition: "CONNECTING";
|
||||||
|
in property <string> weather-kind: "cloud";
|
||||||
in property <string> next-event-label: "NEXT EVENT";
|
in property <string> next-event-label: "NEXT EVENT";
|
||||||
in property <string> next-event-title: "LOADING CALENDAR";
|
in property <string> next-event-title: "LOADING CALENDAR";
|
||||||
in property <string> next-event-time: "6:00 pm";
|
in property <string> next-event-time: "--";
|
||||||
|
in property <string> battery-level: "--%";
|
||||||
|
in property <string> battery-kind: "battery";
|
||||||
|
in property <string> updated-text: "WAITING FOR HOME ASSISTANT";
|
||||||
|
in property <string> action-status: "TAP A ROOM TO CONTROL";
|
||||||
|
in property <string> lights-summary: "LOADING LIGHTS";
|
||||||
|
in property <bool> living-fan-on: false;
|
||||||
|
in property <bool> living-fan-available: false;
|
||||||
|
in-out property <bool> sleeping: false;
|
||||||
|
|
||||||
|
callback select-room(int);
|
||||||
|
callback change-target(int);
|
||||||
|
callback set-mode(string);
|
||||||
|
callback set-fan-mode(string);
|
||||||
|
callback toggle-living-fan();
|
||||||
|
callback toggle-light(int);
|
||||||
|
callback refresh();
|
||||||
|
callback clean-screen();
|
||||||
|
callback user-activity();
|
||||||
|
callback enter-sleep();
|
||||||
|
callback wake-from-sleep();
|
||||||
|
callback quit();
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: 0;
|
x: 0;
|
||||||
@@ -60,28 +337,21 @@ export component MainWindow inherits Window {
|
|||||||
background: black;
|
background: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
x: 0;
|
|
||||||
y: 0;
|
|
||||||
width: parent.width / 2;
|
|
||||||
height: parent.height;
|
|
||||||
background: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
TouchArea {
|
TouchArea {
|
||||||
x: 0;
|
x: 0;
|
||||||
y: 5px;
|
y: 5px;
|
||||||
width: parent.width - 78px;
|
width: parent.width - 78px;
|
||||||
height: 78px;
|
height: 78px;
|
||||||
clicked => { root.current-page = 2; }
|
clicked => { root.user-activity(); root.current-page = 2; }
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
x: 38px;
|
x: 38px;
|
||||||
y: 12px;
|
y: 12px;
|
||||||
width: (parent.width / 2) - (38px * 2) - 100px;
|
width: parent.width - 128px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
text: root.next-event-label;
|
text: root.next-event-label;
|
||||||
|
color: black;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
@@ -89,9 +359,10 @@ export component MainWindow inherits Window {
|
|||||||
Text {
|
Text {
|
||||||
x: 38px;
|
x: 38px;
|
||||||
y: 32px;
|
y: 32px;
|
||||||
width: (parent.width / 2) - (38px * 2) - 100px;
|
width: 390px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
text: root.next-event-title;
|
text: root.next-event-title;
|
||||||
|
color: black;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
overflow: elide;
|
overflow: elide;
|
||||||
@@ -99,40 +370,268 @@ export component MainWindow inherits Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
x: (root.width / 2) - self.width - 38px;
|
x: 438px;
|
||||||
y: 32px;
|
y: 32px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
text: root.next-event-time;
|
text: root.next-event-time;
|
||||||
|
color: black;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
horizontal-alignment: right;
|
horizontal-alignment: right;
|
||||||
vertical-alignment: center;
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
if root.battery-kind == "battery-charging": Image { image-rendering: smooth; x: 552px; y: 34px; width: 34px; height: 30px; source: @image-url("icons/battery-charging.svg"); image-fit: contain; }
|
||||||
x: 0px;
|
if root.battery-kind == "battery-full": Image { image-rendering: smooth; x: 552px; y: 34px; width: 34px; height: 30px; source: @image-url("icons/battery-full.svg"); image-fit: contain; }
|
||||||
y: 32px + 36px + 5px;
|
if root.battery-kind == "battery-medium": Image { image-rendering: smooth; x: 552px; y: 34px; width: 34px; height: 30px; source: @image-url("icons/battery-medium.svg"); image-fit: contain; }
|
||||||
width: (root.width / 2);
|
if root.battery-kind == "battery-low": Image { image-rendering: smooth; x: 552px; y: 34px; width: 34px; height: 30px; source: @image-url("icons/battery-low.svg"); image-fit: contain; }
|
||||||
height: root.height - (32px + 36px + 5px);
|
if root.battery-kind == "battery": Image { image-rendering: smooth; x: 552px; y: 34px; width: 34px; height: 30px; source: @image-url("icons/battery.svg"); image-fit: contain; }
|
||||||
|
if root.battery-kind == "plug": Image { image-rendering: smooth; x: 552px; y: 34px; width: 34px; height: 30px; source: @image-url("icons/plug.svg"); image-fit: contain; }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 590px;
|
||||||
|
y: 32px;
|
||||||
|
width: 66px;
|
||||||
|
height: 36px;
|
||||||
|
text: root.battery-level;
|
||||||
|
color: black;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
horizontal-alignment: right;
|
||||||
|
vertical-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: (root.width / 2);
|
x: parent.width - 66px;
|
||||||
y: 5px;
|
y: 19px;
|
||||||
width: (root.width / 2);
|
width: 44px;
|
||||||
height: root.height - 10px;
|
height: 44px;
|
||||||
|
border-width: 2px;
|
||||||
|
border-color: black;
|
||||||
|
background: close-touch.pressed ? black : white;
|
||||||
|
|
||||||
ComponentCard {
|
close-touch := TouchArea {
|
||||||
title: "Lights";
|
clicked => { root.quit(); }
|
||||||
|
}
|
||||||
|
|
||||||
Vertical {
|
Text {
|
||||||
alignment: start;
|
text: "×";
|
||||||
|
color: close-touch.pressed ? white : black;
|
||||||
|
font-size: 29px;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for light_data in root.lights: LightSwitch {
|
if root.current-page == 0: Rectangle {
|
||||||
light: light_data;
|
x: 0;
|
||||||
toggled(light, checked) => { root.toggle-light(light, checked); }
|
y: 84px;
|
||||||
}
|
width: root.width;
|
||||||
|
height: root.height - 84px;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 38px;
|
||||||
|
y: 10px;
|
||||||
|
width: 180px;
|
||||||
|
height: 21px;
|
||||||
|
text: "OUTSIDE";
|
||||||
|
color: black;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 33px;
|
||||||
|
y: 28px;
|
||||||
|
width: 300px;
|
||||||
|
height: 116px;
|
||||||
|
text: root.outside-temperature;
|
||||||
|
color: black;
|
||||||
|
font-size: 104px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 260px;
|
||||||
|
y: 101px;
|
||||||
|
width: 150px;
|
||||||
|
height: 32px;
|
||||||
|
text: root.outside-high;
|
||||||
|
color: black;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.weather-kind == "sun": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/sun.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud-sun": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud-sun.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud-moon": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud-moon.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud-rain": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud-rain.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud-rain-wind": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud-rain-wind.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud-lightning": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud-lightning.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "snowflake": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/snowflake.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "cloud-fog": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/cloud-fog.svg"); image-fit: contain; }
|
||||||
|
if root.weather-kind == "moon": Image { image-rendering: smooth; x: 520px; y: 20px; width: 120px; height: 105px; source: @image-url("icons/moon.svg"); image-fit: contain; }
|
||||||
|
|
||||||
|
Text {
|
||||||
|
x: 392px;
|
||||||
|
y: 126px;
|
||||||
|
width: parent.width - 430px;
|
||||||
|
height: 22px;
|
||||||
|
text: root.outside-condition;
|
||||||
|
color: black;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
horizontal-alignment: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { x: 38px; y: 151px; width: parent.width - 76px; height: 3px; background: black; }
|
||||||
|
|
||||||
|
Text { x: 38px; y: 173px; width: 260px; height: 24px; text: "THERMOSTATS"; color: black; font-size: 17px; font-weight: 700; letter-spacing: 2px; }
|
||||||
|
Text { x: 403px; y: 175px; width: 135px; height: 20px; text: "NOW"; color: black; font-size: 14px; font-weight: 700; horizontal-alignment: center; }
|
||||||
|
Text { x: 568px; y: 175px; width: 136px; height: 20px; text: "SET"; color: black; font-size: 14px; font-weight: 700; horizontal-alignment: center; }
|
||||||
|
Rectangle { x: 38px; y: 205px; width: parent.width - 76px; height: 1px; background: black; }
|
||||||
|
|
||||||
|
for room[index] in root.thermostats: ThermostatRow {
|
||||||
|
x: 38px;
|
||||||
|
y: 206px + index * 80px;
|
||||||
|
width: root.width - 76px;
|
||||||
|
height: 80px;
|
||||||
|
room: room;
|
||||||
|
row-index: index;
|
||||||
|
selected: root.selected-index == index;
|
||||||
|
row-activated(index) => { root.user-activity(); root.select-room(index); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle { x: 38px; y: 627px; width: parent.width - 76px; height: 4px; background: black; }
|
||||||
|
Text { x: 38px; y: 642px; width: 280px; height: 22px; text: root.selected-name + " / " + root.selected-mode; color: black; font-size: 15px; font-weight: 700; overflow: elide; }
|
||||||
|
Text { x: 34px; y: 661px; width: 190px; height: 78px; text: root.selected-target; color: black; font-size: 62px; font-weight: 700; vertical-alignment: center; }
|
||||||
|
|
||||||
|
ControlButton { x: 236px; y: 662px; width: 78px; height: 78px; label: "−"; activated => { root.user-activity(); root.change-target(-1); } }
|
||||||
|
ControlButton { x: 324px; y: 662px; width: 78px; height: 78px; label: "+"; activated => { root.user-activity(); root.change-target(1); } }
|
||||||
|
ControlButton { x: 412px; y: 662px; width: 92px; height: 78px; label: "HEAT"; enabled: root.selected-supports-heat; active: root.selected-mode == "HEAT" || root.selected-mode == "HEATING"; activated => { root.user-activity(); root.set-mode("heat"); } }
|
||||||
|
ControlButton { x: 514px; y: 662px; width: 92px; height: 78px; label: "COOL"; enabled: root.selected-supports-cool; active: root.selected-mode == "COOL" || root.selected-mode == "COOLING"; activated => { root.user-activity(); root.set-mode("cool"); } }
|
||||||
|
ControlButton { x: 616px; y: 662px; width: 104px; height: 78px; label: "OFF"; active: root.selected-mode == "OFF"; activated => { root.user-activity(); root.set-mode("off"); } }
|
||||||
|
|
||||||
|
Text { x: 38px; y: 754px; width: 92px; height: 54px; text: "FAN\nSPEED"; color: black; font-size: 14px; font-weight: 700; vertical-alignment: center; }
|
||||||
|
ControlButton { x: 138px; y: 752px; width: 76px; height: 58px; label: "AUTO"; enabled: root.selected-supports-fan; active: root.selected-fan-mode == "AUTO"; activated => { root.user-activity(); root.set-fan-mode("Auto"); } }
|
||||||
|
ControlButton { x: 222px; y: 752px; width: 76px; height: 58px; label: "LOW"; enabled: root.selected-supports-fan; active: root.selected-fan-mode == "LOW"; activated => { root.user-activity(); root.set-fan-mode("Low"); } }
|
||||||
|
ControlButton { x: 306px; y: 752px; width: 76px; height: 58px; label: "MED"; enabled: root.selected-supports-fan; active: root.selected-fan-mode == "MED"; activated => { root.user-activity(); root.set-fan-mode("Med"); } }
|
||||||
|
ControlButton { x: 390px; y: 752px; width: 76px; height: 58px; label: "HIGH"; enabled: root.selected-supports-fan; active: root.selected-fan-mode == "HIGH"; activated => { root.user-activity(); root.set-fan-mode("High"); } }
|
||||||
|
ControlButton { x: 482px; y: 752px; width: 238px; height: 58px; label: root.living-fan-available ? root.living-fan-on ? "LIVING FAN ON" : "LIVING FAN OFF" : "LIVING FAN --"; enabled: root.living-fan-available; active: root.living-fan-on; activated => { root.user-activity(); root.toggle-living-fan(); } }
|
||||||
|
|
||||||
|
Text { x: 38px; y: 822px; width: 540px; height: 24px; text: root.action-status; color: black; font-size: 15px; font-weight: 700; overflow: elide; }
|
||||||
|
Rectangle {
|
||||||
|
x: parent.width - 152px; y: 814px; width: 114px; height: 40px; border-width: 2px; border-color: black; background: refresh-touch.pressed ? black : white;
|
||||||
|
refresh-touch := TouchArea { clicked => { root.user-activity(); root.refresh(); } }
|
||||||
|
Text { text: "REFRESH"; color: refresh-touch.pressed ? white : black; font-size: 14px; font-weight: 700; horizontal-alignment: center; vertical-alignment: center; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.current-page == 1: Rectangle {
|
||||||
|
x: 0;
|
||||||
|
y: 84px;
|
||||||
|
width: root.width;
|
||||||
|
height: root.height - 84px;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
Text { x: 38px; y: 18px; width: 230px; height: 30px; text: "LIGHTS"; color: black; font-size: 22px; font-weight: 700; letter-spacing: 2px; }
|
||||||
|
Text { x: 330px; y: 21px; width: 390px; height: 24px; text: root.lights-summary; color: black; font-size: 15px; font-weight: 700; horizontal-alignment: right; }
|
||||||
|
Rectangle { x: 38px; y: 55px; width: parent.width - 76px; height: 3px; background: black; }
|
||||||
|
|
||||||
|
for light[index] in root.lights: LightTile {
|
||||||
|
// x: index < 10 ? 38px : 390px;
|
||||||
|
// y: 72px + (index < 10 ? index : index - 10) * 74px;
|
||||||
|
x: Math.mod(index, 2) == 0 ? 38px : 390px;
|
||||||
|
y: 72px + Math.floor(index / 2) * 74px;
|
||||||
|
width: 330px;
|
||||||
|
height: 66px;
|
||||||
|
light: light;
|
||||||
|
light-index: index;
|
||||||
|
toggled(index) => { root.user-activity(); root.toggle-light(index); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.current-page == 2: Rectangle {
|
||||||
|
x: 0;
|
||||||
|
y: 84px;
|
||||||
|
width: root.width;
|
||||||
|
height: root.height - 84px;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
Text { x: 38px; y: 18px; width: 260px; height: 30px; text: "CALENDAR"; color: black; font-size: 22px; font-weight: 700; letter-spacing: 2px; }
|
||||||
|
Text { x: 400px; y: 21px; width: 320px; height: 24px; text: "NEXT 30 DAYS"; color: black; font-size: 15px; font-weight: 700; horizontal-alignment: right; }
|
||||||
|
Rectangle { x: 38px; y: 55px; width: parent.width - 76px; height: 3px; background: black; }
|
||||||
|
|
||||||
|
for event[index] in root.calendar-events: CalendarRow {
|
||||||
|
x: 38px;
|
||||||
|
y: 68px + index * 110px;
|
||||||
|
width: root.width - 76px;
|
||||||
|
height: 110px;
|
||||||
|
event: event;
|
||||||
|
}
|
||||||
|
|
||||||
|
if root.calendar-events.length == 0: Text {
|
||||||
|
x: 38px; y: 105px; width: parent.width - 76px; height: 50px; text: "NO UPCOMING EVENTS"; color: black; font-size: 24px; font-weight: 700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationButton { x: 38px; y: parent.height - 80px; width: 168px; height: 60px; label: "CLIMATE"; active: root.current-page == 0; activated => { root.user-activity(); root.current-page = 0; } }
|
||||||
|
NavigationButton { x: 222px; y: parent.height - 80px; width: 168px; height: 60px; label: "LIGHTS"; active: root.current-page == 1; activated => { root.user-activity(); root.current-page = 1; } }
|
||||||
|
NavigationButton { x: 406px; y: parent.height - 80px; width: 168px; height: 60px; label: "CALENDAR"; active: root.current-page == 2; activated => { root.user-activity(); root.current-page = 2; } }
|
||||||
|
ScreenRefreshButton { x: 608px; y: parent.height - 80px; width: 112px; height: 60px; activated => { root.user-activity(); root.clean-screen(); } }
|
||||||
|
|
||||||
|
if root.sleeping: Rectangle {
|
||||||
|
x: 0;
|
||||||
|
y: 0;
|
||||||
|
width: root.width;
|
||||||
|
height: root.height;
|
||||||
|
background: #eeeeee;
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
x: 129px;
|
||||||
|
y: 400px;
|
||||||
|
width: 500px;
|
||||||
|
height: 224px;
|
||||||
|
background: white;
|
||||||
|
border-width: 3px;
|
||||||
|
border-color: black;
|
||||||
|
|
||||||
|
Text {
|
||||||
|
y: 48px;
|
||||||
|
width: parent.width;
|
||||||
|
height: 70px;
|
||||||
|
text: "DISPLAY RESTING";
|
||||||
|
color: black;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 800;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
y: 124px;
|
||||||
|
width: parent.width;
|
||||||
|
height: 48px;
|
||||||
|
text: "TAP TO WAKE";
|
||||||
|
color: black;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
horizontal-alignment: center;
|
||||||
|
vertical-alignment: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TouchArea {
|
||||||
|
clicked => {
|
||||||
|
root.sleeping = false;
|
||||||
|
root.wake-from-sleep();
|
||||||
|
root.user-activity();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||