Initial Commit

This commit is contained in:
2022-07-29 01:36:18 +02:00
commit e7baf561bd
32 changed files with 4394 additions and 0 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
Dockerfile
target*

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
target*
dist
config.toml

1913
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

12
Cargo.toml Normal file
View File

@ -0,0 +1,12 @@
[workspace]
members = ["backend", "common", "frontend"]
[profile.dev]
# Issue with const-generics
incremental = false
[profile.release]
lto = true
opt-level = 's'
# Issue with const-generics
incremental = false

73
Dockerfile Normal file
View File

@ -0,0 +1,73 @@
##################
### BASE STAGE ###
##################
FROM rust:1.62.1 as base
# Install build dependencies
RUN cargo install --locked cargo-make trunk strip_cargo_version
RUN rustup target add wasm32-unknown-unknown
RUN rustup target add x86_64-unknown-linux-musl
# required by "ring"
RUN apt-get update && apt-get install -y musl-tools
WORKDIR /app
RUN mkdir frontend backend common
###########################
### STRIP-VERSION STAGE ###
###########################
FROM base AS strip-version
COPY Cargo.lock Cargo.toml ./
COPY frontend/Cargo.toml ./frontend/
COPY backend/Cargo.toml ./backend/
COPY common/Cargo.toml ./common/
RUN strip_cargo_version
###################
### BUILD STAGE ###
###################
FROM base AS build
RUN cargo init --lib frontend
RUN cargo init --bin backend
RUN cargo init --lib common
COPY --from=strip-version /app/frontend/Cargo.toml /app/frontend/
COPY --from=strip-version /app/backend/Cargo.toml /app/backend/
COPY --from=strip-version /app/common/Cargo.toml /app/common/
COPY --from=strip-version /app/Cargo.toml /app/Cargo.lock /app/
WORKDIR /app/backend
RUN cargo build --release --target x86_64-unknown-linux-musl
WORKDIR /app/frontend
RUN cargo build --release --target wasm32-unknown-unknown
WORKDIR /app
COPY . .
WORKDIR /app/backend
RUN cargo build --release --target x86_64-unknown-linux-musl
RUN strip /app/target/x86_64-unknown-linux-musl/release/hemma
WORKDIR /app/frontend
RUN trunk build --release
########################
### PRODUCTION STAGE ###
########################
FROM scratch
ENV RUST_LOG="info"
WORKDIR /
# Copy application binary
COPY --from=build /app/target/x86_64-unknown-linux-musl/release/hemma /
# Copy static web files
COPY --from=build /app/frontend/dist /www
CMD ["/hemma", "--bind", "0.0.0.0:8000", "--frontend", "/www/"]

31
backend/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "hemma"
version = "0.1.0"
edition = "2021"
[dependencies]
futures-util = "0.3.21"
log = "0.4.17"
pretty_env_logger = "0.4.0"
tokio = { version = "1.19.2", features = ["full"] }
warp = "0.3.2"
ron = "0.7.1"
reqwest = { version = "0.11.11", default_features = false, features = ["rustls-tls"] }
async-trait = "0.1.56"
anyhow = "1.0.58"
markdown = "0.3.0"
clap = { version = "3.2.6", features = ["derive"] }
toml = "0.5.9"
serde = { version = "1.0.138", features = ["derive"] }
futures = "0.3.21"
[dependencies.common]
path = "../common"
[dependencies.lighter_lib]
git = "https://git.nubo.sh/hulthe/lighter.git"
#path = "../../lighter/lib"
[dependencies.lighter_manager]
git = "https://git.nubo.sh/hulthe/lighter.git"
#path = "../../lighter/manager"

View File

@ -0,0 +1,31 @@
[mqtt]
#address = "hostname"
#port = 1883
#username = "user"
#password = "password"
[collectors]
markdown_web_links = [
"https://example.org/lmao.md"
]
[[bulbs]]
id = "light/bedroom"
[[bulbs]]
id = "light/living_room"
[[groups]]
name = "Living Room"
bulbs = ["light/living_room"]
x = 0
y = 0
shape = { Rectangle = { w = 10, h = 10 } }
[[groups]]
name = "Bedroom"
bulbs = ["light/bedroom"]
x = 11
y = 0
shape = { Rectangle = { w = 10, h = 10 } }

15
backend/src/collector.rs Normal file
View File

@ -0,0 +1,15 @@
mod markdown_web;
pub use markdown_web::MarkdownWeb;
use serde::Deserialize;
#[async_trait::async_trait]
pub trait Collector {
async fn collect(&mut self) -> anyhow::Result<String>;
}
#[derive(Deserialize)]
pub struct CollectorConfig {
pub markdown_web_links: Vec<String>,
}

View File

@ -0,0 +1,16 @@
use crate::collector::Collector;
use reqwest::get;
pub struct MarkdownWeb {
pub url: String,
}
#[async_trait::async_trait]
impl Collector for MarkdownWeb {
async fn collect(&mut self) -> anyhow::Result<String> {
let text = get(&self.url).await?.text().await?;
let html = markdown::to_html(&text);
Ok(html)
}
}

285
backend/src/main.rs Normal file
View File

