Compare commits
No commits in common. "1a3102257e4d267101ce07b2dba16893286b48f3" and "9ed2fa8b5354245e8faccd8bd441d2712a72e826" have entirely different histories.
1a3102257e
...
9ed2fa8b53
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"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -189,6 +189,9 @@ name = "cc"
|
||||
version = "1.0.79"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
@ -267,12 +270,6 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "color_space"
|
||||
version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3776b2bcc4e914db501bb9be9572dd706e344b9eb8f882894f3daa651d281381"
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.6"
|
||||
@ -1049,12 +1046,6 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.25.2"
|
||||
@ -1254,6 +1245,15 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "json-patch"
|
||||
version = "0.2.7"
|
||||
@ -1289,6 +1289,15 @@ version = "0.2.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "line-wrap"
|
||||
version = "0.1.1"
|
||||
@ -2146,15 +2155,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with"
|
||||
version = "1.14.0"
|
||||
@ -2364,7 +2364,7 @@ dependencies = [
|
||||
"cfg-expr 0.9.1",
|
||||
"heck 0.3.3",
|
||||
"pkg-config",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"version-compare 0.0.11",
|
||||
]
|
||||
|
||||
@ -2377,7 +2377,7 @@ dependencies = [
|
||||
"cfg-expr 0.11.0",
|
||||
"heck 0.4.1",
|
||||
"pkg-config",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"version-compare 0.1.1",
|
||||
]
|
||||
|
||||
@ -2647,21 +2647,21 @@ name = "test-demo"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"color_space",
|
||||
"base64 0.21.0",
|
||||
"core-graphics",
|
||||
"display-info",
|
||||
"env_logger",
|
||||
"hex",
|
||||
"log",
|
||||
"paris",
|
||||
"percent-encoding",
|
||||
"png",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tokio",
|
||||
"toml 0.7.3",
|
||||
"url-build-parse",
|
||||
"webp",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2782,26 +2782,11 @@ dependencies = [
|
||||
"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]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
@ -2810,8 +2795,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
@ -3061,6 +3044,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "webview2-com"
|
||||
version = "0.19.1"
|
||||
@ -3317,7 +3310,7 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
|
||||
dependencies = [
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -16,8 +16,11 @@ tauri-build = { version = "1.2", features = [] }
|
||||
tauri = { version = "1.2", features = ["shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
webp = "0.2.2"
|
||||
base64 = "0.21.0"
|
||||
core-graphics = "0.22.3"
|
||||
display-info = "0.4.1"
|
||||
png = "0.17.7"
|
||||
anyhow = "1.0.69"
|
||||
tokio = {version = "1.26.0", features = ["full"] }
|
||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||
@ -25,9 +28,6 @@ log = "0.4.17"
|
||||
env_logger = "0.10.0"
|
||||
percent-encoding = "2.2.0"
|
||||
url-build-parse = "9.0.0"
|
||||
color_space = "0.5.3"
|
||||
hex = "0.4.3"
|
||||
toml = "0.7.3"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
|
@ -1,139 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
mod config;
|
||||
|
||||
pub use config::*;
|
@ -1,13 +0,0 @@
|
||||
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,
|
||||
}
|
@ -1,36 +0,0 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
@ -1,186 +0,0 @@
|
||||
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(())
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// mod brightness;
|
||||
// mod manager;
|
||||
mod display_config;
|
||||
|
||||
pub use display_config::*;
|
||||
|
||||
// pub use brightness::*;
|
||||
// pub use manager::*;
|
||||
|
||||
|
||||
|
@ -1,54 +0,0 @@
|
||||
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,19 +1,15 @@
|
||||
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod ambient_light;
|
||||
mod display;
|
||||
mod led_color;
|
||||
pub mod screenshot;
|
||||
mod screenshot_manager;
|
||||
|
||||
use ambient_light::LedStripConfig;
|
||||
use base64::Engine;
|
||||
use core_graphics::display::{
|
||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||
};
|
||||
use display_info::DisplayInfo;
|
||||
use paris::{error, info, warn};
|
||||
use screenshot::Screenshot;
|
||||
use paris::{error, info};
|
||||
use screenshot_manager::ScreenshotManager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string;
|
||||
@ -56,6 +52,92 @@ fn list_display_info() -> Result<String, String> {
|
||||
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]
|
||||
async fn subscribe_encoded_screenshot_updated(
|
||||
window: tauri::Window,
|
||||
@ -71,72 +153,6 @@ 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]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
@ -146,12 +162,9 @@ async fn main() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
greet,
|
||||
take_screenshot,
|
||||
list_display_info,
|
||||
subscribe_encoded_screenshot_updated,
|
||||
read_led_strip_configs,
|
||||
write_led_strip_configs,
|
||||
get_led_strips_sample_points,
|
||||
get_one_edge_colors
|
||||
subscribe_encoded_screenshot_updated
|
||||
])
|
||||
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
|
||||
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
||||
@ -199,33 +212,20 @@ async fn main() {
|
||||
let screenshot = rx.borrow().clone();
|
||||
let bytes = screenshot.bytes.read().await;
|
||||
|
||||
let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some()
|
||||
let (scale_factor, width, height) = if url.query.is_some()
|
||||
&& url.query.as_ref().unwrap().contains_key("height")
|
||||
&& url.query.as_ref().unwrap().contains_key("width")
|
||||
{
|
||||
let width = url.query.as_ref().unwrap()["width"]
|
||||
.parse::<u32>()
|
||||
.map_err(|err| {
|
||||
warn!("width parse error: {}", err);
|
||||
err
|
||||
})?;
|
||||
let width =
|
||||
url.query.as_ref().unwrap()["width"].parse::<u32>().unwrap();
|
||||
let height = url.query.as_ref().unwrap()["height"]
|
||||
.parse::<u32>()
|
||||
.map_err(|err| {
|
||||
warn!("height parse error: {}", err);
|
||||
err
|
||||
})?;
|
||||
(
|
||||
screenshot.width as f32 / width as f32,
|
||||
screenshot.height as f32 / height as f32,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
.unwrap();
|
||||
(screenshot.width as f32 / width as f32, width, height)
|
||||
} else {
|
||||
log::debug!("scale by scale_factor");
|
||||
let scale_factor = screenshot.scale_factor;
|
||||
(
|
||||
scale_factor,
|
||||
scale_factor,
|
||||
(screenshot.width as f32 / scale_factor) as u32,
|
||||
(screenshot.height as f32 / scale_factor) as u32,
|
||||
@ -245,9 +245,8 @@ async fn main() {
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let offset = ((y as f32) * scale_factor_y).floor() as usize
|
||||
* bytes_per_row as usize
|
||||
+ ((x as f32) * scale_factor_x).floor() as usize * 4;
|
||||
let offset = ((y as f32) * scale_factor) as usize * bytes_per_row as usize
|
||||
+ ((x as f32) * scale_factor) as usize * 4;
|
||||
let b = bytes[offset];
|
||||
let g = bytes[offset + 1];
|
||||
let r = bytes[offset + 2];
|
||||
|
@ -1,14 +1,8 @@
|
||||
use std::iter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use tauri::async_runtime::RwLock;
|
||||
|
||||
use crate::{
|
||||
ambient_light::{DisplayConfig, LedStripConfig},
|
||||
led_color::LedColor,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Screenshot {
|
||||
pub display_id: u32,
|
||||
@ -17,7 +11,6 @@ pub struct Screenshot {
|
||||
pub bytes_per_row: usize,
|
||||
pub bytes: Arc<RwLock<Vec<u8>>>,
|
||||
pub scale_factor: f32,
|
||||
pub sample_points: ScreenSamplePoints,
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
@ -28,7 +21,6 @@ impl Screenshot {
|
||||
bytes_per_row: usize,
|
||||
bytes: Vec<u8>,
|
||||
scale_factor: f32,
|
||||
sample_points: ScreenSamplePoints,
|
||||
) -> Self {
|
||||
Self {
|
||||
display_id,
|
||||
@ -37,251 +29,14 @@ impl Screenshot {
|
||||
bytes_per_row,
|
||||
bytes: Arc::new(RwLock::new(bytes)),
|
||||
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)]
|
||||
pub struct ScreenshotPayload {
|
||||
pub display_id: u32,
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
// pub base64_image: String,
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use base64::Engine;
|
||||
use core_graphics::display::{
|
||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||
};
|
||||
@ -7,7 +8,7 @@ use paris::{error, info, warn};
|
||||
use tauri::{async_runtime::RwLock, Window};
|
||||
use tokio::sync::{watch, OnceCell};
|
||||
|
||||
use crate::screenshot::{Screenshot, ScreenshotPayload, ScreenSamplePoints};
|
||||
use crate::screenshot::{Screenshot, ScreenshotPayload};
|
||||
|
||||
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
|
||||
log::debug!("take_screenshot");
|
||||
@ -39,7 +40,6 @@ pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Scr
|
||||
bytes_per_row,
|
||||
bytes,
|
||||
scale_factor,
|
||||
ScreenSamplePoints { top: vec![], bottom: vec![], left: vec![], right: vec![] }
|
||||
))
|
||||
}
|
||||
|
||||
@ -205,4 +205,48 @@ 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,8 +4,6 @@ import { DisplayView } from './components/display-view';
|
||||
import { DisplayListContainer } from './components/display-list-container';
|
||||
import { displayStore, setDisplayStore } from './stores/display.store';
|
||||
import { path } from '@tauri-apps/api';
|
||||
import { LedStripConfig } from './models/led-strip-config';
|
||||
import { setLedStripStore } from './stores/led-strip.store';
|
||||
|
||||
function App() {
|
||||
createEffect(() => {
|
||||
@ -14,17 +12,10 @@ function App() {
|
||||
displays: JSON.parse(displays),
|
||||
});
|
||||
});
|
||||
invoke<LedStripConfig[]>('read_led_strip_configs').then((strips) => {
|
||||
console.log(strips);
|
||||
|
||||
setLedStripStore({
|
||||
strips,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="container">
|
||||
<DisplayListContainer>
|
||||
{displayStore.displays.map((display) => {
|
||||
return <DisplayView display={display} />;
|
||||
|
@ -1,45 +1,15 @@
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
onCleanup,
|
||||
onMount,
|
||||
ParentComponent,
|
||||
} from 'solid-js';
|
||||
import { createEffect, createMemo, createSignal, on, ParentComponent } from 'solid-js';
|
||||
import { displayStore, setDisplayStore } from '../stores/display.store';
|
||||
|
||||
export const DisplayListContainer: ParentComponent = (props) => {
|
||||
let root: HTMLElement;
|
||||
const [olStyle, setOlStyle] = createSignal({
|
||||
top: '0px',
|
||||
left: '0px',
|
||||
});
|
||||
const [rootStyle, setRootStyle] = createSignal({
|
||||
// width: '100%',
|
||||
width: '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(() => {
|
||||
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
|
||||
@ -53,27 +23,22 @@ export const DisplayListContainer: ParentComponent = (props) => {
|
||||
...displayStore.displays.map((display) => display.y + display.height),
|
||||
);
|
||||
|
||||
setBound({
|
||||
left: boundLeft,
|
||||
top: boundTop,
|
||||
right: boundRight,
|
||||
bottom: boundBottom,
|
||||
});
|
||||
let observer: ResizeObserver;
|
||||
onMount(() => {
|
||||
observer = new ResizeObserver(resetSize);
|
||||
observer.observe(root);
|
||||
setDisplayStore({
|
||||
viewScale: 1200 / (boundRight - boundLeft),
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
observer?.unobserve(root);
|
||||
});
|
||||
setOlStyle({
|
||||
top: `${-boundTop * displayStore.viewScale}px`,
|
||||
left: `${-boundLeft * displayStore.viewScale}px`,
|
||||
});
|
||||
|
||||
createEffect(() => {});
|
||||
|
||||
setRootStyle({
|
||||
width: `${(boundRight - boundLeft) * displayStore.viewScale}px`,
|
||||
height: `${(boundBottom - boundTop) * displayStore.viewScale}px`,
|
||||
});
|
||||
});
|
||||
return (
|
||||
<section ref={root!} class="relative bg-gray-400/30" style={rootStyle()}>
|
||||
<section class="relative bg-gray-400/30" style={rootStyle()}>
|
||||
<ol class="absolute bg-gray-700" style={olStyle()}>
|
||||
{props.children}
|
||||
</ol>
|
||||
|
@ -1,10 +1,7 @@
|
||||
import { Component, createMemo } from 'solid-js';
|
||||
import { LedStripConfigOfBorders } from '../models/display-config';
|
||||
import { DisplayInfo } from '../models/display-info.model';
|
||||
import { displayStore } from '../stores/display.store';
|
||||
import { ledStripStore } from '../stores/led-strip.store';
|
||||
import { DisplayInfoPanel } from './display-info-panel';
|
||||
import { LedStripPart } from './led-strip-part';
|
||||
import { ScreenView } from './screen-view';
|
||||
|
||||
type DisplayViewProps = {
|
||||
@ -19,47 +16,18 @@ export const DisplayView: Component<DisplayViewProps> = (props) => {
|
||||
const style = createMemo(() => ({
|
||||
top: `${props.display.y * 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 (
|
||||
<section
|
||||
class="absolute bg-gray-300 grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
|
||||
style={style()}
|
||||
>
|
||||
<section class="absolute bg-gray-300" style={style()}>
|
||||
<ScreenView
|
||||
class="row-start-2 col-start-2"
|
||||
displayId={props.display.id}
|
||||
style={{
|
||||
'object-fit': 'contain',
|
||||
}}
|
||||
height={size().height}
|
||||
width={size().width}
|
||||
/>
|
||||
<DisplayInfoPanel
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
@ -1,130 +0,0 @@
|
||||
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,7 +13,9 @@ import {
|
||||
|
||||
type ScreenViewProps = {
|
||||
displayId: number;
|
||||
} & JSX.HTMLAttributes<HTMLDivElement>;
|
||||
height: number;
|
||||
width: number;
|
||||
} & Omit<JSX.HTMLAttributes<HTMLCanvasElement>, 'height' | 'width'>;
|
||||
|
||||
async function subscribeScreenshotUpdate(displayId: number) {
|
||||
await invoke('subscribe_encoded_screenshot_updated', {
|
||||
@ -24,99 +26,36 @@ async function subscribeScreenshotUpdate(displayId: number) {
|
||||
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
||||
let canvas: HTMLCanvasElement;
|
||||
let root: HTMLDivElement;
|
||||
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(() => {
|
||||
let pendingCount = 0;
|
||||
const unlisten = listen<{
|
||||
base64_image: string;
|
||||
display_id: number;
|
||||
height: number;
|
||||
width: number;
|
||||
}>('encoded-screenshot-updated', (event) => {
|
||||
const { drawWidth, drawHeight } = drawInfo();
|
||||
if (event.payload.display_id === localProps.displayId) {
|
||||
const url = convertFileSrc(
|
||||
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
|
||||
`displays/${localProps.displayId}?width=${canvas.width}&height=${canvas.height}`,
|
||||
'ambient-light',
|
||||
);
|
||||
if (pendingCount >= 1) {
|
||||
return;
|
||||
}
|
||||
pendingCount++;
|
||||
fetch(url, {
|
||||
mode: 'cors',
|
||||
})
|
||||
.then((res) => res.body?.getReader().read())
|
||||
.then((buffer) => {
|
||||
if (buffer?.value) {
|
||||
setImageData({
|
||||
buffer: new Uint8ClampedArray(buffer?.value),
|
||||
width: drawWidth,
|
||||
height: drawHeight,
|
||||
});
|
||||
} else {
|
||||
setImageData(null);
|
||||
console.log(buffer?.value?.length);
|
||||
|
||||
let _ctx = ctx();
|
||||
if (_ctx && buffer?.value) {
|
||||
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
const img = new ImageData(
|
||||
new Uint8ClampedArray(buffer.value),
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
);
|
||||
_ctx.putImageData(img, 0, 0);
|
||||
}
|
||||
draw();
|
||||
})
|
||||
.finally(() => {
|
||||
pendingCount--;
|
||||
});
|
||||
}
|
||||
|
||||
@ -124,6 +63,10 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
});
|
||||
subscribeScreenshotUpdate(localProps.displayId);
|
||||
|
||||
onMount(() => {
|
||||
setCtx(canvas.getContext('2d'));
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
unlisten.then((unlisten) => {
|
||||
unlisten();
|
||||
@ -131,28 +74,5 @@ export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
});
|
||||
});
|
||||
|
||||
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>
|
||||
);
|
||||
return <canvas ref={canvas!} class="object-contain" {...rootProps} />;
|
||||
};
|
||||
|
@ -1,2 +0,0 @@
|
||||
export const borders = ['Top', 'Right', 'Bottom', 'Left'] as const;
|
||||
export type Borders = typeof borders[number];
|
@ -1,16 +0,0 @@
|
||||
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) {}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
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,
|
||||
) {}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { DisplayConfig } from './display-config';
|
||||
|
||||
export class PickerConfiguration {
|
||||
constructor(
|
||||
public display_configs: DisplayConfig[] = [],
|
||||
public config_version: number = 1,
|
||||
) {}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export type PixelRgb = [number, number, number];
|
@ -1,12 +0,0 @@
|
||||
import { DisplayConfig } from './display-config';
|
||||
|
||||
export class ScreenshotDto {
|
||||
encode_image!: string;
|
||||
config!: DisplayConfig;
|
||||
colors!: {
|
||||
top: Uint8Array;
|
||||
bottom: Uint8Array;
|
||||
left: Uint8Array;
|
||||
right: Uint8Array;
|
||||
};
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
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