Compare commits

..

No commits in common. "1a3102257e4d267101ce07b2dba16893286b48f3" and "9ed2fa8b5354245e8faccd8bd441d2712a72e826" have entirely different histories.

24 changed files with 223 additions and 1217 deletions

83
src-tauri/Cargo.lock generated
View File

@ -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]]

View File

@ -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

View File

@ -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,
}
}
}

View File

@ -1,3 +0,0 @@
mod config;
pub use config::*;

View File

@ -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,
}

View File

@ -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,
}
}
}

View File

@ -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(())
}
}

View File

@ -1,11 +0,0 @@
// mod brightness;
// mod manager;
mod display_config;
pub use display_config::*;
// pub use brightness::*;
// pub use manager::*;

View File

@ -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())
}
}

View File

@ -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];

View File

@ -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,
}

View File

@ -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)
}
}

View File

@ -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} />;

View File

@ -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>

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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} />;
};

View File

@ -1,2 +0,0 @@
export const borders = ['Top', 'Right', 'Bottom', 'Left'] as const;
export type Borders = typeof borders[number];

View File

@ -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) {}
}

View File

@ -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,
) {}
}

View File

@ -1,8 +0,0 @@
import { DisplayConfig } from './display-config';
export class PickerConfiguration {
constructor(
public display_configs: DisplayConfig[] = [],
public config_version: number = 1,
) {}
}

View File

@ -1 +0,0 @@
export type PixelRgb = [number, number, number];

View File

@ -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;
};
}

View File

@ -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>(),
});