@ -0,0 +1,285 @@
mod collector;
use clap::Parser;
use collector::{Collector, CollectorConfig, MarkdownWeb};
use common::{BulbMap, ClientMessage, ServerMessage};
use futures_util::{SinkExt, StreamExt};
use lighter_manager::{
manager::{BulbCommand, BulbManager, BulbSelector, BulbsConfig},
mqtt_conf::MqttConfig,
};
use log::LevelFilter;
use serde::Deserialize;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::path::PathBuf;
use tokio::sync::broadcast::error::RecvError;
use tokio::sync::{broadcast, mpsc};
use tokio::time::{sleep, Duration};
use tokio::{fs, select, task};
use warp::ws::{self, WebSocket};
use warp::{Filter, Rejection, Reply};
#[macro_use]
extern crate log;
#[derive(Parser)]
struct Opt {
/// More logging
#[clap(short, long, parse(from_occurrences))]
verbose: u8,
/// Supress non-error logs
#[clap(short, long)]
quiet: bool,
#[clap(long, short, default_value = "127.0.0.0:8000")]
bind: SocketAddr,
#[clap(long, default_value = "./www")]
frontend: String,
#[clap(long, short)]
config: PathBuf,
}
#[derive(Deserialize)]
struct Config {
mqtt: MqttConfig,
collectors: CollectorConfig,
#[serde(flatten)]
bulbs: BulbsConfig,
#[serde(flatten)]
bulb_map: BulbMap,
}
struct State {
config: Config,
client_message: broadcast::Sender<ClientRequest>,
server_message: broadcast::Sender<ServerMessage>,
}
#[tokio::main]
async fn main() {
let opt = Opt::parse();
let log_level = match opt.verbose {
_ if opt.quiet => LevelFilter::Error,
0 => LevelFilter::Info,
1 => LevelFilter::Debug,
2.. => LevelFilter::Trace,
};
pretty_env_logger::formatted_builder()
.default_format()
.filter_level(log_level)
.init();
let config = fs::read_to_string(&opt.config)
.await
.expect("Failed to read config");
let config: Config = toml::from_str(&config).expect("Failed to parse config");
// we keep a receiver here to keep the channel active when no clients are connected
let (server_message, _receiver) = broadcast::channel(100);
let (client_message, _) = broadcast::channel(100);
let state = State {
config,
client_message,
server_message,
};
let state = Box::leak(Box::new(state));
task::spawn(info_collector(state));
task::spawn(lights_collector(state));
let ws = warp::path("ws")
// The `ws()` filter will prepare the Websocket handshake.
.and(warp::ws())
.map(|ws: warp::ws::Ws| {
// And then our closure will be called when it completes...
ws.on_upgrade(|websocket| {
info!("Client connected {:?}", websocket);
client_handler(websocket, state)
})
});
let api = warp::path("api").and(ws.recover(not_found));
let index_html = format!("{}/index.html", opt.frontend);
let frontend = warp::fs::dir(opt.frontend).or(warp::fs::file(index_html));
let routes = api.or(frontend);
warp::serve(routes).run(opt.bind).await;
}
async fn not_found(_: Rejection) -> Result<impl Reply, Infallible> {
Ok(warp::http::StatusCode::NOT_FOUND)
}
#[derive(Clone)]
struct ClientRequest {
message: ClientMessage,
response: mpsc::Sender<ServerMessage>,
}
async fn lights_collector(state: &State) {
let config = &state.config;
let server_message = &state.server_message;
let mut client_message = state.client_message.subscribe();
let (cmd, bulb_states) = BulbManager::launch(config.bulbs.clone(), config.mqtt.clone())
.await
.expect("Failed to launch bulb manager");
loop {
let notify = bulb_states.notify_on_change();
sleep(Duration::from_millis(1000 / 10)).await; // limit to 10 updates/second
select! {
_ = notify => {
for (id, mode) in bulb_states.bulbs().await.clone().into_iter() {
if let Err(e) = server_message.send(ServerMessage::BulbMode { id, mode }) {
error!("broadcast channel error: {e}");
return;
}
}
}
request = client_message.recv() => {
let request = match request {
Ok(r) => r,
Err(_) => continue,
};
match request.message {
ClientMessage::SetBulbColor { id, color } => {
if let Err(e) = cmd.send(BulbCommand::SetColor(BulbSelector::Id(id), color)).await {
error!("bulb manager error: {e}");
}
}
ClientMessage::SetBulbPower { id, power } => {
if let Err(e) = cmd.send(BulbCommand::SetPower(BulbSelector::Id(id), power)).await {
error!("bulb manager error: {e}");
}
}
ClientMessage::GetBulbs => {
if let Err(e) = request.response.send(ServerMessage::BulbMap(config.bulb_map.clone())).await {
error!("GetBulbs response channel error: {e}");
return;
}
for (id, mode) in bulb_states.bulbs().await.clone().into_iter() {
if let Err(e) = request.response.send(ServerMessage::BulbMode { id, mode }).await {
error!("GetBulbs response channel error: {e}");
return;
}
}
}
_ => {}
}
}
}
}
}
async fn info_collector(state: &State) {
let mut collectors: Vec<Box<dyn Collector + Send>> = vec![];
for url in &state.config.collectors.markdown_web_links {
collectors.push(Box::new(MarkdownWeb {
url: url.to_string(),
}));
}
let mut collectors = collectors.into_boxed_slice();
let server_message = &state.server_message;
let collectors_len = collectors.len();
let next = move |i: usize| (i + 1) % collectors_len;
let mut i = 0;
loop {
sleep(Duration::from_secs(30)).await;
// don't bother collecting if no clients are connected
// there is always 1 receiver held by main process
if server_message.receiver_count() <= 1 {
continue;
}
i = next(i);
let collector = &mut collectors[i];
let msg = match collector.collect().await {
Ok(html) => ServerMessage::InfoPage { html },
Err(e) => {
warn!("collector error: {e}");
continue;
}
};
if let Err(e) = server_message.send(msg) {
error!("broadcast channel error: {e}");
return;
}
}
}
async fn client_handler(mut socket: WebSocket, state: &State) {
let mut server_message = state.server_message.subscribe();
let (server_responder, mut server_responses) = mpsc::channel(100);
async fn handle_server_message(socket: &mut WebSocket, message: ServerMessage) {
let message = match ron::to_string(&message) {
Ok(msg) => msg,
Err(e) => return error!("Failed to serialize message: {e}"),
};
if let Err(e) = socket.send(ws::Message::text(message)).await {
return warn!("client error: {e}");
}
}
loop {
select! {
response = server_responses.recv() => {
if let Some(message) = response {
handle_server_message(&mut socket, message).await;
}
}
message = server_message.recv() => {
let message = match message {
Ok(msg) => msg,
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => return,
};
handle_server_message(&mut socket, message).await;
}
message = socket.next() => {
match message {
None => return info!("stream closed"),
Some(Err(e)) => return warn!("client error: {e}"),
Some(Ok(message)) => {
let message = ron::from_str(message.to_str().unwrap()).unwrap();
let request = ClientRequest {
message,
response: server_responder.clone(),
};
if let Err(e) = state.client_message.send(request) {
return error!("client message handlers error: {e}");
}
}
}
}
}
}
}

11
common/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.137", features = ["derive"] }
[dependencies.lighter_lib]
git = "https://git.nubo.sh/hulthe/lighter.git"
#path = "../../lighter/lib"

65
common/src/lib.rs Normal file
View File

