Compare commits
2 Commits
9ed2fa8b53
...
1a3102257e
Author | SHA1 | Date | |
---|---|---|---|
1a3102257e | |||
3ede04c31b |
83
src-tauri/Cargo.lock
generated
83
src-tauri/Cargo.lock
generated
@ -181,7 +181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a"
|
checksum = "497049e9477329f8f6a559972ee42e117487d01d1e8c2cc9f836ea6fa23a9e1a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml 0.5.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -189,9 +189,6 @@ name = "cc"
|
|||||||
version = "1.0.79"
|
version = "1.0.79"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||||
dependencies = [
|
|
||||||
"jobserver",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cesu8"
|
name = "cesu8"
|
||||||
@ -270,6 +267,12 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "color_space"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3776b2bcc4e914db501bb9be9572dd706e344b9eb8f882894f3daa651d281381"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "combine"
|
name = "combine"
|
||||||
version = "4.6.6"
|
version = "4.6.6"
|
||||||
@ -1046,6 +1049,12 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hex"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "html5ever"
|
name = "html5ever"
|
||||||
version = "0.25.2"
|
version = "0.25.2"
|
||||||
@ -1245,15 +1254,6 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "json-patch"
|
name = "json-patch"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@ -1289,15 +1289,6 @@ version = "0.2.140"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
|
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libwebp-sys"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "439fd1885aa28937e7edcd68d2e793cb4a22f8733460d2519fbafd2b215672bf"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "line-wrap"
|
name = "line-wrap"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@ -2155,6 +2146,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "1.14.0"
|
version = "1.14.0"
|
||||||
@ -2364,7 +2364,7 @@ dependencies = [
|
|||||||
"cfg-expr 0.9.1",
|
"cfg-expr 0.9.1",
|
||||||
"heck 0.3.3",
|
"heck 0.3.3",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml",
|
"toml 0.5.11",
|
||||||
"version-compare 0.0.11",
|
"version-compare 0.0.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2377,7 +2377,7 @@ dependencies = [
|
|||||||
"cfg-expr 0.11.0",
|
"cfg-expr 0.11.0",
|
||||||
"heck 0.4.1",
|
"heck 0.4.1",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml",
|
"toml 0.5.11",
|
||||||
"version-compare 0.1.1",
|
"version-compare 0.1.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2647,21 +2647,21 @@ name = "test-demo"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64 0.21.0",
|
"color_space",
|
||||||
"core-graphics",
|
"core-graphics",
|
||||||
"display-info",
|
"display-info",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
"paris",
|
"paris",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"png",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"toml 0.7.3",
|
||||||
"url-build-parse",
|
"url-build-parse",
|
||||||
"webp",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2782,11 +2782,26 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
|
"toml_datetime",
|
||||||
|
"toml_edit",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
|
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
@ -2795,6 +2810,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
|
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned",
|
||||||
"toml_datetime",
|
"toml_datetime",
|
||||||
"winnow",
|
"winnow",
|
||||||
]
|
]
|
||||||
@ -3044,16 +3061,6 @@ dependencies = [
|
|||||||
"system-deps 6.0.3",
|
"system-deps 6.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "webp"
|
|
||||||
version = "0.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf022f821f166079a407d000ab57e84de020e66ffbbf4edde999bc7d6e371cae"
|
|
||||||
dependencies = [
|
|
||||||
"image",
|
|
||||||
"libwebp-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.19.1"
|
version = "0.19.1"
|
||||||
@ -3310,7 +3317,7 @@ version = "0.1.12"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml",
|
"toml 0.5.11",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -16,11 +16,8 @@ tauri-build = { version = "1.2", features = [] }
|
|||||||
tauri = { version = "1.2", features = ["shell-open"] }
|
tauri = { version = "1.2", features = ["shell-open"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
webp = "0.2.2"
|
|
||||||
base64 = "0.21.0"
|
|
||||||
core-graphics = "0.22.3"
|
core-graphics = "0.22.3"
|
||||||
display-info = "0.4.1"
|
display-info = "0.4.1"
|
||||||
png = "0.17.7"
|
|
||||||
anyhow = "1.0.69"
|
anyhow = "1.0.69"
|
||||||
tokio = {version = "1.26.0", features = ["full"] }
|
tokio = {version = "1.26.0", features = ["full"] }
|
||||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||||
@ -28,6 +25,9 @@ log = "0.4.17"
|
|||||||
env_logger = "0.10.0"
|
env_logger = "0.10.0"
|
||||||
percent-encoding = "2.2.0"
|
percent-encoding = "2.2.0"
|
||||||
url-build-parse = "9.0.0"
|
url-build-parse = "9.0.0"
|
||||||
|
color_space = "0.5.3"
|
||||||
|
hex = "0.4.3"
|
||||||
|
toml = "0.7.3"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||||
|
139
src-tauri/src/ambient_light/config.rs
Normal file
139
src-tauri/src/ambient_light/config.rs
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
use std::env::current_dir;
|
||||||
|
|
||||||
|
use paris::{error, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::api::path::config_dir;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum Border {
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LedStripConfigOfBorders {
|
||||||
|
pub top: Option<LedStripConfig>,
|
||||||
|
pub bottom: Option<LedStripConfig>,
|
||||||
|
pub left: Option<LedStripConfig>,
|
||||||
|
pub right: Option<LedStripConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LedStripConfig {
|
||||||
|
pub index: usize,
|
||||||
|
pub border: Border,
|
||||||
|
pub display_id: u32,
|
||||||
|
pub start_pos: usize,
|
||||||
|
pub len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedStripConfig {
|
||||||
|
pub async fn read_config() -> anyhow::Result<Vec<Self>> {
|
||||||
|
// config path
|
||||||
|
let path = config_dir()
|
||||||
|
.unwrap_or(current_dir().unwrap())
|
||||||
|
.join("led_strip_config.toml");
|
||||||
|
|
||||||
|
let exists = tokio::fs::try_exists(path.clone())
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to check config file exists: {}", e))?;
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
let config = tokio::fs::read_to_string(path).await?;
|
||||||
|
|
||||||
|
let config: Vec<Self> = toml::from_str(&config)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
} else {
|
||||||
|
info!("config file not exist, fallback to default config");
|
||||||
|
Ok(Self::get_default_config().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn write_config(configs: &Vec<Self>) -> anyhow::Result<()> {
|
||||||
|
let path = config_dir()
|
||||||
|
.unwrap_or(current_dir().unwrap())
|
||||||
|
.join("led_strip_config.toml");
|
||||||
|
|
||||||
|
let config = toml::to_string(configs)
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
|
||||||
|
|
||||||
|
tokio::fs::write(path, config)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("Failed to write config file: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_default_config() -> anyhow::Result<Vec<Self>> {
|
||||||
|
let displays = display_info::DisplayInfo::all().map_err(|e| {
|
||||||
|
error!("can not list display info: {}", e);
|
||||||
|
anyhow::anyhow!("can not list display info: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut configs = Vec::new();
|
||||||
|
for (i, display) in displays.iter().enumerate() {
|
||||||
|
for j in 0..4 {
|
||||||
|
let config = Self {
|
||||||
|
index: j + i * 4 * 30,
|
||||||
|
display_id: display.id,
|
||||||
|
border: match j {
|
||||||
|
0 => Border::Top,
|
||||||
|
1 => Border::Bottom,
|
||||||
|
2 => Border::Left,
|
||||||
|
3 => Border::Right,
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
start_pos: 0,
|
||||||
|
len: 30,
|
||||||
|
};
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(configs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayConfig {
|
||||||
|
pub id: u32,
|
||||||
|
pub index_of_display: usize,
|
||||||
|
pub display_width: usize,
|
||||||
|
pub display_height: usize,
|
||||||
|
pub led_strip_of_borders: LedStripConfigOfBorders,
|
||||||
|
pub scale_factor: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedStripConfigOfBorders {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
top: None,
|
||||||
|
bottom: None,
|
||||||
|
left: None,
|
||||||
|
right: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayConfig {
|
||||||
|
pub fn default(
|
||||||
|
id: u32,
|
||||||
|
index_of_display: usize,
|
||||||
|
display_width: usize,
|
||||||
|
display_height: usize,
|
||||||
|
scale_factor: f32,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
index_of_display,
|
||||||
|
display_width,
|
||||||
|
display_height,
|
||||||
|
led_strip_of_borders: LedStripConfigOfBorders::default(),
|
||||||
|
scale_factor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src-tauri/src/ambient_light/mod.rs
Normal file
3
src-tauri/src/ambient_light/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod config;
|
||||||
|
|
||||||
|
pub use config::*;
|
13
src-tauri/src/display/brightness.rs
Normal file
13
src-tauri/src/display/brightness.rs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum Brightness {
|
||||||
|
Relative(i16),
|
||||||
|
Absolute(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayBrightness {
|
||||||
|
pub brightness: Brightness,
|
||||||
|
pub display_index: usize,
|
||||||
|
}
|
36
src-tauri/src/display/display_config.rs
Normal file
36
src-tauri/src/display/display_config.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayConfig {
|
||||||
|
pub id: usize,
|
||||||
|
pub brightness: u16,
|
||||||
|
pub max_brightness: u16,
|
||||||
|
pub min_brightness: u16,
|
||||||
|
pub contrast: u16,
|
||||||
|
pub max_contrast: u16,
|
||||||
|
pub min_contrast: u16,
|
||||||
|
pub mode: u16,
|
||||||
|
pub max_mode: u16,
|
||||||
|
pub min_mode: u16,
|
||||||
|
pub last_modified_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayConfig {
|
||||||
|
pub fn default(index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
id: index,
|
||||||
|
brightness: 30,
|
||||||
|
contrast: 50,
|
||||||
|
mode: 0,
|
||||||
|
last_modified_at: SystemTime::now(),
|
||||||
|
max_brightness: 100,
|
||||||
|
min_brightness: 0,
|
||||||
|
max_contrast: 100,
|
||||||
|
min_contrast: 0,
|
||||||
|
max_mode: 15,
|
||||||
|
min_mode: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
186
src-tauri/src/display/manager.rs
Normal file
186
src-tauri/src/display/manager.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::HashMap,
|
||||||
|
ops::Sub,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::Config;
|
||||||
|
use ddc_hi::Display;
|
||||||
|
use paris::{error, info, warn};
|
||||||
|
use tauri::async_runtime::Mutex;
|
||||||
|
use tokio::sync::{broadcast, OwnedMutexGuard};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{display::Brightness, models, rpc};
|
||||||
|
|
||||||
|
use super::{display_config::DisplayConfig, DisplayBrightness};
|
||||||
|
use ddc_hi::Ddc;
|
||||||
|
|
||||||
|
pub struct Manager {
|
||||||
|
displays: Arc<Mutex<HashMap<usize, Arc<Mutex<DisplayConfig>>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn global() -> &'static Self {
|
||||||
|
static DISPLAY_MANAGER: once_cell::sync::OnceCell<Manager> =
|
||||||
|
once_cell::sync::OnceCell::new();
|
||||||
|
|
||||||
|
DISPLAY_MANAGER.get_or_init(|| Self::create())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create() -> Self {
|
||||||
|
let instance = Self {
|
||||||
|
displays: Arc::new(Mutex::new(HashMap::new())),
|
||||||
|
};
|
||||||
|
instance
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn subscribe_display_brightness(&self) {
|
||||||
|
let rpc = rpc::Manager::global().await;
|
||||||
|
|
||||||
|
let mut rx = rpc.client().subscribe_change_display_brightness_rx();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Ok(display_brightness) = rx.recv().await {
|
||||||
|
if let Err(err) = self.set_display_brightness(display_brightness).await {
|
||||||
|
error!("set_display_brightness failed. {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_display_config_by_ddc(index: usize) -> anyhow::Result<DisplayConfig> {
|
||||||
|
let mut displays = Display::enumerate();
|
||||||
|
match displays.get_mut(index) {
|
||||||
|
Some(display) => {
|
||||||
|
let mut config = DisplayConfig::default(index);
|
||||||
|
match display.handle.get_vcp_feature(0x10) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.max_brightness = value.maximum();
|
||||||
|
config.min_brightness = 0;
|
||||||
|
config.brightness = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
match display.handle.get_vcp_feature(0x12) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.max_contrast = value.maximum();
|
||||||
|
config.min_contrast = 0;
|
||||||
|
config.contrast = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
match display.handle.get_vcp_feature(0xdc) {
|
||||||
|
Ok(value) => {
|
||||||
|
config.max_mode = value.maximum();
|
||||||
|
config.min_mode = 0;
|
||||||
|
config.mode = value.value();
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
None => anyhow::bail!("display#{} is missed.", index),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_display(&self, index: usize) -> anyhow::Result<OwnedMutexGuard<DisplayConfig>> {
|
||||||
|
let mut displays = self.displays.lock().await;
|
||||||
|
match displays.get_mut(&index) {
|
||||||
|
Some(config) => {
|
||||||
|
let mut config = config.to_owned().lock_owned().await;
|
||||||
|
if config.last_modified_at > SystemTime::now().sub(Duration::from_secs(10)) {
|
||||||
|
info!("cached");
|
||||||
|
return Ok(config);
|
||||||
|
}
|
||||||
|
return match Self::read_display_config_by_ddc(index) {
|
||||||
|
Ok(config) => {
|
||||||
|
let id = config.id;
|
||||||
|
let value = Arc::new(Mutex::new(config));
|
||||||
|
let valueGuard = value.clone().lock_owned().await;
|
||||||
|
displays.insert(id, value);
|
||||||
|
info!("read form ddc");
|
||||||
|
Ok(valueGuard)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"can not read config from display by ddc, use CACHED value. {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
config.last_modified_at = SystemTime::now();
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let config = Self::read_display_config_by_ddc(index).map_err(|err| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"can not read config from display by ddc,use DEFAULT value. {:?}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let id = config.id;
|
||||||
|
let value = Arc::new(Mutex::new(config));
|
||||||
|
let valueGuard = value.clone().lock_owned().await;
|
||||||
|
displays.insert(id, value);
|
||||||
|
Ok(valueGuard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_display_brightness(
|
||||||
|
&self,
|
||||||
|
display_brightness: DisplayBrightness,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match Display::enumerate().get_mut(display_brightness.display_index) {
|
||||||
|
Some(display) => {
|
||||||
|
match self.get_display(display_brightness.display_index).await {
|
||||||
|
Ok(mut config) => {
|
||||||
|
let curr = config.brightness;
|
||||||
|
info!("curr_brightness: {:?}", curr);
|
||||||
|
let mut target = match display_brightness.brightness {
|
||||||
|
Brightness::Relative(v) => curr.wrapping_add_signed(v),
|
||||||
|
Brightness::Absolute(v) => v,
|
||||||
|
};
|
||||||
|
if target.gt(&config.max_brightness) {
|
||||||
|
target = config.max_brightness;
|
||||||
|
} else if target.lt(&config.min_brightness) {
|
||||||
|
target = config.min_brightness;
|
||||||
|
}
|
||||||
|
config.brightness = target;
|
||||||
|
display
|
||||||
|
.handle
|
||||||
|
.set_vcp_feature(0x10, target as u16)
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not set brightness. {:?}", err))?;
|
||||||
|
|
||||||
|
let rpc = rpc::Manager::global().await;
|
||||||
|
|
||||||
|
rpc.publish_desktop_cmd(
|
||||||
|
format!("display{}/brightness", display_brightness.display_index).as_str(),
|
||||||
|
target.to_be_bytes().to_vec(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
info!(
|
||||||
|
"can not get display#{} brightness. {:?}",
|
||||||
|
display_brightness.display_index, err
|
||||||
|
);
|
||||||
|
if let Brightness::Absolute(v) = display_brightness.brightness {
|
||||||
|
display.handle.set_vcp_feature(0x10, v).map_err(|err| {
|
||||||
|
anyhow::anyhow!("can not set brightness. {:?}", err)
|
||||||
|
})?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
warn!("display#{} is not found.", display_brightness.display_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
11
src-tauri/src/display/mod.rs
Normal file
11
src-tauri/src/display/mod.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// mod brightness;
|
||||||
|
// mod manager;
|
||||||
|
mod display_config;
|
||||||
|
|
||||||
|
pub use display_config::*;
|
||||||
|
|
||||||
|
// pub use brightness::*;
|
||||||
|
// pub use manager::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
54
src-tauri/src/led_color.rs
Normal file
54
src-tauri/src/led_color.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
use color_space::{Hsv, Rgb};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct LedColor {
|
||||||
|
bits: [u8; 3],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedColor {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self { bits: [0, 0, 0] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
|
Self { bits: [r, g, b] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
||||||
|
let rgb = Rgb::from(Hsv::new(h, s, v));
|
||||||
|
Self { bits: [rgb.r as u8, rgb.g as u8, rgb.b as u8] }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_rgb(&self) -> [u8; 3] {
|
||||||
|
self.bits
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
self.bits.iter().any(|bit| *bit == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
|
self.bits = [r, g, b];
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
|
self.bits = [
|
||||||
|
(self.bits[0] / 2 + r / 2),
|
||||||
|
(self.bits[1] / 2 + g / 2),
|
||||||
|
(self.bits[2] / 2 + b / 2),
|
||||||
|
];
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for LedColor {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
let hex = format!("#{}", hex::encode(self.bits));
|
||||||
|
serializer.serialize_str(hex.as_str())
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,19 @@
|
|||||||
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
mod ambient_light;
|
||||||
|
mod display;
|
||||||
|
mod led_color;
|
||||||
pub mod screenshot;
|
pub mod screenshot;
|
||||||
mod screenshot_manager;
|
mod screenshot_manager;
|
||||||
|
|
||||||
use base64::Engine;
|
use ambient_light::LedStripConfig;
|
||||||
use core_graphics::display::{
|
use core_graphics::display::{
|
||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||||
};
|
};
|
||||||
use display_info::DisplayInfo;
|
use display_info::DisplayInfo;
|
||||||
use paris::{error, info};
|
use paris::{error, info, warn};
|
||||||
|
use screenshot::Screenshot;
|
||||||
use screenshot_manager::ScreenshotManager;
|
use screenshot_manager::ScreenshotManager;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::to_string;
|
use serde_json::to_string;
|
||||||
@ -52,92 +56,6 @@ fn list_display_info() -> Result<String, String> {
|
|||||||
Ok(json_str)
|
Ok(json_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
fn take_screenshot(display_id: u32, scale_factor: f32) -> Result<String, String> {
|
|
||||||
let exec = || {
|
|
||||||
println!("take_screenshot");
|
|
||||||
let start_at = std::time::Instant::now();
|
|
||||||
|
|
||||||
let cg_display = CGDisplay::new(display_id);
|
|
||||||
let cg_image = CGDisplay::screenshot(
|
|
||||||
cg_display.bounds(),
|
|
||||||
kCGWindowListOptionOnScreenOnly,
|
|
||||||
kCGNullWindowID,
|
|
||||||
kCGWindowImageDefault,
|
|
||||||
)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
|
|
||||||
// println!("take screenshot took {}ms", start_at.elapsed().as_millis());
|
|
||||||
|
|
||||||
let buffer = cg_image.data();
|
|
||||||
let bytes_per_row = cg_image.bytes_per_row() as f32;
|
|
||||||
|
|
||||||
let height = cg_image.height();
|
|
||||||
let width = cg_image.width();
|
|
||||||
|
|
||||||
let image_height = (height as f32 / scale_factor) as u32;
|
|
||||||
let image_width = (width as f32 / scale_factor) as u32;
|
|
||||||
|
|
||||||
// println!(
|
|
||||||
// "raw image: {}x{}, output image: {}x{}",
|
|
||||||
// width, height, image_width, image_height
|
|
||||||
// );
|
|
||||||
// // from bitmap vec
|
|
||||||
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
|
|
||||||
|
|
||||||
for y in 0..image_height {
|
|
||||||
for x in 0..image_width {
|
|
||||||
let offset =
|
|
||||||
(((y as f32) * bytes_per_row + (x as f32) * 4.0) * scale_factor) as usize;
|
|
||||||
let b = buffer[offset];
|
|
||||||
let g = buffer[offset + 1];
|
|
||||||
let r = buffer[offset + 2];
|
|
||||||
let offset = (y * image_width + x) as usize;
|
|
||||||
image_buffer[offset * 3] = r;
|
|
||||||
image_buffer[offset * 3 + 1] = g;
|
|
||||||
image_buffer[offset * 3 + 2] = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!(
|
|
||||||
"convert to image buffer took {}ms",
|
|
||||||
start_at.elapsed().as_millis()
|
|
||||||
);
|
|
||||||
|
|
||||||
// to png image
|
|
||||||
// let mut image_png = Vec::new();
|
|
||||||
// let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
|
|
||||||
// encoder.set_color(png::ColorType::Rgb);
|
|
||||||
// encoder.set_depth(png::BitDepth::Eight);
|
|
||||||
|
|
||||||
// let mut writer = encoder
|
|
||||||
// .write_header()
|
|
||||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
// writer
|
|
||||||
// .write_image_data(&image_buffer)
|
|
||||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
// writer
|
|
||||||
// .finish()
|
|
||||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
// println!("encode to png took {}ms", start_at.elapsed().as_millis());
|
|
||||||
let image_webp =
|
|
||||||
webp::Encoder::from_rgb(&image_buffer, image_width, image_height).encode(90f32);
|
|
||||||
// // base64 image
|
|
||||||
let mut image_base64 = String::new();
|
|
||||||
image_base64.push_str("data:image/webp;base64,");
|
|
||||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_webp);
|
|
||||||
image_base64.push_str(encoded.as_str());
|
|
||||||
|
|
||||||
println!("took {}ms", start_at.elapsed().as_millis());
|
|
||||||
println!("image_base64: {}", image_base64.len());
|
|
||||||
|
|
||||||
Ok(image_base64)
|
|
||||||
};
|
|
||||||
|
|
||||||
exec().map_err(|e: anyhow::Error| {
|
|
||||||
println!("error: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn subscribe_encoded_screenshot_updated(
|
async fn subscribe_encoded_screenshot_updated(
|
||||||
window: tauri::Window,
|
window: tauri::Window,
|
||||||
@ -153,6 +71,72 @@ async fn subscribe_encoded_screenshot_updated(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn read_led_strip_configs() -> Result<Vec<ambient_light::LedStripConfig>, String> {
|
||||||
|
let configs = ambient_light::LedStripConfig::read_config()
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("can not read led strip configs: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})?;
|
||||||
|
Ok(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn write_led_strip_configs(
|
||||||
|
configs: Vec<ambient_light::LedStripConfig>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
ambient_light::LedStripConfig::write_config(&configs)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("can not write led strip configs: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_led_strips_sample_points(
|
||||||
|
config: LedStripConfig,
|
||||||
|
) -> Result<Vec<screenshot::LedSamplePoints>, String> {
|
||||||
|
let displays = DisplayInfo::all().map_err(|e| {
|
||||||
|
error!("can not read led strip config: {}", e);
|
||||||
|
e.to_string()
|
||||||
|
});
|
||||||
|
let display = displays?.into_iter().find(|d| d.id == config.display_id);
|
||||||
|
|
||||||
|
if let None = display {
|
||||||
|
return Err(format!("display not found: {}", config.display_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let display = display.unwrap();
|
||||||
|
|
||||||
|
let config = screenshot::Screenshot::get_sample_point(
|
||||||
|
config,
|
||||||
|
display.width as usize,
|
||||||
|
display.height as usize,
|
||||||
|
);
|
||||||
|
Ok(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_one_edge_colors(
|
||||||
|
display_id: u32,
|
||||||
|
sample_points: Vec<screenshot::LedSamplePoints>,
|
||||||
|
) -> Result<Vec<led_color::LedColor>, String> {
|
||||||
|
let screenshot_manager = ScreenshotManager::global().await;
|
||||||
|
let channels = screenshot_manager.channels.read().await;
|
||||||
|
if let Some(rx) = channels.get(&display_id) {
|
||||||
|
let rx = rx.clone();
|
||||||
|
let screenshot = rx.borrow().clone();
|
||||||
|
let bytes = screenshot.bytes.read().await;
|
||||||
|
let colors =
|
||||||
|
Screenshot::get_one_edge_colors(&sample_points, &bytes, screenshot.bytes_per_row);
|
||||||
|
Ok(colors)
|
||||||
|
} else {
|
||||||
|
Err(format!("display not found: {}", display_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
@ -162,9 +146,12 @@ async fn main() {
|
|||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
greet,
|
||||||
take_screenshot,
|
|
||||||
list_display_info,
|
list_display_info,
|
||||||
subscribe_encoded_screenshot_updated
|
subscribe_encoded_screenshot_updated,
|
||||||
|
read_led_strip_configs,
|
||||||
|
write_led_strip_configs,
|
||||||
|
get_led_strips_sample_points,
|
||||||
|
get_one_edge_colors
|
||||||
])
|
])
|
||||||
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
|
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
|
||||||
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
||||||
@ -212,20 +199,33 @@ async fn main() {
|
|||||||
let screenshot = rx.borrow().clone();
|
let screenshot = rx.borrow().clone();
|
||||||
let bytes = screenshot.bytes.read().await;
|
let bytes = screenshot.bytes.read().await;
|
||||||
|
|
||||||
let (scale_factor, width, height) = if url.query.is_some()
|
let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some()
|
||||||
&& url.query.as_ref().unwrap().contains_key("height")
|
&& url.query.as_ref().unwrap().contains_key("height")
|
||||||
&& url.query.as_ref().unwrap().contains_key("width")
|
&& url.query.as_ref().unwrap().contains_key("width")
|
||||||
{
|
{
|
||||||
let width =
|
let width = url.query.as_ref().unwrap()["width"]
|
||||||
url.query.as_ref().unwrap()["width"].parse::<u32>().unwrap();
|
.parse::<u32>()
|
||||||
|
.map_err(|err| {
|
||||||
|
warn!("width parse error: {}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
let height = url.query.as_ref().unwrap()["height"]
|
let height = url.query.as_ref().unwrap()["height"]
|
||||||
.parse::<u32>()
|
.parse::<u32>()
|
||||||
.unwrap();
|
.map_err(|err| {
|
||||||
(screenshot.width as f32 / width as f32, width, height)
|
warn!("height parse error: {}", err);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
(
|
||||||
|
screenshot.width as f32 / width as f32,
|
||||||
|
screenshot.height as f32 / height as f32,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
log::debug!("scale by scale_factor");
|
log::debug!("scale by scale_factor");
|
||||||
let scale_factor = screenshot.scale_factor;
|
let scale_factor = screenshot.scale_factor;
|
||||||
(
|
(
|
||||||
|
scale_factor,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
(screenshot.width as f32 / scale_factor) as u32,
|
(screenshot.width as f32 / scale_factor) as u32,
|
||||||
(screenshot.height as f32 / scale_factor) as u32,
|
(screenshot.height as f32 / scale_factor) as u32,
|
||||||
@ -245,8 +245,9 @@ async fn main() {
|
|||||||
|
|
||||||
for y in 0..height {
|
for y in 0..height {
|
||||||
for x in 0..width {
|
for x in 0..width {
|
||||||
let offset = ((y as f32) * scale_factor) as usize * bytes_per_row as usize
|
let offset = ((y as f32) * scale_factor_y).floor() as usize
|
||||||
+ ((x as f32) * scale_factor) as usize * 4;
|
* bytes_per_row as usize
|
||||||
|
+ ((x as f32) * scale_factor_x).floor() as usize * 4;
|
||||||
let b = bytes[offset];
|
let b = bytes[offset];
|
||||||
let g = bytes[offset + 1];
|
let g = bytes[offset + 1];
|
||||||
let r = bytes[offset + 2];
|
let r = bytes[offset + 2];
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
use std::iter;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::async_runtime::RwLock;
|
use tauri::async_runtime::RwLock;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
ambient_light::{DisplayConfig, LedStripConfig},
|
||||||
|
led_color::LedColor,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Screenshot {
|
pub struct Screenshot {
|
||||||
pub display_id: u32,
|
pub display_id: u32,
|
||||||
@ -11,6 +17,7 @@ pub struct Screenshot {
|
|||||||
pub bytes_per_row: usize,
|
pub bytes_per_row: usize,
|
||||||
pub bytes: Arc<RwLock<Vec<u8>>>,
|
pub bytes: Arc<RwLock<Vec<u8>>>,
|
||||||
pub scale_factor: f32,
|
pub scale_factor: f32,
|
||||||
|
pub sample_points: ScreenSamplePoints,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screenshot {
|
impl Screenshot {
|
||||||
@ -21,6 +28,7 @@ impl Screenshot {
|
|||||||
bytes_per_row: usize,
|
bytes_per_row: usize,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
scale_factor: f32,
|
scale_factor: f32,
|
||||||
|
sample_points: ScreenSamplePoints,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
display_id,
|
display_id,
|
||||||
@ -29,14 +37,251 @@ impl Screenshot {
|
|||||||
bytes_per_row,
|
bytes_per_row,
|
||||||
bytes: Arc::new(RwLock::new(bytes)),
|
bytes: Arc::new(RwLock::new(bytes)),
|
||||||
scale_factor,
|
scale_factor,
|
||||||
|
sample_points,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_sample_point(
|
||||||
|
config: LedStripConfig,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
) -> Vec<LedSamplePoints> {
|
||||||
|
match config.border {
|
||||||
|
crate::ambient_light::Border::Top => {
|
||||||
|
Self::get_one_edge_sample_points(height / 8, width, config.len, 5)
|
||||||
|
}
|
||||||
|
crate::ambient_light::Border::Bottom => {
|
||||||
|
let points = Self::get_one_edge_sample_points(height / 9, width, config.len, 1);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (x, height - y)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
crate::ambient_light::Border::Left => {
|
||||||
|
let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 1);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (y, x)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
crate::ambient_light::Border::Right => {
|
||||||
|
let points = Self::get_one_edge_sample_points(width / 16, height, config.len, 1);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (width - y, x)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints {
|
||||||
|
let top = match config.led_strip_of_borders.top {
|
||||||
|
Some(led_strip_config) => Self::get_one_edge_sample_points(
|
||||||
|
config.display_height / 8,
|
||||||
|
config.display_width,
|
||||||
|
led_strip_config.len,
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let bottom: Vec<LedSamplePoints> = match config.led_strip_of_borders.bottom {
|
||||||
|
Some(led_strip_config) => {
|
||||||
|
let points = Self::get_one_edge_sample_points(
|
||||||
|
config.display_height / 9,
|
||||||
|
config.display_width,
|
||||||
|
led_strip_config.len,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x, y)| (x, config.display_height - y))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let left: Vec<LedSamplePoints> = match config.led_strip_of_borders.left {
|
||||||
|
Some(led_strip_config) => {
|
||||||
|
let points = Self::get_one_edge_sample_points(
|
||||||
|
config.display_width / 16,
|
||||||
|
config.display_height,
|
||||||
|
led_strip_config.len,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups.into_iter().map(|(x, y)| (y, x)).collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let right: Vec<LedSamplePoints> = match config.led_strip_of_borders.right {
|
||||||
|
Some(led_strip_config) => {
|
||||||
|
let points = Self::get_one_edge_sample_points(
|
||||||
|
config.display_width / 16,
|
||||||
|
config.display_height,
|
||||||
|
led_strip_config.len,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x, y)| (config.display_width - y, x))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ScreenSamplePoints {
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_one_edge_sample_points(
|
||||||
|
width: usize,
|
||||||
|
length: usize,
|
||||||
|
leds: usize,
|
||||||
|
single_axis_points: usize,
|
||||||
|
) -> Vec<LedSamplePoints> {
|
||||||
|
let cell_size_x = length as f64 / single_axis_points as f64 / leds as f64;
|
||||||
|
let cell_size_y = width / single_axis_points;
|
||||||
|
|
||||||
|
let point_start_y = cell_size_y / 2;
|
||||||
|
let point_start_x = cell_size_x / 2.0;
|
||||||
|
let point_y_list: Vec<usize> = (point_start_y..width).step_by(cell_size_y).collect();
|
||||||
|
let point_x_list: Vec<usize> = iter::successors(Some(point_start_x), |i| {
|
||||||
|
let next = i + cell_size_x;
|
||||||
|
(next < (length as f64)).then_some(next)
|
||||||
|
})
|
||||||
|
.map(|i| i as usize)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let points: Vec<Point> = point_x_list
|
||||||
|
.iter()
|
||||||
|
.map(|&x| point_y_list.iter().map(move |&y| (x, y)))
|
||||||
|
.flatten()
|
||||||
|
.collect();
|
||||||
|
points
|
||||||
|
.chunks(single_axis_points * single_axis_points)
|
||||||
|
.into_iter()
|
||||||
|
.map(|points| Vec::from(points))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_colors(&self) -> DisplayColorsOfLedStrips {
|
||||||
|
let bitmap = self.bytes.read().await;
|
||||||
|
|
||||||
|
let top =
|
||||||
|
Self::get_one_edge_colors(&self.sample_points.top, bitmap.as_ref(), self.bytes_per_row)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
let bottom = Self::get_one_edge_colors(
|
||||||
|
&self.sample_points.bottom,
|
||||||
|
bitmap.as_ref(),
|
||||||
|
self.bytes_per_row,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
let left = Self::get_one_edge_colors(
|
||||||
|
&self.sample_points.left,
|
||||||
|
bitmap.as_ref(),
|
||||||
|
self.bytes_per_row,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
let right = Self::get_one_edge_colors(
|
||||||
|
&self.sample_points.right,
|
||||||
|
bitmap.as_ref(),
|
||||||
|
self.bytes_per_row,
|
||||||
|
)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
DisplayColorsOfLedStrips {
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_one_edge_colors(
|
||||||
|
sample_points_of_leds: &Vec<LedSamplePoints>,
|
||||||
|
bitmap: &Vec<u8>,
|
||||||
|
bytes_per_row: usize,
|
||||||
|
) -> Vec<LedColor> {
|
||||||
|
let mut colors = vec![];
|
||||||
|
for led_points in sample_points_of_leds {
|
||||||
|
let mut r = 0.0;
|
||||||
|
let mut g = 0.0;
|
||||||
|
let mut b = 0.0;
|
||||||
|
let len = led_points.len() as f64;
|
||||||
|
for (x, y) in led_points {
|
||||||
|
let position = x * 4 + y * bytes_per_row;
|
||||||
|
b += bitmap[position] as f64;
|
||||||
|
g += bitmap[position + 1] as f64;
|
||||||
|
r += bitmap[position + 2] as f64;
|
||||||
|
}
|
||||||
|
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
||||||
|
// paris::info!("color: {:?}", color.get_rgb());
|
||||||
|
colors.push(color);
|
||||||
|
}
|
||||||
|
colors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type Point = (usize, usize);
|
||||||
|
pub type LedSamplePoints = Vec<Point>;
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ScreenSamplePoints {
|
||||||
|
pub top: Vec<LedSamplePoints>,
|
||||||
|
pub bottom: Vec<LedSamplePoints>,
|
||||||
|
pub left: Vec<LedSamplePoints>,
|
||||||
|
pub right: Vec<LedSamplePoints>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DisplayColorsOfLedStrips {
|
||||||
|
pub top: Vec<u8>,
|
||||||
|
pub bottom: Vec<u8>,
|
||||||
|
pub left: Vec<u8>,
|
||||||
|
pub right: Vec<u8>,
|
||||||
|
}
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct ScreenshotPayload {
|
pub struct ScreenshotPayload {
|
||||||
pub display_id: u32,
|
pub display_id: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
// pub base64_image: String,
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
use base64::Engine;
|
|
||||||
use core_graphics::display::{
|
use core_graphics::display::{
|
||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||||
};
|
};
|
||||||
@ -8,7 +7,7 @@ use paris::{error, info, warn};
|
|||||||
use tauri::{async_runtime::RwLock, Window};
|
use tauri::{async_runtime::RwLock, Window};
|
||||||
use tokio::sync::{watch, OnceCell};
|
use tokio::sync::{watch, OnceCell};
|
||||||
|
|
||||||
use crate::screenshot::{Screenshot, ScreenshotPayload};
|
use crate::screenshot::{Screenshot, ScreenshotPayload, ScreenSamplePoints};
|
||||||
|
|
||||||
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
|
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
|
||||||
log::debug!("take_screenshot");
|
log::debug!("take_screenshot");
|
||||||
@ -40,6 +39,7 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
|||||||
bytes_per_row,
|
bytes_per_row,
|
||||||
bytes,
|
bytes,
|
||||||
scale_factor,
|
scale_factor,
|
||||||
|
ScreenSamplePoints { top: vec![], bottom: vec![], left: vec![], right: vec![] }
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -205,48 +205,4 @@ impl ScreenshotManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn encode_screenshot_to_base64(screenshot: &Screenshot) -> anyhow::Result<String> {
|
|
||||||
let bytes = screenshot.bytes.read().await;
|
|
||||||
|
|
||||||
let scale_factor = screenshot.scale_factor;
|
|
||||||
|
|
||||||
let image_height = (screenshot.height as f32 / scale_factor) as u32;
|
|
||||||
let image_width = (screenshot.width as f32 / scale_factor) as u32;
|
|
||||||
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
|
|
||||||
|
|
||||||
for y in 0..image_height {
|
|
||||||
for x in 0..image_width {
|
|
||||||
let offset = (((y as f32) * screenshot.bytes_per_row as f32 + (x as f32) * 4.0)
|
|
||||||
* scale_factor) as usize;
|
|
||||||
let b = bytes[offset];
|
|
||||||
let g = bytes[offset + 1];
|
|
||||||
let r = bytes[offset + 2];
|
|
||||||
let offset = (y * image_width + x) as usize;
|
|
||||||
image_buffer[offset * 3] = r;
|
|
||||||
image_buffer[offset * 3 + 1] = g;
|
|
||||||
image_buffer[offset * 3 + 2] = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut image_png = Vec::new();
|
|
||||||
let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
|
|
||||||
encoder.set_color(png::ColorType::Rgb);
|
|
||||||
encoder.set_depth(png::BitDepth::Eight);
|
|
||||||
|
|
||||||
let mut writer = encoder
|
|
||||||
.write_header()
|
|
||||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
writer
|
|
||||||
.write_image_data(&image_buffer)
|
|
||||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
writer
|
|
||||||
.finish()
|
|
||||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
|
||||||
|
|
||||||
let mut base64_image = String::new();
|
|
||||||
base64_image.push_str("data:image/webp;base64,");
|
|
||||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_png);
|
|
||||||
base64_image.push_str(encoded.as_str());
|
|
||||||
Ok(base64_image)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
11
src/App.tsx
11
src/App.tsx
@ -4,6 +4,8 @@ import { DisplayView } from './components/display-view';
|
|||||||
import { DisplayListContainer } from './components/display-list-container';
|
import { DisplayListContainer } from './components/display-list-container';
|
||||||
import { displayStore, setDisplayStore } from './stores/display.store';
|
import { displayStore, setDisplayStore } from './stores/display.store';
|
||||||
import { path } from '@tauri-apps/api';
|
import { path } from '@tauri-apps/api';
|
||||||
|
import { LedStripConfig } from './models/led-strip-config';
|
||||||
|
import { setLedStripStore } from './stores/led-strip.store';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@ -12,10 +14,17 @@ function App() {
|
|||||||
displays: JSON.parse(displays),
|
displays: JSON.parse(displays),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
invoke<LedStripConfig[]>('read_led_strip_configs').then((strips) => {
|
||||||
|
console.log(strips);
|
||||||
|
|
||||||
|
setLedStripStore({
|
||||||
|
strips,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<div>
|
||||||
<DisplayListContainer>
|
<DisplayListContainer>
|
||||||
{displayStore.displays.map((display) => {
|
{displayStore.displays.map((display) => {
|
||||||
return <DisplayView display={display} />;
|
return <DisplayView display={display} />;
|
||||||
|
@ -1,15 +1,45 @@
|
|||||||
import { createEffect, createMemo, createSignal, on, ParentComponent } from 'solid-js';
|
import {
|
||||||
|
createEffect,
|
||||||
|
createSignal,
|
||||||
|
onCleanup,
|
||||||
|
onMount,
|
||||||
|
ParentComponent,
|
||||||
|
} from 'solid-js';
|
||||||
import { displayStore, setDisplayStore } from '../stores/display.store';
|
import { displayStore, setDisplayStore } from '../stores/display.store';
|
||||||
|
|
||||||
export const DisplayListContainer: ParentComponent = (props) => {
|
export const DisplayListContainer: ParentComponent = (props) => {
|
||||||
|
let root: HTMLElement;
|
||||||
const [olStyle, setOlStyle] = createSignal({
|
const [olStyle, setOlStyle] = createSignal({
|
||||||
top: '0px',
|
top: '0px',
|
||||||
left: '0px',
|
left: '0px',
|
||||||
});
|
});
|
||||||
const [rootStyle, setRootStyle] = createSignal({
|
const [rootStyle, setRootStyle] = createSignal({
|
||||||
width: '100%',
|
// width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
});
|
});
|
||||||
|
const [bound, setBound] = createSignal({
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
right: 100,
|
||||||
|
bottom: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetSize = () => {
|
||||||
|
const _bound = bound();
|
||||||
|
|
||||||
|
setDisplayStore({
|
||||||
|
viewScale: root.clientWidth / (_bound.right - _bound.left),
|
||||||
|
});
|
||||||
|
|
||||||
|
setOlStyle({
|
||||||
|
top: `${-_bound.top * displayStore.viewScale}px`,
|
||||||
|
left: `${-_bound.left * displayStore.viewScale}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
setRootStyle({
|
||||||
|
height: `${(_bound.bottom - _bound.top) * displayStore.viewScale}px`,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
|
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
|
||||||
@ -23,22 +53,27 @@ export const DisplayListContainer: ParentComponent = (props) => {
|
|||||||
...displayStore.displays.map((display) => display.y + display.height),
|
...displayStore.displays.map((display) => display.y + display.height),
|
||||||
);
|
);
|
||||||
|
|
||||||
setDisplayStore({
|
setBound({
|
||||||
viewScale: 1200 / (boundRight - boundLeft),
|
left: boundLeft,
|
||||||
|
top: boundTop,
|
||||||
|
right: boundRight,
|
||||||
|
bottom: boundBottom,
|
||||||
|
});
|
||||||
|
let observer: ResizeObserver;
|
||||||
|
onMount(() => {
|
||||||
|
observer = new ResizeObserver(resetSize);
|
||||||
|
observer.observe(root);
|
||||||
});
|
});
|
||||||
|
|
||||||
setOlStyle({
|
onCleanup(() => {
|
||||||
top: `${-boundTop * displayStore.viewScale}px`,
|
observer?.unobserve(root);
|
||||||
left: `${-boundLeft * displayStore.viewScale}px`,
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setRootStyle({
|
createEffect(() => {});
|
||||||
width: `${(boundRight - boundLeft) * displayStore.viewScale}px`,
|
|
||||||
height: `${(boundBottom - boundTop) * displayStore.viewScale}px`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<section class="relative bg-gray-400/30" style={rootStyle()}>
|
<section ref={root!} class="relative bg-gray-400/30" style={rootStyle()}>
|
||||||
<ol class="absolute bg-gray-700" style={olStyle()}>
|
<ol class="absolute bg-gray-700" style={olStyle()}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ol>
|
</ol>
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import { Component, createMemo } from 'solid-js';
|
import { Component, createMemo } from 'solid-js';
|
||||||
|
import { LedStripConfigOfBorders } from '../models/display-config';
|
||||||
import { DisplayInfo } from '../models/display-info.model';
|
import { DisplayInfo } from '../models/display-info.model';
|
||||||
import { displayStore } from '../stores/display.store';
|
import { displayStore } from '../stores/display.store';
|
||||||
|
import { ledStripStore } from '../stores/led-strip.store';
|
||||||
import { DisplayInfoPanel } from './display-info-panel';
|
import { DisplayInfoPanel } from './display-info-panel';
|
||||||
|
import { LedStripPart } from './led-strip-part';
|
||||||
import { ScreenView } from './screen-view';
|
import { ScreenView } from './screen-view';
|
||||||
|
|
||||||
type DisplayViewProps = {
|
type DisplayViewProps = {
|
||||||
@ -16,18 +19,47 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
|
|||||||
const style = createMemo(() => ({
|
const style = createMemo(() => ({
|
||||||
top: `${props.display.y * displayStore.viewScale}px`,
|
top: `${props.display.y * displayStore.viewScale}px`,
|
||||||
left: `${props.display.x * displayStore.viewScale}px`,
|
left: `${props.display.x * displayStore.viewScale}px`,
|
||||||
|
height: `${size().height}px`,
|
||||||
|
width: `${size().width}px`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const ledStripConfigs = createMemo(() => {
|
||||||
|
console.log('ledStripConfigs', ledStripStore.strips);
|
||||||
|
return ledStripStore.strips.filter((c) => c.display_id === props.display.id);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section class="absolute bg-gray-300" style={style()}>
|
<section
|
||||||
|
class="absolute bg-gray-300 grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
|
||||||
|
style={style()}
|
||||||
|
>
|
||||||
<ScreenView
|
<ScreenView
|
||||||
|
class="row-start-2 col-start-2"
|
||||||
displayId={props.display.id}
|
displayId={props.display.id}
|
||||||
height={size().height}
|
style={{
|
||||||
width={size().width}
|
'object-fit': 'contain',
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<DisplayInfoPanel
|
<DisplayInfoPanel
|
||||||
display={props.display}
|
display={props.display}
|
||||||
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
|
class="absolute bg-slate-50/10 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black"
|
||||||
/>
|
/>
|
||||||
|
<LedStripPart
|
||||||
|
class="row-start-1 col-start-2 flex-row"
|
||||||
|
config={ledStripConfigs().find((c) => c.border === 'Top')}
|
||||||
|
/>
|
||||||
|
<LedStripPart
|
||||||
|
class="row-start-2 col-start-1 flex-col"
|
||||||
|
config={ledStripConfigs().find((c) => c.border === 'Left')}
|
||||||
|
/>
|
||||||
|
<LedStripPart
|
||||||
|
class="row-start-2 col-start-3 flex-col"
|
||||||
|
config={ledStripConfigs().find((c) => c.border === 'Right')}
|
||||||
|
/>
|
||||||
|
<LedStripPart
|
||||||
|
class="row-start-3 col-start-2 flex-row"
|
||||||
|
config={ledStripConfigs().find((c) => c.border === 'Bottom')}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
130
src/components/led-strip-part.tsx
Normal file
130
src/components/led-strip-part.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
createEffect,
|
||||||
|
createMemo,
|
||||||
|
createSignal,
|
||||||
|
For,
|
||||||
|
JSX,
|
||||||
|
on,
|
||||||
|
onCleanup,
|
||||||
|
splitProps,
|
||||||
|
} from 'solid-js';
|
||||||
|
import { borders } from '../constants/border';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
|
||||||
|
type LedStripPartProps = {
|
||||||
|
config?: LedStripConfig | null;
|
||||||
|
} & JSX.HTMLAttributes<HTMLElement>;
|
||||||
|
|
||||||
|
type PixelProps = {
|
||||||
|
color: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function subscribeScreenshotUpdate(displayId: number) {
|
||||||
|
await invoke('subscribe_encoded_screenshot_updated', {
|
||||||
|
displayId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Pixel: Component<PixelProps> = (props) => {
|
||||||
|
const style = createMemo(() => ({
|
||||||
|
background: props.color,
|
||||||
|
}));
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class="inline-block flex-shrink w-2 h-2 aspect-square rounded-full border border-black"
|
||||||
|
style={style()}
|
||||||
|
title={props.color}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
||||||
|
const [localProps, rootProps] = splitProps(props, ['config']);
|
||||||
|
|
||||||
|
const [ledSamplePoints, setLedSamplePoints] = createSignal();
|
||||||
|
const [colors, setColors] = createSignal<string[]>([]);
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const samplePoints = ledSamplePoints();
|
||||||
|
if (!localProps.config || !samplePoints) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pendingCount = 0;
|
||||||
|
const unlisten = listen<{
|
||||||
|
base64_image: string;
|
||||||
|
display_id: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}>('encoded-screenshot-updated', (event) => {
|
||||||
|
if (event.payload.display_id !== localProps.config!.display_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pendingCount >= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingCount++;
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
samplePoints,
|
||||||
|
displayId: event.payload.display_id,
|
||||||
|
border: localProps.config!.border,
|
||||||
|
});
|
||||||
|
|
||||||
|
invoke<string[]>('get_one_edge_colors', {
|
||||||
|
samplePoints,
|
||||||
|
displayId: event.payload.display_id,
|
||||||
|
})
|
||||||
|
.then((colors) => {
|
||||||
|
setColors(colors);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pendingCount--;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
subscribeScreenshotUpdate(localProps.config.display_id);
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
unlisten.then((unlisten) => unlisten());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (localProps.config) {
|
||||||
|
invoke('get_led_strips_sample_points', {
|
||||||
|
config: localProps.config,
|
||||||
|
}).then((points) => {
|
||||||
|
console.log({ points });
|
||||||
|
setLedSamplePoints(points);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const pixels = createMemo(() => {
|
||||||
|
const _colors = colors();
|
||||||
|
if (_colors) {
|
||||||
|
return <For each={_colors}>{(item) => <Pixel color={item} />}</For>;
|
||||||
|
} else if (localProps.config) {
|
||||||
|
return null;
|
||||||
|
return (
|
||||||
|
<For each={new Array(localProps.config.len).fill(undefined)}>
|
||||||
|
{() => <Pixel color="transparent" />}
|
||||||
|
</For>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
{...rootProps}
|
||||||
|
class={
|
||||||
|
'bg-yellow-50 flex flex-nowrap justify-around items-center overflow-hidden' +
|
||||||
|
rootProps.class
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{pixels()}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
@ -13,9 +13,7 @@ import {
|
|||||||
|
|
||||||
type ScreenViewProps = {
|
type ScreenViewProps = {
|
||||||
displayId: number;
|
displayId: number;
|
||||||
height: number;
|
} & JSX.HTMLAttributes<HTMLDivElement>;
|
||||||
width: number;
|
|
||||||
} & Omit<JSX.HTMLAttributes<HTMLCanvasElement>, 'height' | 'width'>;
|
|
||||||
|
|
||||||
async function subscribeScreenshotUpdate(displayId: number) {
|
async function subscribeScreenshotUpdate(displayId: number) {
|
||||||
await invoke('subscribe_encoded_screenshot_updated', {
|
await invoke('subscribe_encoded_screenshot_updated', {
|
||||||
@ -26,36 +24,99 @@ async function subscribeScreenshotUpdate(displayId: number) {
|
|||||||
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||||
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
|
let root: HTMLDivElement;
|
||||||
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
||||||
|
const [drawInfo, setDrawInfo] = createSignal({
|
||||||
|
drawX: 0,
|
||||||
|
drawY: 0,
|
||||||
|
drawWidth: 0,
|
||||||
|
drawHeight: 0,
|
||||||
|
});
|
||||||
|
const [imageData, setImageData] = createSignal<{
|
||||||
|
buffer: Uint8ClampedArray;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const resetSize = () => {
|
||||||
|
const aspectRatio = canvas.width / canvas.height;
|
||||||
|
|
||||||
|
const drawWidth = Math.round(
|
||||||
|
Math.min(root.clientWidth, root.clientHeight * aspectRatio),
|
||||||
|
);
|
||||||
|
const drawHeight = Math.round(
|
||||||
|
Math.min(root.clientHeight, root.clientWidth / aspectRatio),
|
||||||
|
);
|
||||||
|
|
||||||
|
const drawX = Math.round((root.clientWidth - drawWidth) / 2);
|
||||||
|
const drawY = Math.round((root.clientHeight - drawHeight) / 2);
|
||||||
|
|
||||||
|
setDrawInfo({
|
||||||
|
drawX,
|
||||||
|
drawY,
|
||||||
|
drawWidth,
|
||||||
|
drawHeight,
|
||||||
|
});
|
||||||
|
|
||||||
|
canvas.width = root.clientWidth;
|
||||||
|
canvas.height = root.clientHeight;
|
||||||
|
|
||||||
|
draw(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const draw = (cached: boolean = false) => {
|
||||||
|
const { drawX, drawY } = drawInfo();
|
||||||
|
|
||||||
|
let _ctx = ctx();
|
||||||
|
let raw = imageData();
|
||||||
|
if (_ctx && raw) {
|
||||||
|
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
if (cached) {
|
||||||
|
for (let i = 3; i < raw.buffer.length; i += 8) {
|
||||||
|
raw.buffer[i] = Math.floor(raw.buffer[i] * 0.7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const img = new ImageData(raw.buffer, raw.width, raw.height);
|
||||||
|
_ctx.putImageData(img, drawX, drawY);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
let pendingCount = 0;
|
||||||
const unlisten = listen<{
|
const unlisten = listen<{
|
||||||
base64_image: string;
|
base64_image: string;
|
||||||
display_id: number;
|
display_id: number;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
}>('encoded-screenshot-updated', (event) => {
|
}>('encoded-screenshot-updated', (event) => {
|
||||||
|
const { drawWidth, drawHeight } = drawInfo();
|
||||||
if (event.payload.display_id === localProps.displayId) {
|
if (event.payload.display_id === localProps.displayId) {
|
||||||
const url = convertFileSrc(
|
const url = convertFileSrc(
|
||||||
`displays/${localProps.displayId}?width=${canvas.width}&height=${canvas.height}`,
|
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
|
||||||
'ambient-light',
|
'ambient-light',
|
||||||
);
|
);
|
||||||
|
if (pendingCount >= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
pendingCount++;
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
mode: 'cors',
|
mode: 'cors',
|
||||||
})
|
})
|
||||||
.then((res) => res.body?.getReader().read())
|
.then((res) => res.body?.getReader().read())
|
||||||
.then((buffer) => {
|
.then((buffer) => {
|
||||||
console.log(buffer?.value?.length);
|
if (buffer?.value) {
|
||||||
|
setImageData({
|
||||||
let _ctx = ctx();
|
buffer: new Uint8ClampedArray(buffer?.value),
|
||||||
if (_ctx && buffer?.value) {
|
width: drawWidth,
|
||||||
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
height: drawHeight,
|
||||||
const img = new ImageData(
|
});
|
||||||
new Uint8ClampedArray(buffer.value),
|
} else {
|
||||||
canvas.width,
|
setImageData(null);
|
||||||
canvas.height,
|
|
||||||
);
|
|
||||||
_ctx.putImageData(img, 0, 0);
|
|
||||||
}
|
}
|
||||||
|
draw();
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
pendingCount--;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,10 +124,6 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
|||||||
});
|
});
|
||||||
subscribeScreenshotUpdate(localProps.displayId);
|
subscribeScreenshotUpdate(localProps.displayId);
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
setCtx(canvas.getContext('2d'));
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
unlisten.then((unlisten) => {
|
unlisten.then((unlisten) => {
|
||||||
unlisten();
|
unlisten();
|
||||||
@ -74,5 +131,28 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return <canvas ref={canvas!} class="object-contain" {...rootProps} />;
|
createEffect(() => {
|
||||||
|
let resizeObserver: ResizeObserver;
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
setCtx(canvas.getContext('2d'));
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
resetSize();
|
||||||
|
}).observe(root);
|
||||||
|
});
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
resizeObserver?.unobserve(root);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={root!}
|
||||||
|
{...rootProps}
|
||||||
|
class={'overflow-hidden h-full w-full ' + rootProps.class}
|
||||||
|
>
|
||||||
|
<canvas ref={canvas!} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
2
src/constants/border.ts
Normal file
2
src/constants/border.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const borders = ['Top', 'Right', 'Bottom', 'Left'] as const;
|
||||||
|
export type Borders = typeof borders[number];
|
16
src/models/display-config.ts
Normal file
16
src/models/display-config.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Borders } from '../constants/border';
|
||||||
|
import { LedStripConfig } from './led-strip-config';
|
||||||
|
|
||||||
|
export class LedStripConfigOfBorders implements Record<Borders, LedStripConfig | null> {
|
||||||
|
constructor(
|
||||||
|
public top: LedStripConfig | null = null,
|
||||||
|
public bottom: LedStripConfig | null = null,
|
||||||
|
public left: LedStripConfig | null = null,
|
||||||
|
public right: LedStripConfig | null = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
export class DisplayConfig {
|
||||||
|
led_strip_of_borders = new LedStripConfigOfBorders();
|
||||||
|
|
||||||
|
constructor(public id: number) {}
|
||||||
|
}
|
10
src/models/led-strip-config.ts
Normal file
10
src/models/led-strip-config.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Borders } from '../constants/border';
|
||||||
|
|
||||||
|
export class LedStripConfig {
|
||||||
|
constructor(
|
||||||
|
public readonly display_id: number,
|
||||||
|
public readonly border: Borders,
|
||||||
|
public start_pos: number,
|
||||||
|
public len: number,
|
||||||
|
) {}
|
||||||
|
}
|
8
src/models/picker-configuration.ts
Normal file
8
src/models/picker-configuration.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { DisplayConfig } from './display-config';
|
||||||
|
|
||||||
|
export class PickerConfiguration {
|
||||||
|
constructor(
|
||||||
|
public display_configs: DisplayConfig[] = [],
|
||||||
|
public config_version: number = 1,
|
||||||
|
) {}
|
||||||
|
}
|
1
src/models/pixel-rgb.ts
Normal file
1
src/models/pixel-rgb.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type PixelRgb = [number, number, number];
|
12
src/models/screenshot.dto.ts
Normal file
12
src/models/screenshot.dto.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { DisplayConfig } from './display-config';
|
||||||
|
|
||||||
|
export class ScreenshotDto {
|
||||||
|
encode_image!: string;
|
||||||
|
config!: DisplayConfig;
|
||||||
|
colors!: {
|
||||||
|
top: Uint8Array;
|
||||||
|
bottom: Uint8Array;
|
||||||
|
left: Uint8Array;
|
||||||
|
right: Uint8Array;
|
||||||
|
};
|
||||||
|
}
|
8
src/stores/led-strip.store.tsx
Normal file
8
src/stores/led-strip.store.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { createStore } from 'solid-js/store';
|
||||||
|
import { DisplayConfig } from '../models/display-config';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
|
||||||
|
export const [ledStripStore, setLedStripStore] = createStore({
|
||||||
|
displays: new Array<DisplayConfig>(),
|
||||||
|
strips: new Array<LedStripConfig>(),
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user