@ -0,0 +1,65 @@
use lighter_lib::{BulbColor, BulbId, BulbMode};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub enum ServerMessage {
InfoPage {
html: String,
},
/// Update the state of a bulb
BulbMode {
id: BulbId,
mode: BulbMode,
},
BulbMap(BulbMap),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[non_exhaustive]
pub enum ClientMessage {
//SubscribeToInfo,
//SubscribeToBulbs,
GetBulbs,
SetBulbColor { id: BulbId, color: BulbColor },
SetBulbPower { id: BulbId, power: bool },
}
/// A geometric description of rooms/groups of light bulbs
#[derive(Default, Deserialize, Serialize, Debug, Clone)]
pub struct BulbMap {
pub groups: Vec<BulbGroup>,
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct BulbGroup {
pub name: String,
pub bulbs: Vec<BulbId>,
pub x: u32,
pub y: u32,
pub shape: BulbGroupShape,
}
#[derive(Deserialize, Serialize, Debug, Clone, Copy)]
pub enum BulbGroupShape {
Circle { r: u32 },
Rectangle { w: u32, h: u32 },
}
impl BulbGroupShape {
pub fn height(&self) -> u32 {
match self {
&Self::Circle { r } => r,
&Self::Rectangle { h, .. } => h,
}
}
pub fn width(&self) -> u32 {
match self {
&Self::Circle { r } => r,
&Self::Rectangle { w, .. } => w,
}
}
}

609
frontend/Cargo.lock generated Normal file
View File

@ -0,0 +1,609 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704"
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bumpalo"
version = "3.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "cookie"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
dependencies = [
"percent-encoding",
"time",
"version_check 0.9.4",
]
[[package]]
name = "css_typegen"
version = "0.2.0"
source = "git+https://github.com/hulthe/css_typegen.git?branch=master#ed1c4b1b7c8bf19e1ce045cafa12f3886041134c"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn",
]
[[package]]
name = "dbg"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4677188513e0e9d7adced5997cf9a1e7a3c996c994f90093325c5332c1a8b221"
dependencies = [
"version_check 0.1.5",
]
[[package]]
name = "enclose"
version = "1.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357"
[[package]]
name = "futures"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868"
[[package]]
name = "futures-task"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
[[package]]
name = "futures-util"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "getrandom"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "gloo-events"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
dependencies = [
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-file"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa5d6084efa4a2b182ef3a8649cb6506cb4843f22cf907c6e0a799944248ae90"
dependencies = [
"futures-channel",
"gloo-events",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "gloo-timers"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
dependencies = [
"futures-channel",
"futures-core",
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "gloo-utils"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "929c53c913bb7a88d75d9dc3e9705f963d8c2b9001510b25ddaf671b9fb7049d"
dependencies = [
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "hashbrown"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
[[package]]
name = "indexmap"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]]
name = "js-sys"
version = "0.3.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]]
name = "memchr"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
[[package]]
name = "num_threads"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
dependencies = [
"libc",
]
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "ppv-lite86"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872"
[[package]]
name = "proc-macro2"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "regex"
version = "1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
[[package]]
name = "ryu"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695"
[[package]]
name = "seed"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aadc99b3d823229f987d3b5277c00dcff69d5011ed0ed38c20d6aaa66ed7372"
dependencies = [
"console_error_panic_hook",
"cookie",
"dbg",
"enclose",
"futures",
"getrandom",
"gloo-file",
"gloo-timers",
"gloo-utils",
"indexmap",
"js-sys",
"rand",
"serde",
"serde_json",
"uuid",
"version_check 0.9.4",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
]
[[package]]
name = "serde"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "singit2"
version = "0.2.2"
dependencies = [
"anyhow",
"css_typegen",
"seed",
"serde",
"serde_json",
"wasm-bindgen",
]
[[package]]
name = "slab"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]]
name = "syn"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "time"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217"
dependencies = [
"itoa",
"libc",
"num_threads",
"time-macros",
]
[[package]]
name = "time-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
[[package]]
name = "unicode-ident"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]]
name = "uuid"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
dependencies = [
"cfg-if",
"serde",
"serde_json",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2"
dependencies = [
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
[[package]]
name = "web-sys"
version = "0.3.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
dependencies = [
"js-sys",
"wasm-bindgen",
]

31
frontend/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "hemma_web"
version = "0.1.0"
authors = ["Joakim Hulthe <joakim@hulthe.net"]
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
seed = "0.9.1"
wasm-bindgen = "=0.2.80" # 0.2.81 has a breaking change
serde = { version = "1", features = ['derive'] }
serde_json = "1"
anyhow = "*"
ron = "0.7.1"
[dependencies.css_typegen]
git = "https://github.com/hulthe/css_typegen.git"
branch = "master"
[dependencies.common]
path = "../common"
[dependencies.lighter_lib]
git = "https://git.nubo.sh/hulthe/lighter.git"
#path = "../../lighter/lib"
[dependencies.seed_router]
git = "https://git.nubo.sh/hulthe/seed_router.git"
#path = "../../seed_router/seed_router"

18
frontend/Trunk.toml Normal file
View File

@ -0,0 +1,18 @@
[serve]
address = "0.0.0.0"
[[proxy]]
# This WebSocket proxy example has a backend and ws field. This example will listen for
# WebSocket connections at `/api/ws` and proxy them to `ws://localhost:9000/api/ws`.
backend = "ws://localhost:8000/api/ws"
ws = true
#[[proxy]]
# This proxy example has a backend and a rewrite field. Requests received on `rewrite` will be
# proxied to the backend after rewriting the `rewrite` prefix to the `backend`'s URI prefix.
# E.G., `/api/v1/resource/x/y/z` -> `/resource/x/y/z`
#rewrite = "/api/"
#backend = "http://localhost:8000/api/"

38
frontend/index.html Executable file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- stylesheets -->
<link data-trunk data-inline rel="scss" href="/static/styles/common.scss">
<link data-trunk data-inline rel="scss" href="/static/styles/penguin.scss">
<!-- fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Ubuntu|Ubuntu+Mono&display=swap">
<!-- pwa manifest -->
<!--
<link data-trunk rel="copy-file" href="/static/manifest.json">
<link rel="manifest" href="/static/manifest.json">
-->
<!-- copy image directory -->
<link data-trunk rel="copy-dir" href="/static/images">
<!-- image preloading -->
<link rel="preload" href="/images/penguin1.svg" as="image">
<link rel="preload" href="/images/penguin2.svg" as="image">
<link rel="preload" href="/images/penguin3.svg" as="image">
<!-- icon -->
<link rel="icon" type="image/png" href="/images/icon.png">
<title>hemma</title>
<meta name="description" content="Home automation and information">
</head>
<body>
<div id="app", style="width: 100%;"></div>
</body>
</html>

151
frontend/src/app.rs Normal file
View File

@ -0,0 +1,151 @@
use crate::page;
use common::{ClientMessage, ServerMessage};
use seed::app::orders::OrdersContainer;
use seed::prelude::*;
use seed::{log, window};
use seed_router::Router;
use std::collections::VecDeque;
pub type AppOrders = OrdersContainer<Msg, Model, Vec<Node<Msg>>>;
/// Delays between successive attempts to reconnect in case the socket breaks. In seconds.
const TIMEOUT_CONNECT_DELAYS: &[u32] = &[2, 5, 10, 10, 10, 20, 30, 60, 120, 300];
pub struct Model {
page: Pages,
send_queue: VecDeque<ClientMessage>,
socket: WebSocket,
ws_url: String,
timeout_count: usize,
}
#[derive(Router)]
pub enum Pages {
#[page("404", NotFound)]
NotFound(page::not_found::Model),
#[page("info", Info)]
Info(page::info::Model),
#[page("lights", Lights)]
Lights(page::lights::Model),
}
#[derive(Debug)]
pub enum PageMsg {
NotFound(page::not_found::Msg),
Info(page::info::Msg),
Lights(page::lights::Msg),
}
#[derive(Debug)]
pub enum Msg {
Page(PageMsg),
SendMessage(ClientMessage),
FlushMessageQueue,
// Global
Connect,
SocketOpened(),
SocketClosed(CloseEvent),
SocketError(),
SocketMessage(WebSocketMessage),
}
pub fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.subscribe(Msg::SendMessage);
let location = window().location();
let host = location.host().expect("Failed to get hostname");
let ws_protocol = match location.protocol().ok().as_deref() {
Some("http:") => "ws",
_ => "wss",
};
let ws_url = format!("{ws_protocol}://{host}/api/ws");
Model {
page: Pages::from_url(url, &mut orders.proxy(Msg::Page))
.unwrap_or(Pages::NotFound(Default::default())),
send_queue: Default::default(),
socket: open_socket(&ws_url, orders),
ws_url,
timeout_count: 0,
}
}
fn open_socket(url: &str, orders: &mut impl Orders<Msg>) -> WebSocket {
WebSocket::builder(url, orders)
.on_open(Msg::SocketOpened)
.on_close(Msg::SocketClosed)
.on_error(Msg::SocketError)
.on_message(Msg::SocketMessage)
.build_and_open()
.expect("failed to open websocket")
}
pub fn update(msg: Msg, model: &mut Model, orders: &mut AppOrders) {
#[cfg(debug_assertions)]
log!(format!("{msg:?}"));
match msg {
Msg::Page(msg) => model.page.update(msg, &mut orders.proxy(Msg::Page)),
Msg::FlushMessageQueue => {
while let Some(message) = model.send_queue.pop_front() {
let serialized = ron::to_string(&message).unwrap();
if let Err(e) = model.socket.send_text(serialized) {
model.send_queue.push_front(message);
log!(e);
return;
}
}
}
Msg::Connect => {
model.socket = open_socket(&model.ws_url, orders);
}
Msg::SendMessage(message) => {
model.send_queue.push_back(message);
orders.send_msg(Msg::FlushMessageQueue);
}
Msg::SocketOpened() => {
model.timeout_count = 0;
orders.send_msg(Msg::FlushMessageQueue);
}
Msg::SocketClosed(_event) => {
let timeout_sec = TIMEOUT_CONNECT_DELAYS[model.timeout_count];
let timeout_ms = timeout_sec * 1000;
orders.perform_cmd(cmds::timeout(timeout_ms, || Msg::Connect));
log!(format!(
"Socket closed, trying to reconnect in {timeout_sec} seconds"
));
model.timeout_count = TIMEOUT_CONNECT_DELAYS.len().min(model.timeout_count + 1);
}
Msg::SocketError() => {}
Msg::SocketMessage(message) => {
if let Err(e) = handle_ws_msg(message, orders) {
log!(e);
}
}
}
}
fn handle_ws_msg(message: WebSocketMessage, orders: &mut impl Orders<Msg>) -> anyhow::Result<()> {
let message = message.text().map_err(|e| anyhow::format_err!("{e:?}"))?;
let message: ServerMessage = ron::from_str(&message)?;
orders.notify(message);
Ok(())
}
pub fn view(model: &Model) -> Vec<Node<Msg>> {
vec![model.page.view().map_msg(Msg::Page)]
//match &model.page {
// Pages::NotFound => vec![h1!["Not Found"]],
// Pages::InfoScreen => vec![div![C![C.info_box], raw![&model.info_page]]],
// Pages::Lights(page) => vec![],
//}
}

View File

@ -0,0 +1,225 @@
use crate::css::C;
use lighter_lib::BulbColor;
use seed::prelude::*;
use seed::{attrs, div, C};
use std::f32::consts::PI;
use web_sys::MouseEvent;
#[derive(Default)]
pub struct ColorPicker {
hue: f32,
saturation: f32,
brightness: f32,
temperature: f32,
mode: ColorPickerSetting,
dragging: Option<ColorPickerAttr>,
}
#[derive(Default)]
enum ColorPickerSetting {
#[default]
HSB,
Kelvin,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorPickerAttr {
HueSat,
Brightness,
Temperature,
}
#[derive(Debug)]
pub enum ColorPickerMsg {
MouseDown(ColorPickerAttr, MouseEvent),
MouseMove(ColorPickerAttr, MouseEvent),
MouseUp(Option<ColorPickerAttr>, MouseEvent),
SetColor(BulbColor),
}
impl ColorPicker {
pub fn update(&mut self, msg: ColorPickerMsg, orders: &mut impl Orders<ColorPickerMsg>) {
match msg {
ColorPickerMsg::MouseDown(target, event) => {
self.dragging = Some(target);
self.handle_mouse_event(target, event);
}
ColorPickerMsg::MouseMove(target, event) => {
self.handle_mouse_event(target, event);
}
ColorPickerMsg::MouseUp(target, event) => {
if let Some(target) = target {
self.handle_mouse_event(target, event);
}
match self.dragging.take() {
Some(ColorPickerAttr::HueSat) => self.mode = ColorPickerSetting::HSB,
Some(ColorPickerAttr::Brightness) => {}
Some(ColorPickerAttr::Temperature) => self.mode = ColorPickerSetting::Kelvin,
None => return,
}
let color = match self.mode {
ColorPickerSetting::HSB => {
BulbColor::hsb(self.hue, self.saturation, self.brightness)
}
ColorPickerSetting::Kelvin => {
BulbColor::kelvin(self.temperature, self.brightness)
}
};
orders.send_msg(ColorPickerMsg::SetColor(color));
}
ColorPickerMsg::SetColor { .. } => {}
}
}
fn handle_mouse_event(&mut self, target: ColorPickerAttr, event: MouseEvent) {
if Some(target) != self.dragging {
return;
}
let handle_bar = || {
let y = event.offset_y() as f32;
let height = 200.0;
1.0 - (y / height).clamp(0.0, 1.0)
};
match target {
ColorPickerAttr::HueSat => {
let (x, y) = (event.offset_x() as f32, event.offset_y() as f32);
let radius = 100.0;
let x = x - radius;
let y = y - radius;
let angle = (x.atan2(y) + PI) / (2.0 * PI);
self.hue = 1.0 - angle;
self.saturation = ((x.powi(2) + y.powi(2)).sqrt() / radius).clamp(0.0, 1.0);
}
ColorPickerAttr::Brightness => self.brightness = handle_bar(),
ColorPickerAttr::Temperature => self.temperature = handle_bar(),
}
}
pub fn view(&self) -> Node<ColorPickerMsg> {
use ColorPickerAttr::{Brightness, HueSat, Temperature};
use ColorPickerMsg::{MouseDown, MouseMove, MouseUp};
let (hs_x, hs_y) = {
let radius = 100.0;
let angle = -self.hue * PI * 2.0;
let x = -self.saturation * radius * angle.sin();
let y = -self.saturation * radius * angle.cos();
(x + radius - 5.0, y + radius - 5.0)
};
let br_y = {
let height = 200.0;
let marker_height = 10.0;
(1.0 - self.brightness) * height - marker_height / 2.0
};
let temp_y = {
let height = 200.0;
let marker_height = 10.0;
(1.0 - self.temperature) * height - marker_height / 2.0
};
let (r, g, b) = hsb_to_rgb(self.hue, 1.0, 1.0);
let saturation_gradient = match self.mode {
ColorPickerSetting::HSB => {
format!("background: linear-gradient(0deg, #000, rgba({r},{g},{b},1));")
}
ColorPickerSetting::Kelvin => format!("background: linear-gradient(0deg, #000, #fff);"),
};
div![
C![C.color_picker],
mouse_ev(Ev::MouseUp, |ev| MouseUp(None, ev)),
div![
C![C.color_wheel],
div![
C![C.color_wheel_marker],
attrs! {
At::Style => format!("margin-left: {hs_x}px; margin-top: {hs_y}px;"),
},
],
mouse_ev(Ev::MouseDown, |ev| MouseDown(HueSat, ev)),
mouse_ev(Ev::MouseMove, |ev| MouseMove(HueSat, ev)),
mouse_ev(Ev::MouseUp, |ev| MouseUp(Some(HueSat), ev)),
//mouse_ev(Ev::MouseLeave, |ev| MouseUp(HueSat, ev)),
],
div![
C![C.brightness_bar],
attrs! { At::Style => saturation_gradient },
div![
C![C.color_bar_marker],
attrs! {
At::Style => format!("margin-top: {br_y}px;"),
},
],
mouse_ev(Ev::MouseDown, |ev| MouseDown(Brightness, ev)),
mouse_ev(Ev::MouseMove, |ev| MouseMove(Brightness, ev)),
mouse_ev(Ev::MouseUp, |ev| MouseUp(Some(Brightness), ev)),
//mouse_ev(Ev::MouseLeave, |ev| MouseUp(Brightness, ev)),
],
div![
C![C.temperature_bar],
div![
C![C.color_bar_marker],
attrs! {
At::Style => format!("margin-top: {temp_y}px;"),
},
],
mouse_ev(Ev::MouseDown, |ev| MouseDown(Temperature, ev)),
mouse_ev(Ev::MouseMove, |ev| MouseMove(Temperature, ev)),
mouse_ev(Ev::MouseUp, |ev| MouseUp(Some(Temperature), ev)),
//mouse_ev(Ev::MouseLeave, |ev| MouseUp(Temperature, ev)),
],
]
}
pub fn set_hsb(&mut self, h: f32, s: f32, b: f32) {
self.hue = h;
self.saturation = s;
self.brightness = b;
self.mode = ColorPickerSetting::HSB;
}
pub fn set_kelvin(&mut self, t: f32, b: f32) {
self.temperature = t;
self.brightness = b;
self.mode = ColorPickerSetting::Kelvin;
}
pub fn set_color(&mut self, color: BulbColor) {
match color {
BulbColor::HSB { h, s, b } => self.set_hsb(h, s, b),
BulbColor::Kelvin { t, b } => self.set_kelvin(t, b),
}
}
}
fn hsb_to_rgb(h: f32, s: f32, b: f32) -> (u8, u8, u8) {
let h = 360.0 * h.clamp(0.0, 1.0);
let c = b * s; // chroma
let x = c * (1. - ((h / 60.) % 2. - 1.).abs());
let m = b - c;
let m = |v| ((v + m) * 255.0) as u8;
let (r, g, b) = match h {
_ if h < 60. => (c, x, 0.0),
_ if h < 120.0 => (x, c, 0.0),
_ if h < 180.0 => (0.0, c, x),
_ if h < 240.0 => (0.0, x, c),
_ if h < 300.0 => (x, 0.0, c),
_ => (c, 0.0, x),
};
(m(r), m(g), m(b))
}

View File

@ -0,0 +1 @@
pub mod color_picker;

7
frontend/src/css.rs Normal file
View File

@ -0,0 +1,7 @@
use css_typegen::css_typegen;
// NOTE: Remember to edit index.html when adding new css-files!
// Generate rust types for css-classes.
// Used for autocompletion and extra compile-time checks.
css_typegen!("frontend/static/styles");

12
frontend/src/lib.rs Normal file
View File

@ -0,0 +1,12 @@
mod app;
mod components;
mod css;
mod page;
use seed::prelude::wasm_bindgen;
use seed::App;
#[wasm_bindgen(start)]
pub fn start() {
App::start("app", app::init, app::update, app::view);
}

39
frontend/src/page/info.rs Normal file
View File

@ -0,0 +1,39 @@
use common::ServerMessage;
use seed::prelude::*;
use seed::{div, raw};
use seed_router::Page;
#[derive(Default)]
pub struct Model {
content: String,
}
#[derive(Debug)]
pub enum Msg {
ServerMessage(ServerMessage),
}
impl Page for Model {
type Msg = Msg;
fn new(orders: &mut impl Orders<Self::Msg>) -> Self {
orders.subscribe(Msg::ServerMessage);
Model {
content: r#"<div class="penguin"></div>"#.into(),
}
}
fn update(&mut self, msg: Self::Msg, _orders: &mut impl Orders<Self::Msg>) {
match msg {
Msg::ServerMessage(ServerMessage::InfoPage { html }) => {
self.content = html;
}
Msg::ServerMessage(_) => {}
}
}
fn view(&self) -> Node<Self::Msg> {
div![raw![&self.content]]
}
}

240
frontend/src/page/lights.rs Normal file
View File

@ -0,0 +1,240 @@
use crate::components::color_picker::{ColorPicker, ColorPickerMsg};
use crate::css::C;
use common::{BulbGroup, BulbGroupShape, BulbMap, ClientMessage, ServerMessage};
use lighter_lib::{BulbId, BulbMode};
use seed::prelude::*;
use seed::{attrs, button, div, C};
use seed_router::Page;
use std::collections::{BTreeMap, HashSet};
use std::fmt::Write;
/// /lights page
#[derive(Default)]
pub struct Model {
bulb_states: BTreeMap<BulbId, BulbMode>,
bulb_map: BulbMap,
/// The currently selected bulb map groups
selected_groups: HashSet<usize>,
/// Whether the currently selected map groups have been interacted with
groups_interacted: bool,
color_picker: ColorPicker,
}
#[derive(Debug)]
pub enum Msg {
ServerMessage(ServerMessage),
SelectGroup(usize),
DeselectGroups,
ColorPicker(ColorPickerMsg),
SetBulbPower(bool),
}
impl Page for Model {
type Msg = Msg;
fn new(orders: &mut impl Orders<Self::Msg>) -> Self {
orders.subscribe(Msg::ServerMessage);
orders.notify(ClientMessage::GetBulbs);
Model::default()
}
fn update(&mut self, msg: Self::Msg, orders: &mut impl Orders<Self::Msg>) {
match msg {
Msg::ServerMessage(msg) => match msg {
ServerMessage::BulbMode { id, mode: new_mode } => {
*self.bulb_states.entry(id).or_default() = new_mode
//color_picker.set_color(mode.color);
}
ServerMessage::BulbMap(bulb_map) => {
self.bulb_map = bulb_map;
self.selected_groups.clear();
}
_ => {}
},
Msg::DeselectGroups => {
self.selected_groups.clear();
}
Msg::SelectGroup(index) => {
if self.groups_interacted {
self.groups_interacted = false;
// If this group is the only selected group, don't clear it
if self.selected_groups.len() != 1 || !self.selected_groups.contains(&index) {
self.selected_groups.clear();
}
}
if !self.selected_groups.insert(index) {
self.selected_groups.remove(&index);
} else {
let bulb = self
.bulb_map
.groups
.get(index)
.and_then(|group| group.bulbs.first())
.and_then(|id| self.bulb_states.get(id));
if let Some(bulb) = bulb {
self.color_picker.set_color(bulb.color);
}
}
}
Msg::ColorPicker(ColorPickerMsg::SetColor(color)) => {
self.groups_interacted = true;
self.for_selected_bulbs(|id, _| {
let message = ClientMessage::SetBulbColor {
id: id.clone(),
color,
};
orders.notify(message);
});
}
Msg::ColorPicker(msg) => {
self.color_picker
.update(msg, &mut orders.proxy(|msg| Msg::ColorPicker(msg)));
}
Msg::SetBulbPower(power) => {
self.groups_interacted = true;
self.for_selected_bulbs(|id, _| {
let message = ClientMessage::SetBulbPower {
id: id.clone(),
power,
};
orders.notify(message);
});
}
}
}
fn view(&self) -> Node<Self::Msg> {
//let view_bulb = |(id, (mode, color_picker)): (&BulbId, &(BulbMode, ColorPicker))| {
// div![
// C![C.bulb_box],
// h1![id],
// div![
// C![C.bulb_controls],
// {
// let id = id.clone();
// color_picker.view().map_msg(|msg| Msg::ColorPicker(id, msg))
// },
// button![
// if mode.power {
// C![C.bulb_power_button, C.bulb_power_button_on]
// } else {
// C![C.bulb_power_button]
// },
// {
// let id = id.clone();
// let power = !mode.power;
// ev(Ev::Click, move |_| Msg::SetBulbPower(id, power))
// },
// ],
// ],
// ]
//};
let bulb_map_width = self
.bulb_map
.groups
.iter()
.map(|group| group.x + group.shape.width())
.max()
.unwrap_or(0);
let bulb_map_height = self
.bulb_map
.groups
.iter()
.map(|group| group.y + group.shape.height())
.max()
.unwrap_or(0);
let view_bulb_group = |(i, group): (usize, &BulbGroup)| {
let (w, h) = (group.shape.width(), group.shape.height());
let mut style = String::new();
write!(
&mut style,
"margin-left: {}rem; margin-top: {}rem; width: {}rem; height: {}rem;",
group.x, group.y, w, h
)
.ok();
if let BulbGroupShape::Circle { r } = group.shape {
write!(&mut style, " border-radius: {r}rem;").ok();
}
div![
&group.name[..1],
if self.selected_groups.contains(&i) {
C![C.bulb_group, C.bulb_group_selected]
} else {
C![C.bulb_group]
},
attrs! {
At::Style => style,
},
ev(Ev::Click, move |event| {
event.stop_propagation();
Msg::SelectGroup(i)
}),
]
};
let (_color, power) = self
.selected_groups
.iter()
.next()
.and_then(|&index| self.bulb_map.groups.get(index))
.and_then(|group| group.bulbs.first())
.and_then(|id| self.bulb_states.get(id))
.map(|bulb| (bulb.color, bulb.power))
.unwrap_or_default();
div![
C![C.bulb_box],
div![
C![C.bulb_map],
attrs! {
At::Style => format!("width: {}rem; height: {}rem;", bulb_map_width, bulb_map_height),
},
ev(Ev::Click, |_| Msg::DeselectGroups),
self.bulb_map.groups.iter().enumerate().map(view_bulb_group),
],
div![
C![C.bulb_controls],
self.color_picker
.view()
.map_msg(|msg| Msg::ColorPicker(msg)),
button![
if power {
C![C.bulb_power_button, C.bulb_power_button_on]
} else {
C![C.bulb_power_button]
},
ev(Ev::Click, move |_| Msg::SetBulbPower(!power)),
div![attrs! { At::Id => "switch_socket" }],
div![attrs! { At::Id => "off_label" }, "Off"],
div![attrs! { At::Id => "on_label" }, "On"],
div![attrs! { At::Id => "lever_stem" }],
div![attrs! { At::Id => "lever_face" }],
],
],
]
}
}
impl Model {
fn for_selected_bulbs(&self, mut f: impl FnMut(&BulbId, &BulbMode)) {
self.selected_groups
.iter()
.filter_map(|&index| self.bulb_map.groups.get(index))
.flat_map(|group| group.bulbs.iter())
.filter_map(|id| self.bulb_states.get(id).map(|bulb| (id, bulb)))
.for_each(|(id, bulb)| f(id, bulb));
}
}

3
frontend/src/page/mod.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod info;
pub mod lights;
pub mod not_found;

View File

@ -0,0 +1,25 @@
use seed::h1;
use seed::prelude::*;
use seed_router::Page;
#[derive(Default)]
pub struct Model;
#[derive(Debug)]
pub enum Msg {}
impl Page for Model {
type Msg = Msg;
fn new(_orders: &mut impl Orders<Self::Msg>) -> Self {
Model
}
fn update(&mut self, msg: Self::Msg, _orders: &mut impl Orders<Self::Msg>) {
match msg {}
}
fn view(&self) -> Node<Self::Msg> {
h1!["404: Not found"]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="120"
height="120"
viewBox="0 0 31.75 31.75"
version="1.1"
id="svg8"
sodipodi:docname="penguin1.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview14"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="6.9083333"
inkscape:cx="60"
inkscape:cy="68.54041"
inkscape:window-width="1702"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
style="display:none">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 7.5967544,-6.2400058 c 0,0 -0.041418,0.1031026 -0.02862,0.2093299 0.012798,0.1062273 0.084435,0.1741091 0.084435,0.1741091 l -0.083147,0.088222 c 0,0 -0.1053512,-0.138166 -0.073877,-0.2774823 0.031474,-0.1393166 0.1012092,-0.1941783 0.101209,-0.1941787 z"
id="path1039" />
</g>
<g
id="layer2"
style="display:inline;fill:#ffffff">
<g
id="g1085"
transform="matrix(4.3607823,0,0,4.3607823,-25.60314,0)"
style="fill:#ffffff">
<path
style="fill:#ffffff;stroke-width:0.0678637"
d="m 8.9055733,7.2413582 c -0.565186,-0.09685 -1.176774,-0.415363 -1.632858,-0.850389 -1.503214,-1.43381 -1.115157,-3.86618 0.767032,-4.807792 0.257986,-0.129065 0.613842,-0.340937 0.790791,-0.470828 0.427006,-0.31344801 0.957267,-0.88614801 0.957267,-1.03388001 0,-0.09418 0.01716,-0.101117 0.08424,-0.03403 0.04633,0.04633 0.09978,0.25562 0.118763,0.465073 l 0.03452,0.380824 0.213389,-0.180047 c 0.117363,-0.09903 0.228326,-0.180047 0.246583,-0.180047 0.01825,0 0.05453,0.199156 0.08059,0.442568 0.04564,0.42610801 0.0569,0.44741901 0.303068,0.57300001 0.140621,0.07174 0.394132,0.23612 0.563357,0.365292 l 0.307682,0.234855 0.07434,-0.163155 c 0.159263,-0.34954 0.752058,-0.0992 0.869072,0.367026 0.06159,0.245394 -0.05541,0.517892 -0.23331,0.543385 -0.08323,0.01193 -0.150934,0.0272 -0.150453,0.03393 4.74e-4,0.0067 0.06156,0.205833 0.135728,0.442438 0.385927,1.231145 -0.08926,2.609103 -1.154563,3.348037 -0.643116,0.446087 -1.5987099,0.656795 -2.3752269,0.523734 z m 1.2351777,-0.136144 c 0.558283,-0.117422 1.025941,-0.369806 1.37326,-0.741116 0.473889,-0.506621 0.628567,-0.967713 0.420215,-1.252649 -0.07888,-0.107873 -0.07888,-0.161324 0,-0.350108 0.121083,-0.289793 0.117727,-0.594607 -0.01134,-1.029201 -0.101025,-0.340191 -0.181472,-0.425109 -0.268827,-0.28377 -0.08904,0.144072 -0.389985,0.0647 -0.617562,-0.162872 -0.17695,-0.176948 -0.230737,-0.287688 -0.230737,-0.475043 0,-0.241031 -0.0037,-0.244309 -0.280028,-0.244309 -0.176302,0 -0.427095,0.07473 -0.6770717,0.201763 l -0.397045,0.201762 -0.337336,-0.209698 c -0.815418,-0.506889 -1.602182,-0.242198 -2.027616,0.682148 -0.191776,0.416675 -0.236961,1.14766 -0.08788,1.421699 0.06294,0.115695 0.06651,0.180504 0.01288,0.234129 -0.116061,0.11606 -0.08718,0.501906 0.059,0.78846 0.482693,0.946148 1.827708,1.480112 3.0700767,1.218805 z M 8.8477543,5.2533082 c -0.424112,-0.10747 -0.654904,-0.301236 -0.592603,-0.497529 0.06213,-0.195726 0.665023,-0.497533 1.076087,-0.538679 0.611047,-0.06116 1.5000967,0.382898 1.3821197,0.69034 -0.119917,0.312492 -1.2041087,0.513493 -1.8656037,0.345868 z m 1.2589217,-0.1425 c 0.456161,-0.135464 0.539706,-0.23998 0.34103,-0.426626 -0.419028,-0.393651 -1.1014207,-0.461363 -1.6826577,-0.166964 -0.455585,0.230757 -0.494803,0.363836 -0.150648,0.511195 0.486065,0.208121 0.97753,0.235258 1.4922757,0.0824 z M 8.6341183,4.7696162 c 0,-0.08863 0.121319,-0.101086 0.82579,-0.08483 0.5862597,0.01353 0.8339577,0.04366 0.8539537,0.103883 0.02154,0.06486 -0.172846,0.08483 -0.8257887,0.08483 -0.72146,0 -0.853955,-0.01612 -0.853955,-0.103884 z m 2.9852977,-1.422177 c 0.116451,-0.140314 0.03796,-0.315999 -0.115476,-0.258467 -0.109958,0.04122 -0.111839,0.03384 -0.02006,-0.07879 0.09429,-0.115732 0.08966,-0.120606 -0.06733,-0.07078 -0.151,0.04792 -0.158931,0.04187 -0.07599,-0.05808 0.08138,-0.09805 0.07308,-0.112539 -0.0684,-0.119456 -0.115019,-0.0056 -0.127085,-0.01674 -0.04224,-0.03891 0.145624,-0.03806 0.154107,-0.128726 0.01697,-0.181351 -0.432561,-0.165989 -0.470623,0.571787 -0.04153,0.804994 0.228877,0.12439 0.311386,0.124558 0.414065,8.42e-4 z m 0.337056,-0.43736 0.16668,-0.128093 -0.131214,-0.243646 c -0.07217,-0.134006 -0.148013,-0.243647 -0.168546,-0.243647 -0.02982,0 -0.274289,0.150867 -0.397626,0.245381 -0.03646,0.02793 0.26552,0.501375 0.318664,0.499607 0.02495,-8.28e-4 0.120369,-0.05915 0.212042,-0.129602 z m 0.629143,-0.248805 c 0.176025,-0.386328 -0.430301,-0.9977 -0.667609,-0.673165 -0.104081,0.142338 -0.09554,0.249613 0.02302,0.289132 0.04119,0.01373 0.09163,-0.03017 0.112083,-0.09756 0.02046,-0.06738 0.02721,-0.03826 0.015,0.0647 -0.01797,0.151566 -0.0046,0.1726 0.0703,0.110451 0.111754,-0.09275 0.124608,0.01632 0.01486,0.126073 -0.05828,0.05827 -0.03712,0.06896 0.08483,0.04283 0.137963,-0.02955 0.144981,-0.02359 0.04653,0.03952 -0.06377,0.04088 -0.09868,0.119327 -0.07757,0.174329 0.06225,0.162216 0.290873,0.116122 0.378559,-0.07633 z"
id="path913-7" />
<path
style="fill:#ffffff;stroke-width:0.0691565"
d="M 7.8768005,4.236173 C 7.6543017,4.153323 7.6524223,3.710674 7.8742605,3.628625 8.0875869,3.549705 8.2761935,3.613255 8.3404236,3.785709 8.4567516,4.098027 8.1933217,4.354002 7.8768404,4.236173 Z M 8.1361383,3.927654 c 0,-0.117352 -0.1782109,-0.133788 -0.2468281,-0.02276 -0.052137,0.08436 0.1031823,0.211193 0.1916582,0.156514 0.030349,-0.01875 0.055173,-0.07894 0.055173,-0.13375 z m 2.6417907,0.232298 c -0.219104,-0.219102 0.03747,-0.636462 0.342282,-0.556756 0.196786,0.05146 0.30135,0.306717 0.201823,0.492685 -0.08796,0.164344 -0.406338,0.201838 -0.544105,0.06407 z m 0.370255,-0.130506 c 0.07127,-0.115313 -0.08903,-0.247547 -0.184421,-0.152145 -0.0798,0.0798 -0.02986,0.227327 0.07696,0.227327 0.03354,0 0.0819,-0.03383 0.107459,-0.07518 z"
id="path913-6-5" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 10.825894,3.3605122 c 0,0 -0.04142,0.1031026 -0.02862,0.2093299 0.0128,0.1062273 0.08444,0.1741091 0.08444,0.1741091 l -0.08315,0.088222 c 0,0 -0.105351,-0.138166 -0.07388,-0.2774823 0.03147,-0.1393166 0.101209,-0.1941783 0.10121,-0.1941787 z"
id="path1039-6" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 7.7793686,3.3458678 c 0,0 -0.04142,0.1031026 -0.02862,0.2093299 0.0128,0.1062273 0.08444,0.1741091 0.08444,0.1741091 l -0.08315,0.088222 c 0,0 -0.105351,-0.138166 -0.07388,-0.2774823 0.03147,-0.1393166 0.101209,-0.1941783 0.10121,-0.1941787 z"
id="path1039-6-2" />
</g>
</g>
<g
id="layer3"
style="display:inline" />
</svg>

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="120"
height="120"
viewBox="0 0 31.75 31.75"
version="1.1"
id="svg8"
sodipodi:docname="penguin2.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview839"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="6.2083333"
inkscape:cx="6.8456376"
inkscape:cy="60"
inkscape:window-width="1702"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
style="display:inline;fill:#ffffff">
<path
style="fill:#ffffff;stroke-width:0.177246"
d="m 13.134179,31.651166 c -1.367874,-0.18043 -3.4872278,-0.85238 -4.780367,-1.51563 -1.871747,-0.96002 -4.179215,-3.22966 -5.0074297,-4.92534 -1.5914865,-3.25839 -1.6801158,-7.72336 -0.2278948,-11.48085 1.0247424,-2.65144 2.7098602,-4.5706201 5.4768881,-6.2376401 4.4640374,-2.6894 6.9208324,-4.59636 8.5218534,-6.61464 0.435944,-0.54955 0.851411,-0.9404 0.923263,-0.86855 0.34663,0.34663 0.709996,2.45556 0.596276,3.46071 l -0.118698,1.04918 0.543199,-0.3868 c 0.298763,-0.21273 0.754695,-0.66407 1.013185,-1.00297 0.258491,-0.3389 0.531184,-0.55268 0.605988,-0.47507 0.277608,0.28804 0.872923,2.3693 0.872923,3.05181 0,0.78189 -0.07309,0.70941 2.140938,2.12277 3.360595,2.14527 5.28622,5.2741001 5.845846,9.4985401 0.238089,1.79727 0.119928,4.12317 -0.289284,5.69424 -0.438918,1.68512 -1.295306,3.20011 -2.596637,4.59356 -2.715779,2.90803 -6.119638,4.1954 -10.928887,4.13338 -1.132683,-0.0146 -2.298705,-0.0581 -2.591161,-0.0967 z m 5.683631,-1.1265 c 5.074026,-0.89873 8.492767,-3.70496 9.49005,-7.7898 0.358508,-1.46844 0.317005,-5.28368 -0.07569,-6.95754 -0.594746,-2.53514 -2.005266,-4.78931 -3.64922,-5.8318801 -1.428352,-0.90583 -1.561881,-0.82384 -2.840659,1.7442101 -0.615739,1.23654 -1.777129,3.21756 -2.580868,4.40229 l -1.461342,2.15404 0.927607,0.3153 c 1.188141,0.40386 2.476979,1.20634 2.848748,1.77373 0.272049,0.4152 0.269021,0.4806 -0.04735,1.02231 -0.778356,1.33273 -2.272203,1.79512 -5.849412,1.81056 l -2.340153,0.0101 -2.489801,2.46697 c -2.6276133,2.60352 -2.8788341,3.03045 -2.0880964,3.54856 1.8126654,1.18771 7.0732674,1.8772 10.1561944,1.33115 z m 3.183004,-13.17071 c -0.205126,-0.14999 -0.329285,-0.4602 -0.329285,-0.8227 0,-0.96498 1.087198,-1.43355 1.715448,-0.73934 0.872429,0.96402 -0.338235,2.3283 -1.386163,1.56204 z m -3.186586,4.78402 c 1.133944,-0.30198 1.619925,-0.56299 1.85054,-0.9939 0.246861,-0.46126 -0.269147,-0.94676 -1.770094,-1.66543 -1.062457,-0.50872 -1.296906,-0.55176 -3.013183,-0.55311 -1.794444,-0.001 -1.910137,0.0225 -3.231141,0.66815 -1.860639,0.90936 -2.118197,1.48499 -0.948717,2.12031 1.182601,0.64245 5.357705,0.89133 7.112595,0.42398 z m -5.26747,-0.96936 c -1.113796,-0.13246 -1.303724,-0.23853 -1.149022,-0.64168 0.08921,-0.23246 0.582443,-0.27351 3.286452,-0.27351 1.749822,0 3.307658,0.0484 3.461858,0.10758 0.350902,0.13466 0.365077,0.53097 0.02367,0.66198 -0.432127,0.16582 -4.555272,0.2726 -5.622968,0.14563 z m -3.7845902,-3.81466 c 0.7343882,-0.537 0.2478022,-1.88618 -0.6802563,-1.88618 -0.5030742,0 -0.9992445,0.51278 -0.9992445,1.03268 0,0.90987 0.9453857,1.39029 1.6795008,0.8535 z"
id="path865" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 7.5967544,-6.2400058 c 0,0 -0.041418,0.1031026 -0.02862,0.2093299 0.012798,0.1062273 0.084435,0.1741091 0.084435,0.1741091 l -0.083147,0.088222 c 0,0 -0.1053512,-0.138166 -0.073877,-0.2774823 0.031474,-0.1393166 0.1012092,-0.1941783 0.101209,-0.1941787 z"
id="path1039" />
</g>
<g
id="layer2"
style="display:none" />
<g
id="layer3"
style="display:none" />
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="120"
height="120"
viewBox="0 0 31.75 31.75"
version="1.1"
id="svg8"
sodipodi:docname="penguin3.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:dc="http://purl.org/dc/elements/1.1/">
<sodipodi:namedview
id="namedview906"
pagecolor="#505050"
bordercolor="#ffffff"
borderopacity="1"
inkscape:pageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
showgrid="false"
inkscape:zoom="6.2083333"
inkscape:cx="6.8456376"
inkscape:cy="60"
inkscape:window-width="1702"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg8" />
<defs
id="defs2" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
style="display:none">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 7.5967544,-6.2400058 c 0,0 -0.041418,0.1031026 -0.02862,0.2093299 0.012798,0.1062273 0.084435,0.1741091 0.084435,0.1741091 l -0.083147,0.088222 c 0,0 -0.1053512,-0.138166 -0.073877,-0.2774823 0.031474,-0.1393166 0.1012092,-0.1941783 0.101209,-0.1941787 z"
id="path1039" />
</g>
<g
id="layer2"
style="display:none" />
<g
id="layer3"
style="display:inline;fill:#ffffff">
<path
style="display:inline;fill:#ffffff;stroke-width:0.294273"
d="M 13.219872,31.561152 C 7.7639295,30.619019 3.1854282,26.04262 2.2530273,20.599337 1.3266857,15.191452 4.3731265,9.445337 9.4587119,7.0081512 12.617064,5.4945709 15.836366,2.8084407 16.825842,0.86116517 L 17.263414,0 17.622933,0.67182296 c 0.19778,0.36951884 0.313308,1.25361504 0.256757,1.96469194 -0.05655,0.7110726 -0.05991,1.2928605 -0.0077,1.2928605 0.05242,0 0.52936,-0.3310521 1.059852,-0.7356803 0.530492,-0.4046239 1.05135,-0.7356802 1.157461,-0.7356802 0.106124,0 0.192929,0.845204 0.192929,1.8782354 v 1.8782312 l 1.580587,0.7733087 c 7.26375,3.5537888 9.530087,12.9645358 4.6616,19.3568408 -3.030552,3.979101 -8.363039,6.069856 -13.304766,5.216521 z m 5.356014,-0.592886 c 3.720242,-0.783427 7.101118,-3.48828 7.892345,-6.314256 0.336074,-1.200345 0.329308,-1.506279 -0.04795,-2.163583 -0.383774,-0.668636 -0.386199,-0.89544 -0.01957,-1.791532 0.938375,-2.291723 -0.417007,-6.522811 -2.67179,-8.34061 -0.91567,-0.738208 -1.4406,-0.918725 -2.942743,-1.011981 -1.593611,-0.09893 -2.045846,0.0015 -3.523445,0.781916 l -1.694978,0.895411 -1.487934,-0.924941 c -3.376446,-2.0989003 -6.6719039,-1.163244 -8.4524067,2.399829 -1.1527548,2.306846 -1.4375744,4.436294 -0.8292982,6.200159 0.2922024,0.847387 0.3536045,1.421626 0.1714833,1.603781 -0.4403631,0.440338 -0.3350945,2.176004 0.2005035,3.304594 0.7011922,1.477632 2.814632,3.472702 4.6084279,4.350322 2.6015972,1.272827 5.8052792,1.640958 8.7973002,1.010891 z m -5.355571,-7.922727 c -1.664073,-0.348285 -2.79605,-1.176171 -2.79605,-2.044978 0,-0.89302 2.504528,-2.316003 4.490206,-2.551169 1.788767,-0.211822 3.947996,0.437993 5.353112,1.610989 0.846392,0.706597 0.936274,0.903347 0.653807,1.431145 -0.704264,1.315915 -4.845636,2.151609 -7.701075,1.554013 z m 5.207815,-0.725102 c 2.110112,-0.626633 2.388166,-1.069391 1.265061,-2.014417 -1.037329,-0.87285 -2.579593,-1.366029 -4.266265,-1.364263 -1.351595,0.0015 -4.190311,1.218923 -4.45594,1.911124 -0.557627,1.453161 4.291873,2.407531 7.457144,1.467556 z m -6.385357,-1.479496 c 0,-0.384327 0.526071,-0.438355 3.580814,-0.367817 2.54216,0.05868 3.616241,0.189313 3.702949,0.450461 0.0934,0.281224 -0.749501,0.367859 -3.580813,0.367859 -3.128417,0 -3.70295,-0.06991 -3.70295,-0.450469 z M 8.5850578,18.025628 c -0.9467707,-0.352541 -0.9547704,-2.236086 -0.010638,-2.585219 0.907738,-0.335818 1.7102922,-0.0654 1.9836022,0.668419 0.494995,1.328966 -0.625944,2.418182 -1.9726282,1.9168 z m 1.1035225,-1.3128 c 0,-0.499353 -0.7583177,-0.569291 -1.0502946,-0.09685 -0.2218645,0.358966 0.439061,0.898662 0.815537,0.665994 0.1291443,-0.07979 0.2347576,-0.335903 0.2347576,-0.569129 z m 11.2412737,0.988467 c -0.932325,-0.932316 0.159441,-2.708253 1.456467,-2.36909 0.837358,0.218971 1.282295,1.305132 0.858791,2.096457 -0.374285,0.699311 -1.729037,0.858855 -2.315258,0.272633 z m 1.575497,-0.555325 c 0.303266,-0.490676 -0.378838,-1.053354 -0.784743,-0.647403 -0.339562,0.339563 -0.127059,0.967315 0.327478,0.967315 0.142719,0 0.348498,-0.143952 0.457256,-0.319903 z"
id="path913-6" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,261 @@
body {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
background-color: #302f3b;
color: #ffffff;
height: 100%;
font-family: Ubuntu Mono,monospace;
font-size: x-large;
}
.info_box {
margin: auto;
max-width: 40em;
}
.nobr {
white-space: nowrap;
}
.center {
margin: auto;
}
.hidden {
visibility: hidden;
}
/* Tooltip container */
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* If you want dots under the hoverable text */
}
/* Tooltip text */
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: #555;
color: #fff;
text-align: center;
padding: 5px 0;
border-radius: 6px;
/* Position the tooltip text */
position: absolute;
z-index: 1;
top: 125%;
left: 50%;
margin-left: -100px;
/* Fade in tooltip */
opacity: 0;
transition: opacity 0.3s;
}
/* Tooltip arrow */
.tooltip .tooltiptext::after {
content: "";
position: absolute;
bottom: 100%;
left: 100px;
margin-left: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent transparent #555 transparent;
}
/* Show the tooltip text when you mouse over the tooltip container */
.tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
.bulb_box {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
justify-content: space-around;
width: fit-content;
}
.bulb_box > * {
margin: 0.5em;
}
.bulb_controls {
display: flex;
flex-direction: row;
align-items: center;
padding-right: 0.5em;
background: #56636e;
border: solid 0.25em #5b3f63;
}
.bulb_map {
background: url(images/blueprint_bg.png);
background-size: auto;
background-size: 1em;
padding: 2rem;
border: solid 0.4rem #5b3f63;
}
.bulb_map::after {
position: absolute;
width: 100%;
height: 100%;
content: #0003;
}
.bulb_group {
color: black;
text-align: center;
background: #aaaaaaaa;
border: solid 0.25rem white;
border-radius: 0.25rem;
font-size: 2em;
position: absolute;
box-shadow: #0006 0.1em 0.1em 0.1em;
transition: 0.3s ease-in-out;
}
.bulb_group_selected {
border-color: gold;
scale: 1.05;
transition: 0.3s ease-in-out;
}
.bulb_power_button {
width: 70px;
height: 175px;
border-radius: 50px;
margin-top: 1em;
margin-bottom: 1em;
background-color: #b5b5b5;
transition: background-color 0.2s ease-in-out;
}
.bulb_power_button_on {
}
.bulb_power_button > div {
position: absolute;
width: 40px;
margin-left: 8.75px;
color: #000;
font-weight: 700;
}
.bulb_power_button > #lever_face {
height: 40px;
background-color: #cccccc;
margin-top: 25px;
transition: 0.3s ease-in-out;
}
.bulb_power_button_on > #lever_face {
margin-top: -65px;
}
.bulb_power_button > #lever_stem {
height: 30px;
background-color: #eeeeee;
margin-top: 0px;
transition: 0.3s ease-in-out;
}
.bulb_power_button_on > #lever_stem {
margin-top: -30px;
}
.bulb_power_button > #off_label {
height: 30px;
background-color: #cccccc;
padding-top: 15px;
margin-top: -45px;
}
.bulb_power_button > #on_label {
height: 30px;
background-color: #cccccc;
padding-top: 15px;
margin-top: 0px;
}
.bulb_power_button > #switch_socket {
width: 47.5px;
margin-top: -55px;
margin-left: 0;
height: 100px;
background-color: #4f4f4f;
border: solid 5px #fff;
border-radius: 5px;
}
.color_picker {
display: flex;
flex-direction: row;
padding: 0.5em;
}
.color_wheel {
background-image: url(/images/hsb.png);
width: 200px;
height: 200px;
background-size: contain;
border: solid 5px #a4829c;
border-radius: 200px;
}
.color_wheel_marker {
width: 6px;
height: 6px;
background: white;
border: solid 2px black;
border-radius: 10px;
position: absolute;
pointer-events: none;
transition: margin 0.1s ease-out;
}
.brightness_bar {
width: 30px;
height: 200px;
margin-left: 10px;
border: solid 5px #a4829c;
}
.temperature_bar {
width: 30px;
height: 200px;
margin-left: 10px;
background: linear-gradient(0deg, #f81, #eff);
border: solid 5px #a4829c;
}
.color_bar_marker {
width: 30px;
height: 8px;
border-top: solid 2px black;
border-bottom: solid 2px black;
position: absolute;
background: white;
pointer-events: none;
transition: margin 0.1s ease-out;
}

View File

@ -0,0 +1,59 @@
@keyframes penguin-spin {
0% {
transform: perspective(36em) rotateY(-90deg);
background-image: url(/images/penguin1.svg);
}
24.99% {
transform: perspective(36em) rotateY(90deg);
background-image: url(/images/penguin1.svg);
}
25% {
transform: perspective(36em) rotateY(90deg);
background-image: url(/images/penguin2.svg);
}
49.99% {
transform: perspective(36em) rotateY(270deg);
background-image: url(/images/penguin2.svg);
}
50% {
transform: perspective(36em) rotateY(270deg);
background-image: url(/images/penguin3.svg);
}
74.99% {
transform: perspective(36em) rotateY(450deg);
background-image: url(/images/penguin3.svg);
}
75% {
transform: perspective(36em) rotateY(450deg);
background-image: url(/images/penguin2.svg);
}
99.99% {
transform: perspective(36em) rotateY(630deg);
background-image: url(/images/penguin2.svg);
}
100% {
transform: perspective(36em) rotateY(630deg);
background-image: url(/images/penguin1.svg);
}
}
.penguin {
width: 12em;
height: 12em;
margin: auto;
margin-top: 1em;
margin-bottom: 1em;
background-image: url(/images/penguin1.svg);
background-size: contain;
animation-name: penguin-spin;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-timing-function: cubic-bezier(0.1, 0.5, 0.9, 0.5);
}
.penguin_small {
width: 1.5em;
height: 1.5em;
margin-top: 0em;
margin-bottom: 0em;
}