Compare commits
33 Commits
1c38fd970e
...
v0
Author | SHA1 | Date | |
---|---|---|---|
596da96f8c | |||
653729fcc2 | |||
eeddff1dc1 | |||
e09b93432c | |||
6e65ef1a4d | |||
550328ba1e | |||
070100cdbc | |||
5422cca393 | |||
4e3b765059 | |||
6ec61c1bcc | |||
bd025d6d71 | |||
45f3f79a49 | |||
a7137f0d51 | |||
a33659c8c5 | |||
2058220ead | |||
af671a4e63 | |||
d3dfdb4d82 | |||
a55db2553b | |||
1882b8ccdc | |||
af03b22d05 | |||
458cc85db2 | |||
cb5fb901f9 | |||
6802dbb7c0 | |||
a3e2b5a234 | |||
a12c503e81 | |||
5384a30872 | |||
56f65fed10 | |||
5293ed52ff | |||
9030d48e21 | |||
366b137258 | |||
4ad78ae5cc | |||
5042ff8bfb | |||
082fcaee20 |
5
babel-plugin-macros.config.cjs
Normal file
5
babel-plugin-macros.config.cjs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
twin: {
|
||||||
|
preset: 'emotion',
|
||||||
|
},
|
||||||
|
};
|
48
package.json
48
package.json
@@ -10,27 +10,45 @@
|
|||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^1.1.0",
|
"@emotion/react": "^11.10.6",
|
||||||
|
"@emotion/styled": "^11.10.6",
|
||||||
|
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.3.0",
|
||||||
|
"@fortawesome/free-solid-svg-icons": "^6.3.0",
|
||||||
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
|
"@mui/material": "^5.11.13",
|
||||||
|
"@tauri-apps/api": "^1.2.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"notistack": "^2.0.8",
|
||||||
|
"ramda": "^0.28.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-async-hook": "^4.0.0",
|
||||||
"react-dom": "^18.2.0"
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.1.0",
|
"@babel/plugin-transform-react-jsx": "^7.21.0",
|
||||||
"@types/node": "^18.7.10",
|
"@emotion/babel-plugin-jsx-pragmatic": "^0.2.0",
|
||||||
"@types/react": "^18.0.15",
|
"@emotion/serialize": "^1.1.1",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@tauri-apps/cli": "^1.2.3",
|
||||||
"@vitejs/plugin-react": "^2.0.0",
|
"@types/debug": "^4.1.7",
|
||||||
"autoprefixer": "^10.4.13",
|
"@types/node": "^18.15.3",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"@types/ramda": "^0.28.23",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"@types/react": "^18.0.28",
|
||||||
"eslint-plugin-jsx-a11y": "^6.6.1",
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react": "^2.2.0",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"babel-plugin-macros": "^3.1.0",
|
||||||
|
"eslint-config-prettier": "^8.7.0",
|
||||||
|
"eslint-plugin-import": "^2.27.5",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"eslint-plugin-simple-import-sort": "^8.0.0",
|
"eslint-plugin-simple-import-sort": "^8.0.0",
|
||||||
"postcss": "^8.4.19",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^2.7.1",
|
"prettier": "^2.8.4",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.7",
|
||||||
"typescript": "^4.6.4",
|
"twin.macro": "^3.1.0",
|
||||||
"vite": "^3.0.2"
|
"typescript": "^4.9.5",
|
||||||
|
"vite": "^3.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1607
pnpm-lock.yaml
generated
1607
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
1937
src-tauri/Cargo.lock
generated
1937
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -28,19 +28,26 @@ tokio = { version = "1.22.0", features = ["full"] }
|
|||||||
tracing = "0.1.37"
|
tracing = "0.1.37"
|
||||||
tracing-subscriber = "0.3.16"
|
tracing-subscriber = "0.3.16"
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
rumqttc = "0.17.0"
|
|
||||||
time = { version = "0.3.17", features = ["formatting"] }
|
time = { version = "0.3.17", features = ["formatting"] }
|
||||||
color_space = "0.5.3"
|
color_space = "0.5.3"
|
||||||
futures = "0.3.25"
|
futures = "0.3.25"
|
||||||
either = "1.8.0"
|
either = "1.8.0"
|
||||||
|
image = "0.24.5"
|
||||||
|
mdns = "3.0.0"
|
||||||
|
macos-app-nap = "0.0.1"
|
||||||
|
ddc-hi = "0.4.1"
|
||||||
|
redb = "0.13.0"
|
||||||
|
paho-mqtt = "0.12.0"
|
||||||
|
core-graphics = "0.22.3"
|
||||||
|
display-info = "0.4.1"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||||
default = [ "custom-protocol" ]
|
default = ["custom-protocol"]
|
||||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||||
# DO NOT remove this
|
# DO NOT remove this
|
||||||
custom-protocol = [ "tauri/custom-protocol" ]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
test_dir = "0.2.0"
|
test_dir = "0.2.0"
|
||||||
|
@@ -1,27 +1,28 @@
|
|||||||
use futures::{future::join_all, stream::FuturesUnordered, StreamExt};
|
use futures::future::join_all;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use paris::info;
|
use paris::{error, info, warn};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::value::Index;
|
|
||||||
use std::{collections::HashMap, iter::Map, sync::Arc, thread, time::Duration};
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::Arc,
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use tauri::async_runtime::RwLock;
|
use tauri::async_runtime::RwLock;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
join,
|
|
||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
task,
|
|
||||||
time::{sleep, Instant},
|
time::{sleep, Instant},
|
||||||
};
|
};
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
picker::{
|
picker::{
|
||||||
config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor, manager::Picker,
|
self, config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor,
|
||||||
screenshot::Screenshot,
|
screenshot::Screenshot,
|
||||||
},
|
},
|
||||||
rpc,
|
rpc,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||||
pub enum AmbientLightMode {
|
pub enum AmbientLightMode {
|
||||||
None,
|
None,
|
||||||
Follow,
|
Follow,
|
||||||
@@ -40,25 +41,25 @@ impl CoreManager {
|
|||||||
ambient_light_mode: Arc::new(RwLock::new(AmbientLightMode::None)),
|
ambient_light_mode: Arc::new(RwLock::new(AmbientLightMode::None)),
|
||||||
});
|
});
|
||||||
|
|
||||||
tokio::spawn(async {
|
|
||||||
loop {
|
|
||||||
core.play_flowing_light().await;
|
|
||||||
match core.play_follow().await {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(error) => {
|
|
||||||
warn!("Can not following displays. {}", error);
|
|
||||||
sleep(Duration::from_millis(1000)).await;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sleep(Duration::from_millis(10)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
core
|
core
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_ambient_light(&self, target_mode: AmbientLightMode) {
|
pub async fn set_ambient_light(&self, target_mode: AmbientLightMode) {
|
||||||
let mut mode = self.ambient_light_mode.write().await;
|
let mut mode = self.ambient_light_mode.write().await;
|
||||||
*mode = target_mode;
|
*mode = target_mode;
|
||||||
|
|
||||||
|
drop(mode);
|
||||||
|
|
||||||
|
match target_mode {
|
||||||
|
AmbientLightMode::Flowing => self.play_flowing_light().await,
|
||||||
|
AmbientLightMode::None => {}
|
||||||
|
AmbientLightMode::Follow => match self.play_follow().await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(error) => {
|
||||||
|
warn!("Can not following displays. {}", error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play_flowing_light(&self) {
|
pub async fn play_flowing_light(&self) {
|
||||||
@@ -74,7 +75,8 @@ impl CoreManager {
|
|||||||
colors.push(color);
|
colors.push(color);
|
||||||
}
|
}
|
||||||
hue = (hue + 1.0) % 360.0;
|
hue = (hue + 1.0) % 360.0;
|
||||||
match rpc::manager::Manager::global()
|
match rpc::Manager::global()
|
||||||
|
.await
|
||||||
.publish_led_colors(&colors)
|
.publish_led_colors(&colors)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
@@ -91,74 +93,118 @@ impl CoreManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play_follow(&self) -> anyhow::Result<()> {
|
pub async fn play_follow(&self) -> anyhow::Result<()> {
|
||||||
let lock = self.ambient_light_mode.read().await;
|
macos_app_nap::prevent();
|
||||||
let mut futs = vec![];
|
let mut futs = vec![];
|
||||||
if let AmbientLightMode::Follow = *lock {
|
let configs = picker::config::Manager::global().reload_config().await;
|
||||||
drop(lock);
|
let configs = match configs {
|
||||||
let configs = Picker::global().display_configs.lock().await;
|
Ok(c) => c.display_configs,
|
||||||
|
Err(err) => anyhow::bail!("can not get display configs. {:?}", err),
|
||||||
|
};
|
||||||
|
info!("piker display configs: {:?}", configs);
|
||||||
|
|
||||||
let (tx, mut rx) = mpsc::channel(10);
|
let (tx, mut rx) = mpsc::channel(10);
|
||||||
|
|
||||||
for config in configs.to_owned() {
|
for config in configs.clone() {
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
let fut = tokio::spawn(async move {
|
let fut = tokio::spawn(async move {
|
||||||
match Self::follow_display_by_config(config, tx).await {
|
match Self::follow_display_by_config(config, tx).await {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("following failed. {}", error);
|
warn!("following failed. {}", error);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
futs.push(fut);
|
|
||||||
}
|
|
||||||
|
|
||||||
let configs = configs.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let mut global_colors = HashMap::new();
|
|
||||||
while let Some(screenshot) = rx.recv().await {
|
|
||||||
let start_at = Instant::now();
|
|
||||||
match screenshot.get_top_colors().await {
|
|
||||||
Ok(colors) => {
|
|
||||||
let start = screenshot.get_top_of_led_start_at().min(screenshot.get_top_of_led_end_at());
|
|
||||||
|
|
||||||
let colors_len = colors.len();
|
|
||||||
for (index, color) in colors.into_iter().enumerate() {
|
|
||||||
global_colors.insert(index + start, color);
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("led count: {}, spend: {:?}", global_colors.len(), start_at.elapsed());
|
|
||||||
|
|
||||||
if global_colors.len() == 60 {
|
|
||||||
let mut colors = vec![];
|
|
||||||
for index in 0..global_colors.len() {
|
|
||||||
colors.push(*global_colors.get(&index).unwrap());
|
|
||||||
}
|
|
||||||
global_colors = HashMap::new();
|
|
||||||
match rpc::manager::Manager::global()
|
|
||||||
.publish_led_colors(&colors)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {
|
|
||||||
info!("publish successful",);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
warn!("publish led colors failed. {}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
futs.push(fut);
|
||||||
join_all(futs).await;
|
|
||||||
} else {
|
|
||||||
drop(lock);
|
|
||||||
return Ok(());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let total_colors_count = configs
|
||||||
|
.iter()
|
||||||
|
.flat_map(|c| {
|
||||||
|
vec![
|
||||||
|
c.led_strip_of_borders.top,
|
||||||
|
c.led_strip_of_borders.bottom,
|
||||||
|
c.led_strip_of_borders.left,
|
||||||
|
c.led_strip_of_borders.right,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.flat_map(|l| match l {
|
||||||
|
Some(l) => (l.global_start_position.min(l.global_end_position)
|
||||||
|
..l.global_start_position.max(l.global_end_position))
|
||||||
|
.collect(),
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<HashSet<_>>()
|
||||||
|
.len();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut global_sub_pixels = HashMap::new();
|
||||||
|
while let Some(screenshot) = rx.recv().await {
|
||||||
|
let colors = screenshot.get_colors();
|
||||||
|
let config = screenshot.get_config();
|
||||||
|
for (colors, config) in vec![
|
||||||
|
(colors.top, config.led_strip_of_borders.top),
|
||||||
|
(colors.right, config.led_strip_of_borders.right),
|
||||||
|
(colors.bottom, config.led_strip_of_borders.bottom),
|
||||||
|
(colors.left, config.led_strip_of_borders.left),
|
||||||
|
] {
|
||||||
|
match config {
|
||||||
|
Some(config) => {
|
||||||
|
let (sign, start) =
|
||||||
|
if config.global_start_position <= config.global_end_position {
|
||||||
|
(1, config.global_start_position as isize * 3)
|
||||||
|
} else {
|
||||||
|
(-1, (config.global_start_position as isize + 1) * 3 - 1)
|
||||||
|
};
|
||||||
|
for (index, color) in colors.into_iter().enumerate() {
|
||||||
|
let pixel_index = index / 3;
|
||||||
|
let sub_pixel_index = index % 3;
|
||||||
|
let offset = if sign < 0 {
|
||||||
|
2 - sub_pixel_index
|
||||||
|
} else {
|
||||||
|
sub_pixel_index
|
||||||
|
};
|
||||||
|
let global_sub_pixel_index =
|
||||||
|
(sign * (pixel_index as isize * 3 + offset as isize) + start)
|
||||||
|
as usize;
|
||||||
|
global_sub_pixels.insert(global_sub_pixel_index, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// info!(
|
||||||
|
// "led count: {}, spend: {:?}",
|
||||||
|
// global_sub_pixels.len(),
|
||||||
|
// start_at.elapsed()
|
||||||
|
// );
|
||||||
|
|
||||||
|
if global_sub_pixels.len() >= total_colors_count * 3 {
|
||||||
|
let mut colors = vec![];
|
||||||
|
for index in 0..global_sub_pixels.len() {
|
||||||
|
colors.push(*global_sub_pixels.get(&index).unwrap_or(&100));
|
||||||
|
}
|
||||||
|
// info!("{:?}", colors);
|
||||||
|
global_sub_pixels = HashMap::new();
|
||||||
|
match rpc::Manager::global()
|
||||||
|
.await
|
||||||
|
.publish_led_sub_pixels(colors)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(_) => {
|
||||||
|
// info!("publish successful",);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
warn!("publish led colors failed. {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
join_all(futs).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,18 +222,18 @@ impl CoreManager {
|
|||||||
if let AmbientLightMode::Follow = *lock {
|
if let AmbientLightMode::Follow = *lock {
|
||||||
drop(lock);
|
drop(lock);
|
||||||
let screenshot = picker.take_screenshot()?;
|
let screenshot = picker.take_screenshot()?;
|
||||||
info!("Take Screenshot Spend: {:?}", start.elapsed());
|
// info!("Take Screenshot Spend: {:?}", start.elapsed());
|
||||||
tx.send(screenshot).await;
|
match tx.send(screenshot).await {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
error!("send screenshot to main thread was failed. {:?}", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
tokio::time::sleep_until(next_tick).await;
|
tokio::time::sleep_until(next_tick).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Picker::global().take_screenshots_for_all().await?;
|
|
||||||
// // let colors = Picker::global().get_led_strip_colors().await?;
|
|
||||||
|
|
||||||
// // let colors = colors.into_iter().rev().collect();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
src-tauri/src/db/db.rs
Normal file
25
src-tauri/src/db/db.rs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
use std::env::current_dir;
|
||||||
|
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use redb::Database;
|
||||||
|
use tauri::api::path::config_dir;
|
||||||
|
|
||||||
|
use crate::picker;
|
||||||
|
|
||||||
|
trait GlobalDatabase<T> {
|
||||||
|
fn global() -> &'static T;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlobalDatabase<Database> for Database {
|
||||||
|
fn global() -> &'static Database {
|
||||||
|
static GLOBAL_DATABASE: OnceCell<Database> = OnceCell::new();
|
||||||
|
|
||||||
|
GLOBAL_DATABASE.get_or_init(|| {
|
||||||
|
let path = config_dir()
|
||||||
|
.unwrap_or(current_dir().unwrap())
|
||||||
|
.join("main.redb");
|
||||||
|
let db = Database::create(path).unwrap();
|
||||||
|
return db;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
3
src-tauri/src/db/mod.rs
Normal file
3
src-tauri/src/db/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
mod db;
|
||||||
|
|
||||||
|
pub use db::*;
|
13
src-tauri/src/display/brightness.rs
Normal file
13
src-tauri/src/display/brightness.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum Brightness {
|
||||||
|
Relative(i16),
|
||||||
|
Absolute(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayBrightness {
|
||||||
|
pub brightness: Brightness,
|
||||||
|
pub display_index: usize,
|
||||||
|
}
|
36
src-tauri/src/display/display_config.rs
Normal file
36
src-tauri/src/display/display_config.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayConfig {
|
||||||
|
pub id: usize,
|
||||||
|
pub brightness: u16,
|
||||||
|
pub max_brightness: u16,
|
||||||
|
pub min_brightness: u16,
|
||||||
|
pub contrast: u16,
|
||||||
|
pub max_contrast: u16,
|
||||||
|
pub min_contrast: u16,
|
||||||
|
pub mode: u16,
|
||||||
|
pub max_mode: u16,
|
||||||
|
pub min_mode: u16,
|
||||||
|
pub last_modified_at: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayConfig {
|
||||||
|
pub fn default(index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
id: index,
|
||||||
|
brightness: 30,
|
||||||
|
contrast: 50,
|
||||||
|
mode: 0,
|
||||||
|
last_modified_at: SystemTime::now(),
|
||||||
|
max_brightness: 100,
|
||||||
|
min_brightness: 0,
|
||||||
|
max_contrast: 100,
|
||||||
|
min_contrast: 0,
|
||||||
|
max_mode: 15,
|
||||||
|
min_mode: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
186
src-tauri/src/display/manager.rs
Normal file
186
src-tauri/src/display/manager.rs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
|
collections::HashMap,
|
||||||
|
ops::Sub,
|
||||||
|
sync::Arc,
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
|
use base64::Config;
|
||||||
|
use ddc_hi::Display;
|
||||||
|
use paris::{error, info};
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
9
src-tauri/src/display/mod.rs
Normal file
9
src-tauri/src/display/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
mod brightness;
|
||||||
|
mod manager;
|
||||||
|
mod display_config;
|
||||||
|
|
||||||
|
pub use brightness::*;
|
||||||
|
pub use manager::*;
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -2,31 +2,35 @@
|
|||||||
all(not(debug_assertions), target_os = "windows"),
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
#![feature(bool_to_option)]
|
||||||
|
|
||||||
mod core;
|
mod core;
|
||||||
|
mod db;
|
||||||
|
mod display;
|
||||||
mod picker;
|
mod picker;
|
||||||
mod rpc;
|
mod rpc;
|
||||||
|
mod models;
|
||||||
|
|
||||||
use crate::core::AmbientLightMode;
|
use crate::core::AmbientLightMode;
|
||||||
use crate::core::CoreManager;
|
use crate::core::CoreManager;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
use paris::*;
|
use paris::*;
|
||||||
use picker::led_color::LedColor;
|
use picker::config::DisplayConfig;
|
||||||
use picker::manager::Picker;
|
use picker::manager::Picker;
|
||||||
use std::vec;
|
use picker::screenshot::ScreenshotDto;
|
||||||
|
use tauri::async_runtime::Mutex;
|
||||||
|
|
||||||
|
static GET_SCREENSHOT_LOCK: OnceCell<Mutex<bool>> = OnceCell::new();
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn refresh_displays() {
|
async fn take_snapshot() -> Vec<ScreenshotDto> {
|
||||||
match Picker::global().refresh_displays().await {
|
info!("Hi?");
|
||||||
Ok(_) => {}
|
let _lock = GET_SCREENSHOT_LOCK
|
||||||
Err(error) => {
|
.get_or_init(|| Mutex::new(false))
|
||||||
error!("{}", error)
|
.lock()
|
||||||
}
|
.await;
|
||||||
}
|
info!("Hi!");
|
||||||
}
|
let manager = Picker::global().await;
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn take_snapshot() -> Vec<String> {
|
|
||||||
let manager = Picker::global();
|
|
||||||
|
|
||||||
let start = time::Instant::now();
|
let start = time::Instant::now();
|
||||||
let base64_bitmap_list = match manager.list_displays().await {
|
let base64_bitmap_list = match manager.list_displays().await {
|
||||||
@@ -40,21 +44,45 @@ async fn take_snapshot() -> Vec<String> {
|
|||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
info!("截图花费 {} s", start.elapsed().as_seconds_f32());
|
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
|
||||||
base64_bitmap_list
|
base64_bitmap_list
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_led_strip_colors() -> Result<Vec<LedColor>, String> {
|
async fn get_screenshot_by_config(config: DisplayConfig) -> Result<ScreenshotDto, String> {
|
||||||
let colors = Picker::global().get_led_strip_colors().await;
|
info!("Hi?");
|
||||||
match colors {
|
// let _lock = GET_SCREENSHOT_LOCK.get_or_init(|| Mutex::new(false)).lock().await;
|
||||||
Ok(colors) => {
|
// info!("Hi!");
|
||||||
rpc::manager::Manager::global()
|
let start = time::Instant::now();
|
||||||
.publish_led_colors(&colors.to_vec())
|
let screenshot_dto = Picker::preview_display_by_config(config).await;
|
||||||
.await;
|
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
|
||||||
Ok(colors)
|
match screenshot_dto {
|
||||||
|
Ok(screenshot_dto) => Ok(screenshot_dto),
|
||||||
|
Err(error) => {
|
||||||
|
error!("preview_display_by_config failed. {}", error);
|
||||||
|
Err(format!("preview_display_by_config failed. {}", error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn get_picker_config() -> picker::config::Configuration {
|
||||||
|
let configuration = picker::config::Manager::global().get_config().await;
|
||||||
|
info!("configuration: {:?}", configuration);
|
||||||
|
configuration
|
||||||
|
}
|
||||||
|
#[tauri::command]
|
||||||
|
async fn write_picker_config(config: picker::config::Configuration) -> Result<(), String> {
|
||||||
|
let manager = picker::config::Manager::global();
|
||||||
|
let path = picker::config::Manager::get_config_file_path();
|
||||||
|
info!("log save in {:?}", path.to_str());
|
||||||
|
manager.set_config(&config).await;
|
||||||
|
match picker::config::Manager::write_config_to_disk(path, &config) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
error!("can not write picker config. {:?}", err);
|
||||||
|
Err(format!("can not write picker config. {:?}", err))
|
||||||
}
|
}
|
||||||
Err(error) => Err(format!("{}", error)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,18 +90,23 @@ async fn get_led_strip_colors() -> Result<Vec<LedColor>, String> {
|
|||||||
async fn play_mode(target_mode: AmbientLightMode) {
|
async fn play_mode(target_mode: AmbientLightMode) {
|
||||||
info!("target mode: {:?}", target_mode);
|
info!("target mode: {:?}", target_mode);
|
||||||
|
|
||||||
CoreManager::global().set_ambient_light(target_mode).await;
|
tokio::spawn(async move { CoreManager::global().set_ambient_light(target_mode).await });
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
rpc::manager::Manager::global();
|
let display_manager = display::Manager::global();
|
||||||
|
tokio::spawn(display_manager.subscribe_display_brightness());
|
||||||
|
let rpc_manager = rpc::Manager::global().await;
|
||||||
|
tokio::spawn(rpc_manager.listen());
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
take_snapshot,
|
take_snapshot,
|
||||||
refresh_displays,
|
|
||||||
get_led_strip_colors,
|
|
||||||
play_mode,
|
play_mode,
|
||||||
|
get_picker_config,
|
||||||
|
get_screenshot_by_config,
|
||||||
|
write_picker_config,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
8
src-tauri/src/models/cmd_resp_with_range.rs
Normal file
8
src-tauri/src/models/cmd_resp_with_range.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct CmdRespWithRange<T = u16> {
|
||||||
|
pub value: T,
|
||||||
|
pub max: T,
|
||||||
|
pub min: T,
|
||||||
|
}
|
10
src-tauri/src/models/config_display_cmd.rs
Normal file
10
src-tauri/src/models/config_display_cmd.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use super::control_value::ControlValue;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct ConfigDisplayCmd<T = ControlValue> {
|
||||||
|
pub display_index: usize,
|
||||||
|
pub value: T,
|
||||||
|
}
|
8
src-tauri/src/models/control_value.rs
Normal file
8
src-tauri/src/models/control_value.rs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum ControlValue<AT = u16, RT = i16> {
|
||||||
|
Absolute(AT),
|
||||||
|
Relative(RT),
|
||||||
|
}
|
10
src-tauri/src/models/mod.rs
Normal file
10
src-tauri/src/models/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
mod control_value;
|
||||||
|
mod mq_message;
|
||||||
|
mod config_display_cmd;
|
||||||
|
mod cmd_resp_with_range;
|
||||||
|
|
||||||
|
pub use control_value::*;
|
||||||
|
pub use mq_message::*;
|
||||||
|
pub use config_display_cmd::*;
|
||||||
|
pub use cmd_resp_with_range::*;
|
||||||
|
|
17
src-tauri/src/models/mq_message.rs
Normal file
17
src-tauri/src/models/mq_message.rs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
use super::{ConfigDisplayCmd, CmdRespWithRange};
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum CmdMqMessage {
|
||||||
|
Brightness(ConfigDisplayCmd),
|
||||||
|
Contrast(ConfigDisplayCmd),
|
||||||
|
PresetMode(ConfigDisplayCmd),
|
||||||
|
}
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub enum CmdRespMqMessage {
|
||||||
|
Brightness(ConfigDisplayCmd<CmdRespWithRange>),
|
||||||
|
Contrast(ConfigDisplayCmd<CmdRespWithRange>),
|
||||||
|
PresetMode(ConfigDisplayCmd<CmdRespWithRange>),
|
||||||
|
}
|
@@ -1,49 +1,56 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
#[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 struct LedStripConfig {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub global_start_position: usize,
|
pub global_start_position: usize,
|
||||||
pub global_end_position: usize,
|
pub global_end_position: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
pub struct DisplayConfig {
|
pub struct DisplayConfig {
|
||||||
|
pub id: u32,
|
||||||
pub index_of_display: usize,
|
pub index_of_display: usize,
|
||||||
pub display_width: usize,
|
pub display_width: usize,
|
||||||
pub display_height: usize,
|
pub display_height: usize,
|
||||||
pub top_led_strip: LedStripConfig,
|
pub led_strip_of_borders: LedStripConfigOfBorders,
|
||||||
pub bottom_led_strip: LedStripConfig,
|
pub scale_factor: f32,
|
||||||
pub left_led_strip: LedStripConfig,
|
|
||||||
pub right_led_strip: LedStripConfig,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayConfig {
|
impl LedStripConfigOfBorders {
|
||||||
pub fn default(index_of_display: usize, display_width: usize, display_height: usize) -> Self {
|
pub fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
index_of_display,
|
top: None,
|
||||||
display_width,
|
bottom: None,
|
||||||
display_height,
|
left: None,
|
||||||
top_led_strip: LedStripConfig {
|
right: None,
|
||||||
index: 0,
|
}
|
||||||
global_start_position: 0,
|
}
|
||||||
global_end_position: 0,
|
}
|
||||||
},
|
|
||||||
bottom_led_strip: LedStripConfig {
|
impl DisplayConfig {
|
||||||
index: 0,
|
pub fn default(
|
||||||
global_start_position: 0,
|
id: u32,
|
||||||
global_end_position: 0,
|
index_of_display: usize,
|
||||||
},
|
display_width: usize,
|
||||||
left_led_strip: LedStripConfig {
|
display_height: usize,
|
||||||
index: 0,
|
scale_factor: f32,
|
||||||
global_start_position: 0,
|
) -> Self {
|
||||||
global_end_position: 0,
|
Self {
|
||||||
},
|
id,
|
||||||
right_led_strip: LedStripConfig {
|
index_of_display,
|
||||||
index: 0,
|
display_width,
|
||||||
global_start_position: 0,
|
display_height,
|
||||||
global_end_position: 0,
|
led_strip_of_borders: LedStripConfigOfBorders::default(),
|
||||||
},
|
scale_factor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,18 +3,20 @@ use std::{
|
|||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
io::Read,
|
io::Read,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
use paris::info;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::api::path::config_dir;
|
use tauri::{api::path::config_dir, async_runtime::Mutex};
|
||||||
|
|
||||||
use super::DisplayConfig;
|
use super::DisplayConfig;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct Configuration {
|
pub struct Configuration {
|
||||||
config_version: u8,
|
pub config_version: u8,
|
||||||
display_configs: Vec<DisplayConfig>,
|
pub display_configs: Vec<DisplayConfig>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Configuration {
|
impl Configuration {
|
||||||
@@ -26,9 +28,8 @@ impl Configuration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct Manager {
|
pub struct Manager {
|
||||||
config: Configuration,
|
config: Arc<Mutex<Configuration>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manager {
|
impl Manager {
|
||||||
@@ -43,7 +44,9 @@ impl Manager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(config: Configuration) -> Self {
|
pub fn new(config: Configuration) -> Self {
|
||||||
Self { config }
|
Self {
|
||||||
|
config: Arc::new(Mutex::new(config)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_config_file_path() -> PathBuf {
|
pub fn get_config_file_path() -> PathBuf {
|
||||||
@@ -70,13 +73,32 @@ impl Manager {
|
|||||||
.map_err(|error| anyhow::anyhow!("can not parse config file contents. {}", error))
|
.map_err(|error| anyhow::anyhow!("can not parse config file contents. {}", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_config_to_disk(&self, config_file_path: PathBuf) -> anyhow::Result<()> {
|
pub fn write_config_to_disk(
|
||||||
let contents = serde_json::to_string(&self.config)
|
config_file_path: PathBuf,
|
||||||
|
config: &Configuration,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let contents = serde_json::to_string(config)
|
||||||
.map_err(|error| anyhow::anyhow!("can not serialize config. {}", error))?;
|
.map_err(|error| anyhow::anyhow!("can not serialize config. {}", error))?;
|
||||||
|
info!("contents: {}", contents);
|
||||||
fs::write(config_file_path, contents.as_bytes())
|
fs::write(config_file_path, contents.as_bytes())
|
||||||
.map_err(|error| anyhow::anyhow!("can not write config file. {}", error))?;
|
.map_err(|error| anyhow::anyhow!("can not write config file. {}", error))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_config(&self) -> Configuration {
|
||||||
|
self.config.lock().await.clone()
|
||||||
|
}
|
||||||
|
pub async fn set_config(&self, new_config: &Configuration) {
|
||||||
|
let mut config = self.config.lock().await;
|
||||||
|
*config = new_config.clone();
|
||||||
|
}
|
||||||
|
pub async fn reload_config(&self) -> anyhow::Result<Configuration> {
|
||||||
|
let mut config = self.config.lock().await;
|
||||||
|
let new_config = Self::read_config_from_disk(Self::get_config_file_path())
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not reload config. {:?}", err))?;
|
||||||
|
*config = new_config.clone();
|
||||||
|
return anyhow::Ok(new_config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -89,14 +111,15 @@ mod tests {
|
|||||||
|
|
||||||
use crate::picker::config::Configuration;
|
use crate::picker::config::Configuration;
|
||||||
|
|
||||||
#[test]
|
#[tokio::test]
|
||||||
fn write_config_to_disk_should_be_successful() {
|
async fn write_config_to_disk_should_be_successful() {
|
||||||
let temp = TestDir::temp().create("config_dir", test_dir::FileType::Dir);
|
let temp = TestDir::temp().create("config_dir", test_dir::FileType::Dir);
|
||||||
let config_file_path = temp.path("config_dir").join("picker.config.json");
|
let config_file_path = temp.path("config_dir").join("picker.config.json");
|
||||||
let manager = crate::picker::config::manger::Manager::default();
|
crate::picker::config::manger::Manager::write_config_to_disk(
|
||||||
manager
|
config_file_path.clone(),
|
||||||
.write_config_to_disk(config_file_path.clone())
|
&Configuration::default(),
|
||||||
.unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let contents = fs::read_to_string(config_file_path.clone()).unwrap();
|
let contents = fs::read_to_string(config_file_path.clone()).unwrap();
|
||||||
let _config: Configuration = serde_json::from_str(contents.as_str()).unwrap();
|
let _config: Configuration = serde_json::from_str(contents.as_str()).unwrap();
|
||||||
@@ -114,7 +137,8 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.to_string()
|
.to_string()
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
).unwrap();
|
)
|
||||||
|
.unwrap();
|
||||||
let _manager =
|
let _manager =
|
||||||
crate::picker::config::manger::Manager::read_config_from_disk(config_file_path.clone())
|
crate::picker::config::manger::Manager::read_config_from_disk(config_file_path.clone())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
|
use core_graphics::display::{
|
||||||
|
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||||
|
};
|
||||||
use paris::info;
|
use paris::info;
|
||||||
use scrap::{Capturer, Display};
|
use scrap::{Capturer, Display};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use super::{config::DisplayConfig, screen::Screen, screenshot::Screenshot};
|
use super::{config::DisplayConfig, screen::Screen, screenshot::Screenshot};
|
||||||
|
|
||||||
@@ -9,17 +13,10 @@ pub struct DisplayPicker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayPicker {
|
impl DisplayPicker {
|
||||||
pub fn new(screen: Screen, config: DisplayConfig) -> Self {
|
|
||||||
Self { screen, config }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_config(config: DisplayConfig) -> anyhow::Result<Self> {
|
pub fn from_config(config: DisplayConfig) -> anyhow::Result<Self> {
|
||||||
let displays = Display::all()
|
let displays = Display::all()
|
||||||
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||||
let display = displays
|
let display = displays.into_iter().skip(config.index_of_display).next();
|
||||||
.into_iter()
|
|
||||||
.skip(config.index_of_display)
|
|
||||||
.next();
|
|
||||||
|
|
||||||
match display {
|
match display {
|
||||||
Some(display) => {
|
Some(display) => {
|
||||||
@@ -39,13 +36,67 @@ impl DisplayPicker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn take_screenshot(&mut self) -> anyhow::Result<Screenshot> {
|
pub fn take_screenshot(&mut self) -> anyhow::Result<Screenshot> {
|
||||||
let bitmap = self
|
debug!("take_screenshot");
|
||||||
.screen
|
let start_at = std::time::Instant::now();
|
||||||
.take()
|
|
||||||
.map_err(|error| anyhow::anyhow!("take screenshot for display failed. {}", error))?;
|
let cg_display = CGDisplay::new(self.config.id);
|
||||||
|
let cg_image = CGDisplay::screenshot(
|
||||||
|
cg_display.bounds(),
|
||||||
|
kCGWindowListOptionOnScreenOnly,
|
||||||
|
kCGNullWindowID,
|
||||||
|
kCGWindowImageDefault,
|
||||||
|
)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", self.config.id))?;
|
||||||
|
debug!("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 scale_factor = self.config.scale_factor;
|
||||||
|
let image_height = (height as f32 / scale_factor) as u32;
|
||||||
|
let image_width = (width as f32 / scale_factor) as u32;
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
"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 a = buffer[offset + 3];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
debug!(
|
||||||
|
"convert to image buffer took {}ms",
|
||||||
|
start_at.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
|
||||||
|
// println!("encode to png took {}ms", start_at.elapsed().as_millis());
|
||||||
|
// // // base64 image
|
||||||
|
// let mut image_base64 = String::new();
|
||||||
|
// image_base64.push_str("data:image/png;base64,");
|
||||||
|
// let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(image_png);
|
||||||
|
// image_base64.push_str(encoded.as_str());
|
||||||
|
|
||||||
|
// println!("took {}ms", start_at.elapsed().as_millis());
|
||||||
|
// println!("image_base64: {}", image_base64.len());
|
||||||
|
|
||||||
// info!("bitmap size {}", bitmap.len());
|
// info!("bitmap size {}", bitmap.len());
|
||||||
let screenshot = Screenshot::new(bitmap, self.config);
|
let screenshot = Screenshot::new(image_buffer, self.config);
|
||||||
Ok(screenshot)
|
Ok(screenshot)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use color_space::{Hsv, Rgb};
|
use color_space::{Hsv, Rgb};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct LedColor {
|
pub struct LedColor {
|
||||||
bits: [u8; 3],
|
bits: [u8; 3],
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
|
use core_graphics::display::CGDisplay;
|
||||||
|
use display_info::DisplayInfo;
|
||||||
use futures::{stream::FuturesUnordered, StreamExt};
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use paris::info;
|
use paris::info;
|
||||||
use scrap::Display;
|
use scrap::Display;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::{sync::Mutex, task};
|
use tokio::{
|
||||||
|
sync::{Mutex, OnceCell},
|
||||||
|
task,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::picker::{config::LedStripConfig, screen::Screen};
|
use crate::picker::{config, screen::Screen};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor,
|
config::DisplayConfig,
|
||||||
screenshot::Screenshot,
|
display_picker::DisplayPicker,
|
||||||
|
screenshot::{Screenshot, ScreenshotDto},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Picker {
|
pub struct Picker {
|
||||||
@@ -19,90 +24,43 @@ pub struct Picker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Picker {
|
impl Picker {
|
||||||
pub fn global() -> &'static Picker {
|
pub async fn global() -> &'static Picker {
|
||||||
static SCREEN_COLOR_PICKER: OnceCell<Picker> = OnceCell::new();
|
static SCREEN_COLOR_PICKER: OnceCell<Picker> = OnceCell::const_new();
|
||||||
|
|
||||||
SCREEN_COLOR_PICKER.get_or_init(|| Picker {
|
SCREEN_COLOR_PICKER
|
||||||
screens: Arc::new(Mutex::new(vec![])),
|
.get_or_init(|| async {
|
||||||
screenshots: Arc::new(Mutex::new(vec![])),
|
let configs = config::Manager::global().get_config().await.display_configs;
|
||||||
display_configs: Arc::new(Mutex::new(vec![
|
info!("Global Picker use configs. {:?}", configs);
|
||||||
DisplayConfig {
|
Picker {
|
||||||
index_of_display: 1,
|
screens: Arc::new(Mutex::new(vec![])),
|
||||||
display_width: 1920,
|
screenshots: Arc::new(Mutex::new(vec![])),
|
||||||
display_height: 1200,
|
display_configs: Arc::new(Mutex::new(
|
||||||
top_led_strip: LedStripConfig {
|
configs,
|
||||||
index: 1,
|
)),
|
||||||
global_start_position: 59,
|
}
|
||||||
global_end_position: 32,
|
})
|
||||||
},
|
.await
|
||||||
bottom_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 0,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
left_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 0,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
right_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 0,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DisplayConfig {
|
|
||||||
index_of_display: 0,
|
|
||||||
display_width: 3008,
|
|
||||||
display_height: 1692,
|
|
||||||
top_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 31,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
bottom_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 0,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
left_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 0,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
right_led_strip: LedStripConfig {
|
|
||||||
index: 0,
|
|
||||||
global_start_position: 0,
|
|
||||||
global_end_position: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_displays(&self) -> anyhow::Result<Vec<String>> {
|
pub async fn list_displays(&self) -> anyhow::Result<Vec<ScreenshotDto>> {
|
||||||
let mut configs = self.display_configs.lock().await;
|
let mut configs = vec![];
|
||||||
let screenshots = self.screenshots.lock().await;
|
|
||||||
|
|
||||||
let displays = Display::all()
|
let displays = DisplayInfo::all()
|
||||||
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||||
|
|
||||||
configs.clear();
|
// configs.clear();
|
||||||
let mut futs = FuturesUnordered::new();
|
let mut futs = FuturesUnordered::new();
|
||||||
|
|
||||||
for (index, display) in displays.iter().enumerate() {
|
for (index, display) in displays.iter().enumerate() {
|
||||||
let height = display.height();
|
let height = display.height as usize;
|
||||||
let width = display.width();
|
let width = display.width as usize;
|
||||||
let config = DisplayConfig::default(index, width, height);
|
let config = DisplayConfig::default(display.id, index, width, height, display.scale_factor);
|
||||||
configs.push(config);
|
configs.push(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index, display) in displays.iter().enumerate() {
|
for config in configs.iter() {
|
||||||
let height = display.height();
|
|
||||||
let width = display.width();
|
|
||||||
let config = configs[index];
|
|
||||||
futs.push(async move {
|
futs.push(async move {
|
||||||
let join = task::spawn(Self::preview_display_by_config(config));
|
let join = task::spawn(Self::preview_display_by_config(config.clone()));
|
||||||
join.await?
|
join.await?
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,63 +78,12 @@ impl Picker {
|
|||||||
Ok(bitmap_string_list)
|
Ok(bitmap_string_list)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<String> {
|
pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<ScreenshotDto> {
|
||||||
let start = time::Instant::now();
|
let start = time::Instant::now();
|
||||||
let mut picker = DisplayPicker::from_config(config)?;
|
let mut picker = DisplayPicker::from_config(config)?;
|
||||||
let screenshot = picker.take_screenshot()?;
|
let screenshot = picker.take_screenshot()?;
|
||||||
info!("Take Screenshot Spend: {}", start.elapsed());
|
info!("Take Screenshot Spend: {}", start.elapsed());
|
||||||
|
|
||||||
anyhow::Ok(screenshot.to_webp_base64().await)
|
anyhow::Ok(screenshot.to_dto().await)
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn refresh_displays(&self) -> anyhow::Result<()> {
|
|
||||||
// let displays = Display::all()
|
|
||||||
// .map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
|
||||||
// let mut screens = self.screens.lock().await;
|
|
||||||
// let mut screenshots = self.screenshots.lock().await;
|
|
||||||
// screens.clear();
|
|
||||||
// info!("number of displays: {}", displays.len());
|
|
||||||
// for display in displays {
|
|
||||||
// let height = display.height();
|
|
||||||
// let width = display.width();
|
|
||||||
// match Capturer::new(display) {
|
|
||||||
// Ok(capturer) => screens.push(Screen::new(capturer, width, height)),
|
|
||||||
// Err(error) => screens.push(Screen::new_failed(
|
|
||||||
// anyhow::anyhow!("{}", error),
|
|
||||||
// width,
|
|
||||||
// height,
|
|
||||||
// )),
|
|
||||||
// };
|
|
||||||
// screenshots.push(Screenshot::new(width, height));
|
|
||||||
// }
|
|
||||||
|
|
||||||
// screens.reverse();
|
|
||||||
// screenshots.reverse();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn take_screenshots_for_all(&self) -> anyhow::Result<Vec<Screenshot>> {
|
|
||||||
let mut screens = self.screens.lock().await;
|
|
||||||
let screenshots = self.screenshots.lock().await;
|
|
||||||
for (index, screen) in screens.iter_mut().enumerate() {
|
|
||||||
let bitmap = screen.take().map_err(|error| {
|
|
||||||
anyhow::anyhow!("take screenshot for display failed. {}", error)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Ok(screenshots.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_led_strip_colors(&self) -> anyhow::Result<Vec<LedColor>> {
|
|
||||||
let screenshots = self.screenshots.lock().await;
|
|
||||||
let mut colors = Vec::new();
|
|
||||||
for screenshot in screenshots.iter() {
|
|
||||||
let result = screenshot
|
|
||||||
.get_top_colors()
|
|
||||||
.await
|
|
||||||
.map_err(|error| anyhow::anyhow!("get top colors failed. {}", error))?;
|
|
||||||
colors.extend_from_slice(&result);
|
|
||||||
}
|
|
||||||
Ok(colors)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
83
src-tauri/src/picker/preview_manager.rs
Normal file
83
src-tauri/src/picker/preview_manager.rs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use paris::{info, warn};
|
||||||
|
use scrap::Display;
|
||||||
|
use std::{borrow::Borrow, sync::Arc};
|
||||||
|
use tokio::{sync::Mutex, task};
|
||||||
|
|
||||||
|
use crate::picker::{config, screen::Screen};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::DisplayConfig,
|
||||||
|
display_picker::DisplayPicker,
|
||||||
|
manager::Picker,
|
||||||
|
screenshot::{Screenshot, ScreenshotDto},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PreviewPicker {
|
||||||
|
pub pickers: Arc<Mutex<Vec<Arc<Mutex<DisplayPicker>>>>>,
|
||||||
|
pub screenshots: Arc<Mutex<Vec<Screenshot>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreviewPicker {
|
||||||
|
pub fn global() -> &'static PreviewPicker {
|
||||||
|
static SCREEN_COLOR_PREVIEW_PICKER: OnceCell<PreviewPicker> = OnceCell::new();
|
||||||
|
|
||||||
|
SCREEN_COLOR_PREVIEW_PICKER.get_or_init(|| PreviewPicker {
|
||||||
|
pickers: Arc::new(Mutex::new(vec![])),
|
||||||
|
screenshots: Arc::new(Mutex::new(vec![])),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_displays(&self) {
|
||||||
|
let mut pickers = self.pickers.lock().await;
|
||||||
|
let displays = Display::all()
|
||||||
|
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||||
|
|
||||||
|
let mut configs = vec![];
|
||||||
|
let mut futs = FuturesUnordered::new();
|
||||||
|
|
||||||
|
for (index, display) in displays.iter().enumerate() {
|
||||||
|
let height = display.height();
|
||||||
|
let width = display.width();
|
||||||
|
let config = DisplayConfig::default(index, width, height);
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
for config in configs.iter() {
|
||||||
|
let picker = DisplayPicker::from_config(*config);
|
||||||
|
match picker {
|
||||||
|
Ok(picker) => {
|
||||||
|
pickers.push(Arc::new(Mutex::new(picker)));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!(
|
||||||
|
"can not create DisplayPicker from config. config: {:?}",
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_screenshot_by_config(
|
||||||
|
&self,
|
||||||
|
config: DisplayConfig,
|
||||||
|
) -> anyhow::Result<ScreenshotDto> {
|
||||||
|
let start = time::Instant::now();
|
||||||
|
let mut picker = DisplayPicker::from_config(config)?;
|
||||||
|
let screenshot = picker.take_screenshot()?;
|
||||||
|
info!("Take Screenshot Spend: {}", start.elapsed());
|
||||||
|
|
||||||
|
anyhow::Ok(screenshot.to_dto().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<ScreenshotDto> {
|
||||||
|
let start = time::Instant::now();
|
||||||
|
let mut picker = DisplayPicker::from_config(config)?;
|
||||||
|
let screenshot = picker.take_screenshot()?;
|
||||||
|
info!("Take Screenshot Spend: {}", start.elapsed());
|
||||||
|
|
||||||
|
anyhow::Ok(screenshot.to_dto().await)
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,6 @@ use std::{io::ErrorKind::WouldBlock, time::Duration, thread};
|
|||||||
|
|
||||||
pub struct Screen {
|
pub struct Screen {
|
||||||
capturer: Option<Capturer>,
|
capturer: Option<Capturer>,
|
||||||
init_error: Option<anyhow::Error>,
|
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
pub height: usize,
|
pub height: usize,
|
||||||
}
|
}
|
||||||
@@ -12,16 +11,6 @@ impl Screen {
|
|||||||
pub fn new(capturer: Capturer, width: usize, height: usize) -> Self {
|
pub fn new(capturer: Capturer, width: usize, height: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
capturer: Some(capturer),
|
capturer: Some(capturer),
|
||||||
init_error: None,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_failed(init_error: anyhow::Error, width: usize, height: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
capturer: None,
|
|
||||||
init_error: Some(init_error),
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}
|
}
|
||||||
|
@@ -1,127 +1,286 @@
|
|||||||
use std::ops::Range;
|
use image::ImageBuffer;
|
||||||
|
use image::{ImageOutputFormat, Rgb};
|
||||||
|
use paris::{error, info};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::iter;
|
||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
use color_space::{Hsv, Rgb};
|
use super::{config::DisplayConfig, led_color::LedColor};
|
||||||
use either::Either;
|
|
||||||
|
|
||||||
use super::{
|
type Point = (usize, usize);
|
||||||
config::{DisplayConfig, LedStripConfig},
|
type LedSamplePoints = Vec<Point>;
|
||||||
led_color::LedColor,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
struct ScreenSamplePoints {
|
||||||
|
pub top: Vec<LedSamplePoints>,
|
||||||
|
pub bottom: Vec<LedSamplePoints>,
|
||||||
|
pub left: Vec<LedSamplePoints>,
|
||||||
|
pub right: Vec<LedSamplePoints>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct Screenshot {
|
pub struct Screenshot {
|
||||||
bitmap: Vec<u8>,
|
bitmap: Vec<u8>,
|
||||||
config: DisplayConfig,
|
config: DisplayConfig,
|
||||||
|
sample_points: ScreenSamplePoints,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Screenshot {
|
impl Screenshot {
|
||||||
pub fn new(bitmap: Vec<u8>, config: DisplayConfig) -> Self {
|
pub fn new(bitmap: Vec<u8>, config: DisplayConfig) -> Self {
|
||||||
Self { bitmap, config }
|
Self {
|
||||||
}
|
bitmap,
|
||||||
|
config,
|
||||||
pub async fn get_top_colors(&self) -> anyhow::Result<Vec<LedColor>> {
|
sample_points: Self::get_sample_points(config),
|
||||||
self.get_x_colors(XPosition::Top, self.config.top_led_strip)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
pub async fn get_bottom_colors(&self) -> anyhow::Result<Vec<LedColor>> {
|
|
||||||
self.get_x_colors(XPosition::Bottom, self.config.bottom_led_strip)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_top_of_led_start_at(&self) -> usize {
|
|
||||||
self.config.top_led_strip.global_start_position
|
|
||||||
}
|
|
||||||
pub fn get_top_of_led_end_at(&self) -> usize {
|
|
||||||
self.config.top_led_strip.global_end_position
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_x_colors(
|
|
||||||
&self,
|
|
||||||
position: XPosition,
|
|
||||||
strip_config: LedStripConfig,
|
|
||||||
) -> anyhow::Result<Vec<LedColor>> {
|
|
||||||
let bitmap = &self.bitmap;
|
|
||||||
let number_of_leds = strip_config
|
|
||||||
.global_start_position
|
|
||||||
.abs_diff(strip_config.global_end_position)
|
|
||||||
+ 1;
|
|
||||||
if number_of_leds == 0 {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
}
|
||||||
let cell_size_x = self.config.display_width / number_of_leds;
|
}
|
||||||
let cell_size_y = self.config.display_height / 8;
|
|
||||||
let cell_size = cell_size_x * cell_size_y;
|
fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints {
|
||||||
let y_range = match position {
|
let top = match config.led_strip_of_borders.top {
|
||||||
XPosition::Top => 20..cell_size_y + 20,
|
Some(led_strip_config) => Self::get_one_edge_sample_points(
|
||||||
XPosition::Bottom => {
|
config.display_height / 8,
|
||||||
self.config.display_height - 20 - cell_size_y..self.config.display_height - 20
|
config.display_width,
|
||||||
|
led_strip_config
|
||||||
|
.global_start_position
|
||||||
|
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||||
|
5,
|
||||||
|
),
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.step_by(5);
|
|
||||||
|
|
||||||
let x_range = if strip_config.global_start_position < strip_config.global_end_position {
|
|
||||||
Either::Left(strip_config.global_start_position..=strip_config.global_end_position)
|
|
||||||
} else {
|
|
||||||
Either::Right(
|
|
||||||
(strip_config.global_end_position..=strip_config.global_start_position).rev(),
|
|
||||||
)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut colors = Vec::new();
|
let bottom: Vec<LedSamplePoints> = match config.led_strip_of_borders.bottom {
|
||||||
let stride = bitmap.len() / self.config.display_height;
|
Some(led_strip_config) => {
|
||||||
|
let points = Self::get_one_edge_sample_points(
|
||||||
|
config.display_height / 9,
|
||||||
|
config.display_width,
|
||||||
|
led_strip_config
|
||||||
|
.global_start_position
|
||||||
|
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||||
|
5,
|
||||||
|
);
|
||||||
|
points
|
||||||
|
.into_iter()
|
||||||
|
.map(|groups| -> Vec<Point> {
|
||||||
|
groups
|
||||||
|
.into_iter()
|
||||||
|
.map(|(x, y)| (x, config.display_height - y))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
vec![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
for pos in x_range {
|
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
|
||||||
|
.global_start_position
|
||||||
|
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||||
|
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
|
||||||
|
.global_start_position
|
||||||
|
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||||
|
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 fn get_colors(&self) -> DisplayColorsOfLedStrips {
|
||||||
|
let top = self
|
||||||
|
.get_one_edge_colors(&self.sample_points.top)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
let bottom = self
|
||||||
|
.get_one_edge_colors(&self.sample_points.bottom)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
let left = self
|
||||||
|
.get_one_edge_colors(&self.sample_points.left)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
let right = self
|
||||||
|
.get_one_edge_colors(&self.sample_points.right)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|color| color.get_rgb())
|
||||||
|
.collect();
|
||||||
|
DisplayColorsOfLedStrips {
|
||||||
|
top,
|
||||||
|
bottom,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_one_edge_colors(
|
||||||
|
&self,
|
||||||
|
sample_points_of_leds: &Vec<LedSamplePoints>,
|
||||||
|
) -> Vec<LedColor> {
|
||||||
|
let mut colors = vec![];
|
||||||
|
for led_points in sample_points_of_leds {
|
||||||
let mut r = 0.0;
|
let mut r = 0.0;
|
||||||
let mut g = 0.0;
|
let mut g = 0.0;
|
||||||
let mut b = 0.0;
|
let mut b = 0.0;
|
||||||
let mut count = 0;
|
let len = led_points.len() as f64;
|
||||||
for x in (pos * cell_size_x..(pos + 1) * cell_size_x).step_by(5) {
|
for (x, y) in led_points {
|
||||||
for y in y_range.to_owned() {
|
let position = (x + y * self.config.display_width) * 4;
|
||||||
let i = stride * y + 4 * x;
|
r += self.bitmap[position + 2] as f64;
|
||||||
r += bitmap[i + 2] as f64;
|
g += self.bitmap[position + 1] as f64;
|
||||||
g += bitmap[i + 1] as f64;
|
b += self.bitmap[position] as f64;
|
||||||
b += bitmap[i] as f64;
|
|
||||||
count+=1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let rgb = Rgb::new(
|
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
||||||
r / count as f64,
|
|
||||||
g / count as f64,
|
|
||||||
b / count as f64,
|
|
||||||
);
|
|
||||||
let hsv = Hsv::from(rgb);
|
|
||||||
// info!("HSV: {:?}", [hsv.h, hsv.s, hsv.v]);
|
|
||||||
let color = LedColor::from_hsv(hsv.h, hsv.s, hsv.v);
|
|
||||||
// paris::info!("color: {:?}", color.get_rgb());
|
// paris::info!("color: {:?}", color.get_rgb());
|
||||||
colors.push(color);
|
colors.push(color);
|
||||||
}
|
}
|
||||||
return Ok(colors);
|
colors
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn to_webp_base64(&self) -> String {
|
pub async fn to_webp_base64(&self) -> String {
|
||||||
let bitmap = &self.bitmap;
|
let bitmap = &self.bitmap;
|
||||||
let mut bitflipped =
|
|
||||||
Vec::with_capacity(self.config.display_width * self.config.display_height * 3);
|
|
||||||
let stride = bitmap.len() / self.config.display_height;
|
let stride = bitmap.len() / self.config.display_height;
|
||||||
|
|
||||||
for y in 0..self.config.display_height {
|
let mut image_buffer = ImageBuffer::new(
|
||||||
for x in 0..self.config.display_width {
|
self.config.display_width as u32 / 3,
|
||||||
let i = stride * y + 4 * x;
|
self.config.display_height as u32 / 3,
|
||||||
bitflipped.extend_from_slice(&[bitmap[i + 2], bitmap[i + 1], bitmap[i]]);
|
);
|
||||||
|
|
||||||
|
for y in 0..self.config.display_height / 3 {
|
||||||
|
for x in 0..self.config.display_width / 3 {
|
||||||
|
let i = stride * y * 3 + 4 * x * 3;
|
||||||
|
image_buffer.put_pixel(
|
||||||
|
x as u32,
|
||||||
|
y as u32,
|
||||||
|
Rgb::<u8>([bitmap[i + 2], bitmap[i + 1], bitmap[i]]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let webp_memory = webp::Encoder::from_rgb(
|
// let webp_memory =
|
||||||
bitflipped.as_slice(),
|
// webp::Encoder::from_rgb(bitflipped.as_slice(), size_x, size_y).encode(50.0);
|
||||||
self.config.display_width as u32,
|
// return base64::encode(&*webp_memory);
|
||||||
self.config.display_height as u32,
|
|
||||||
)
|
let mut cursor = std::io::Cursor::new(vec![]);
|
||||||
.encode(100.0);
|
match image_buffer.write_to(&mut cursor, ImageOutputFormat::Tiff) {
|
||||||
return base64::encode(&*webp_memory);
|
Ok(_) => {
|
||||||
|
return base64::encode(cursor.into_inner());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("can not encode image. {:?}", err);
|
||||||
|
return String::from("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn to_dto(&self) -> ScreenshotDto {
|
||||||
|
let rk = SystemTime::now();
|
||||||
|
info!("[{:?} {:p}] to_dto", rk.elapsed(), &self);
|
||||||
|
let encode_image = self.to_webp_base64().await;
|
||||||
|
info!("[{:?} {:p}] image", rk.elapsed(), &self);
|
||||||
|
let config = self.config.clone();
|
||||||
|
info!("[{:?} {:p}] cloned", rk.elapsed(), &self);
|
||||||
|
let colors = self.get_colors();
|
||||||
|
info!("[{:?} {:p}] colors", rk.elapsed(), &self);
|
||||||
|
ScreenshotDto {
|
||||||
|
encode_image,
|
||||||
|
config,
|
||||||
|
colors,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn get_config(&self) -> DisplayConfig {
|
||||||
|
self.config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum XPosition {
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
Top,
|
pub struct ScreenshotDto {
|
||||||
Bottom,
|
pub config: DisplayConfig,
|
||||||
|
pub encode_image: String,
|
||||||
|
pub colors: DisplayColorsOfLedStrips,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayColorsOfLedStrips {
|
||||||
|
pub top: Vec<u8>,
|
||||||
|
pub bottom: Vec<u8>,
|
||||||
|
pub left: Vec<u8>,
|
||||||
|
pub right: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +1,42 @@
|
|||||||
use crate::picker::led_color::LedColor;
|
use paris::error;
|
||||||
|
use tokio::sync::{broadcast, OnceCell};
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
use super::mqtt::MqttConnection;
|
use crate::{display, models, picker::led_color::LedColor};
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
|
use super::mqtt::MqttRpc;
|
||||||
|
|
||||||
pub struct Manager {
|
pub struct Manager {
|
||||||
mqtt: MqttConnection,
|
client: MqttRpc,
|
||||||
|
initialized: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Manager {
|
impl Manager {
|
||||||
pub fn global() -> &'static Self {
|
pub async fn global() -> &'static Self {
|
||||||
static RPC_MANAGER: OnceCell<Manager> = OnceCell::new();
|
static RPC_MANAGER: OnceCell<Manager> = OnceCell::const_new();
|
||||||
|
|
||||||
RPC_MANAGER.get_or_init(|| Manager::new())
|
RPC_MANAGER
|
||||||
|
.get_or_init(|| async { Manager::new().await.unwrap() })
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new() -> Self {
|
pub async fn new() -> anyhow::Result<Self> {
|
||||||
let mut mqtt = MqttConnection::new();
|
let mqtt = MqttRpc::new().await?;
|
||||||
mqtt.initialize();
|
let initialized = match mqtt.initialize().await {
|
||||||
Self { mqtt }
|
Ok(_) => true,
|
||||||
|
Err(err) => {
|
||||||
|
error!("initialize for mqtt was failed. {:?}", err);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
client: mqtt,
|
||||||
|
initialized,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn listen(&self) {
|
||||||
|
self.client.listen().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn publish_led_colors(&self, colors: &Vec<LedColor>) -> anyhow::Result<()> {
|
pub async fn publish_led_colors(&self, colors: &Vec<LedColor>) -> anyhow::Result<()> {
|
||||||
@@ -27,15 +46,18 @@ impl Manager {
|
|||||||
.flatten()
|
.flatten()
|
||||||
.collect::<Vec<u8>>();
|
.collect::<Vec<u8>>();
|
||||||
|
|
||||||
self.mqtt
|
self.publish_led_sub_pixels(payload).await
|
||||||
.client
|
}
|
||||||
.publish(
|
|
||||||
"display-ambient-light/desktop/colors",
|
pub async fn publish_led_sub_pixels(&self, payload: Vec<u8>) -> anyhow::Result<()> {
|
||||||
rumqttc::QoS::AtLeastOnce,
|
self.client.publish_led_sub_pixels(payload).await
|
||||||
false,
|
}
|
||||||
payload,
|
pub async fn publish_desktop_cmd(&self, field: &str, payload: Vec<u8>) -> anyhow::Result<()>
|
||||||
)
|
{
|
||||||
.await
|
self.client.publish_desktop_cmd(field, payload).await
|
||||||
.map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error))
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> &MqttRpc {
|
||||||
|
&self.client
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,2 +1,4 @@
|
|||||||
pub mod manager;
|
mod manager;
|
||||||
pub mod mqtt;
|
pub mod mqtt;
|
||||||
|
|
||||||
|
pub use manager::*;
|
@@ -1,38 +1,187 @@
|
|||||||
use rumqttc::{AsyncClient, MqttOptions, QoS};
|
use crate::{display, models};
|
||||||
use std::{time::Duration};
|
use futures::StreamExt;
|
||||||
|
use paho_mqtt as mqtt;
|
||||||
|
use paris::{error, info, warn};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::{borrow::Borrow, fmt::Debug, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
use tauri::async_runtime::{Mutex, TokioJoinHandle};
|
||||||
use time::{format_description, OffsetDateTime};
|
use time::{format_description, OffsetDateTime};
|
||||||
use tokio::task;
|
use tokio::{sync::broadcast, task, time::sleep};
|
||||||
use tracing::warn;
|
|
||||||
|
|
||||||
pub struct MqttConnection {
|
const DISPLAY_TOPIC: &'static str = "display-ambient-light/display";
|
||||||
pub client: AsyncClient,
|
const DESKTOP_TOPIC: &'static str = "display-ambient-light/desktop";
|
||||||
|
const DISPLAY_BRIGHTNESS_TOPIC: &'static str = "display-ambient-light/board/brightness";
|
||||||
|
const BOARD_SEND_CMD: &'static str = "display-ambient-light/board/cmd";
|
||||||
|
|
||||||
|
pub struct MqttRpc {
|
||||||
|
client: mqtt::AsyncClient,
|
||||||
|
change_display_brightness_tx: broadcast::Sender<display::DisplayBrightness>,
|
||||||
|
message_tx: broadcast::Sender<models::CmdMqMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MqttConnection {
|
impl MqttRpc {
|
||||||
pub fn new() -> Self {
|
pub async fn new() -> anyhow::Result<Self> {
|
||||||
let mut options = MqttOptions::new("rumqtt-async", "192.168.31.11", 1883);
|
let client = mqtt::AsyncClient::new("tcp://192.168.31.11:1883")
|
||||||
options.set_keep_alive(Duration::from_secs(5));
|
.map_err(|err| anyhow::anyhow!("can not create MQTT client. {:?}", err))?;
|
||||||
|
|
||||||
let (client, mut eventloop) = AsyncClient::new(options, 10);
|
client.set_connected_callback(|client| {
|
||||||
task::spawn(async move {
|
info!("MQTT server connected.");
|
||||||
while let Ok(notification) = eventloop.poll().await {
|
|
||||||
// println!("Received = {:?}", notification);
|
client.subscribe("display-ambient-light/board/#", mqtt::QOS_1);
|
||||||
}
|
|
||||||
|
client.subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1);
|
||||||
});
|
});
|
||||||
Self { client }
|
client.set_connection_lost_callback(|client| {
|
||||||
|
info!("MQTT server connection lost.");
|
||||||
|
});
|
||||||
|
client.set_disconnected_callback(|_, a1, a2| {
|
||||||
|
info!("MQTT server disconnected. {:?} {:?}", a1, a2);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut last_will_payload = serde_json::Map::new();
|
||||||
|
last_will_payload.insert("message".to_string(), json!("offline"));
|
||||||
|
last_will_payload.insert(
|
||||||
|
"time".to_string(),
|
||||||
|
serde_json::Value::String(
|
||||||
|
OffsetDateTime::now_utc()
|
||||||
|
.format(
|
||||||
|
&format_description::parse("[year]-[month]-[day] [hour]:[minute]:[second]")
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let last_will = mqtt::Message::new(
|
||||||
|
format!("{}/status", DESKTOP_TOPIC),
|
||||||
|
serde_json::to_string(&last_will_payload)
|
||||||
|
.unwrap()
|
||||||
|
.as_bytes(),
|
||||||
|
mqtt::QOS_1,
|
||||||
|
);
|
||||||
|
|
||||||
|
let connect_options = mqtt::ConnectOptionsBuilder::new()
|
||||||
|
.keep_alive_interval(Duration::from_secs(5))
|
||||||
|
.will_message(last_will)
|
||||||
|
.automatic_reconnect(Duration::from_secs(1), Duration::from_secs(5))
|
||||||
|
.finalize();
|
||||||
|
|
||||||
|
let token = client.connect(connect_options);
|
||||||
|
|
||||||
|
token.await.map_err(|err| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"can not connect MQTT server. wait for connect token failed. {:?}",
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let (change_display_brightness_tx, _) =
|
||||||
|
broadcast::channel::<display::DisplayBrightness>(16);
|
||||||
|
let (message_tx, _) = broadcast::channel::<models::CmdMqMessage>(32);
|
||||||
|
Ok(Self {
|
||||||
|
client,
|
||||||
|
change_display_brightness_tx,
|
||||||
|
message_tx,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize(&mut self) {
|
pub async fn listen(&self) {
|
||||||
self.subscribe_board();
|
let change_display_brightness_tx2 = self.change_display_brightness_tx.clone();
|
||||||
|
let message_tx_cloned = self.message_tx.clone();
|
||||||
|
|
||||||
|
let mut stream = self.client.to_owned().get_stream(100);
|
||||||
|
|
||||||
|
while let Some(notification) = stream.next().await {
|
||||||
|
match notification {
|
||||||
|
Some(notification) => match notification.topic() {
|
||||||
|
DISPLAY_BRIGHTNESS_TOPIC => {
|
||||||
|
let payload_text = String::from_utf8(notification.payload().to_vec());
|
||||||
|
match payload_text {
|
||||||
|
Ok(payload_text) => {
|
||||||
|
let display_brightness: Result<display::DisplayBrightness, _> =
|
||||||
|
serde_json::from_str(payload_text.as_str());
|
||||||
|
match display_brightness {
|
||||||
|
Ok(display_brightness) => {
|
||||||
|
match change_display_brightness_tx2.send(display_brightness)
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"can not send display brightness to channel. {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"can not parse display brightness from payload. {:?}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("can not parse display brightness from payload. {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BOARD_SEND_CMD => {
|
||||||
|
let payload_text = String::from_utf8(notification.payload().to_vec());
|
||||||
|
match payload_text {
|
||||||
|
Ok(payload_text) => {
|
||||||
|
let message: Result<models::CmdMqMessage, _> =
|
||||||
|
serde_json::from_str(payload_text.as_str());
|
||||||
|
match message {
|
||||||
|
Ok(message) => match message_tx_cloned.send(message) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("can not send message to channel. {:?}", err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!("can not parse message from payload. {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
warn!("can not parse message from payload. {:?}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
warn!("can not get notification from MQTT server.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn initialize(&self) -> anyhow::Result<()> {
|
||||||
|
// self.subscribe_board()?;
|
||||||
|
// self.subscribe_display()?;
|
||||||
self.broadcast_desktop_online();
|
self.broadcast_desktop_online();
|
||||||
|
anyhow::Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn subscribe_board(&self) {
|
fn subscribe_board(&self) -> anyhow::Result<()> {
|
||||||
self.client
|
self.client
|
||||||
.subscribe("display-ambient-light/board/#", QoS::AtMostOnce).await;
|
.subscribe("display-ambient-light/board/#", mqtt::QOS_1)
|
||||||
|
.wait()
|
||||||
|
.map_err(|err| anyhow::anyhow!("subscribe board failed. {:?}", err))
|
||||||
|
.map(|_| ())
|
||||||
|
}
|
||||||
|
fn subscribe_display(&self) -> anyhow::Result<()> {
|
||||||
|
self.client
|
||||||
|
.subscribe(format!("{}/#", DISPLAY_TOPIC), mqtt::QOS_1)
|
||||||
|
.wait()
|
||||||
|
.map_err(|err| anyhow::anyhow!("subscribe board failed. {:?}", err))
|
||||||
|
.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn broadcast_desktop_online(&mut self) {
|
fn broadcast_desktop_online(&self) {
|
||||||
let client = self.client.to_owned();
|
let client = self.client.to_owned();
|
||||||
task::spawn(async move {
|
task::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
@@ -40,15 +189,12 @@ impl MqttConnection {
|
|||||||
.format(&format_description::well_known::Iso8601::DEFAULT)
|
.format(&format_description::well_known::Iso8601::DEFAULT)
|
||||||
{
|
{
|
||||||
Ok(now_str) => {
|
Ok(now_str) => {
|
||||||
match client
|
let msg = mqtt::Message::new(
|
||||||
.publish(
|
"display-ambient-light/desktop/online",
|
||||||
"display-ambient-light/desktop/online",
|
now_str.as_bytes(),
|
||||||
QoS::AtLeastOnce,
|
mqtt::QOS_0,
|
||||||
false,
|
);
|
||||||
now_str.as_bytes(),
|
match client.publish(msg).await {
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("can not publish last online time. {}", error)
|
warn!("can not publish last online time. {}", error)
|
||||||
@@ -63,4 +209,32 @@ impl MqttConnection {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn publish_led_sub_pixels(&self, payload: Vec<u8>) -> anyhow::Result<()> {
|
||||||
|
self.client
|
||||||
|
.publish(mqtt::Message::new(
|
||||||
|
"display-ambient-light/desktop/colors",
|
||||||
|
payload,
|
||||||
|
mqtt::QOS_1,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subscribe_change_display_brightness_rx(
|
||||||
|
&self,
|
||||||
|
) -> broadcast::Receiver<display::DisplayBrightness> {
|
||||||
|
self.change_display_brightness_tx.subscribe()
|
||||||
|
}
|
||||||
|
pub async fn publish_desktop_cmd(&self, field: &str, payload: Vec<u8>) -> anyhow::Result<()>
|
||||||
|
{
|
||||||
|
self.client
|
||||||
|
.publish(mqtt::Message::new(
|
||||||
|
format!("{}/{}", DESKTOP_TOPIC, field),
|
||||||
|
payload,
|
||||||
|
mqtt::QOS_1,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -28,7 +28,7 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"identifier": "com.tauri.dev",
|
"identifier": "cc.ivanli.ambient",
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"entitlements": null,
|
"entitlements": null,
|
||||||
|
73
src/App.tsx
73
src/App.tsx
@@ -1,11 +1,15 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import reactLogo from './assets/react.svg';
|
import tw from 'twin.macro';
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import clsx from 'clsx';
|
import { Configurator } from './configurator/configurator';
|
||||||
|
import { ButtonSwitch } from './commons/components/button';
|
||||||
|
import { fillParentCss } from './styles/fill-parent';
|
||||||
|
|
||||||
type Mode = 'Flowing' | 'Follow' | null;
|
type Mode = 'Flowing' | 'Follow' | null;
|
||||||
|
|
||||||
|
localStorage.setItem('debug', '*');
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [screenshots, setScreenshots] = useState<string[]>([]);
|
const [screenshots, setScreenshots] = useState<string[]>([]);
|
||||||
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
||||||
@@ -17,10 +21,6 @@ function App() {
|
|||||||
setScreenshots(base64TextList.map((text) => `data:image/webp;base64,${text}`));
|
setScreenshots(base64TextList.map((text) => `data:image/webp;base64,${text}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshDisplays = useCallback(async () => {
|
|
||||||
await invoke('refresh_displays');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getLedStripColors = useCallback(async () => {
|
const getLedStripColors = useCallback(async () => {
|
||||||
setLedStripColors(await invoke('get_led_strip_colors'));
|
setLedStripColors(await invoke('get_led_strip_colors'));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -41,67 +41,32 @@ function App() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div css={[fillParentCss]} tw="box-border flex flex-col">
|
||||||
<div className="flex justify-between">
|
<div tw="flex justify-between">
|
||||||
{ledStripColors.map((it) => (
|
{ledStripColors.map((it) => (
|
||||||
<span className=" h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
<span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-1 justify-center w-screen overflow-hidden">
|
<div tw="flex gap-1 justify-center w-screen overflow-hidden">
|
||||||
{screenshots.map((screenshot) => (
|
{screenshots.map((screenshot) => (
|
||||||
<div className="flex-auto">
|
<div tw="flex-auto">
|
||||||
<img src={screenshot} />
|
<img src={screenshot} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-5 justify-center ">
|
<div tw="flex gap-5 justify-center">
|
||||||
<button
|
<ButtonSwitch onClick={() => takeSnapshot()}>Take Snapshot</ButtonSwitch>
|
||||||
className="bg-black bg-opacity-20"
|
<ButtonSwitch onClick={() => getLedStripColors()}>Get Colors</ButtonSwitch>
|
||||||
type="button"
|
<ButtonSwitch onClick={() => switchCurrentMode('Flowing')}>
|
||||||
onClick={() => refreshDisplays()}
|
|
||||||
>
|
|
||||||
Refresh Displays
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="bg-black bg-opacity-20"
|
|
||||||
type="button"
|
|
||||||
onClick={() => takeSnapshot()}
|
|
||||||
>
|
|
||||||
Take Snapshot
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="bg-black bg-opacity-20"
|
|
||||||
type="button"
|
|
||||||
onClick={() => getLedStripColors()}
|
|
||||||
>
|
|
||||||
Get Colors
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={clsx('bg-black', 'bg-opacity-20', {
|
|
||||||
'bg-gradient-to-r from-purple-500 to-blue-500': currentMode === 'Flowing',
|
|
||||||
})}
|
|
||||||
type="button"
|
|
||||||
onClick={() => switchCurrentMode('Flowing')}
|
|
||||||
>
|
|
||||||
Flowing Light
|
Flowing Light
|
||||||
</button>
|
</ButtonSwitch>
|
||||||
<button
|
<ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch>
|
||||||
className={clsx('bg-black', 'bg-opacity-20', {
|
|
||||||
'bg-gradient-to-r from-purple-500 to-blue-500': currentMode === 'Follow',
|
|
||||||
})}
|
|
||||||
type="button"
|
|
||||||
onClick={() => switchCurrentMode('Follow')}
|
|
||||||
>
|
|
||||||
Follow
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex gap-5 justify-center">
|
<div css={[fillParentCss]}>
|
||||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
<Configurator />
|
||||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
21
src/commons/components/button.tsx
Normal file
21
src/commons/components/button.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import tw, { theme } from 'twin.macro';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
value?: boolean;
|
||||||
|
isSmall?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonSwitch = styled.button(({ value, isSmall }: ButtonProps) => [
|
||||||
|
// The common button styles
|
||||||
|
tw`px-8 py-2 rounded-xl transform duration-75 dark:bg-black m-2 shadow-lg text-opacity-95 dark:shadow-gray-800`,
|
||||||
|
|
||||||
|
tw`hover:(scale-105)`,
|
||||||
|
tw`focus:(scale-100)`,
|
||||||
|
|
||||||
|
value && 'bg-gradient-to-r from-purple-500 to-blue-500',
|
||||||
|
|
||||||
|
isSmall ? tw`text-sm` : tw`text-lg`,
|
||||||
|
]);
|
62
src/configurator/components/completed-container.tsx
Normal file
62
src/configurator/components/completed-container.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { isNil } from 'ramda';
|
||||||
|
import { FC, Fragment, useMemo } from 'react';
|
||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
import { useLedCount } from '../contents/led-count';
|
||||||
|
import { PixelRgb } from '../models/pixel-rgb';
|
||||||
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
import { BorderLedStrip } from './completed-led-strip';
|
||||||
|
|
||||||
|
const StyledCompletedContainerBorder = styled.section(
|
||||||
|
tw`dark:shadow-xl border-gray-600 bg-black border rounded-full flex flex-nowrap justify-around items-center p-px h-3 mb-1`,
|
||||||
|
css`
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1 / span 1;
|
||||||
|
justify-self: stretch;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
interface StyledCompletedContainerProps {
|
||||||
|
borderLedStrips: BorderLedStrip[];
|
||||||
|
overrideBorderLedStrips?: BorderLedStrip[];
|
||||||
|
}
|
||||||
|
export const CompletedContainer: FC<StyledCompletedContainerProps> = ({
|
||||||
|
borderLedStrips,
|
||||||
|
overrideBorderLedStrips,
|
||||||
|
}) => {
|
||||||
|
const { ledCount } = useLedCount();
|
||||||
|
const completedPixels = useMemo(() => {
|
||||||
|
const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]);
|
||||||
|
(overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => {
|
||||||
|
if (isNil(config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.global_start_position <= config.global_end_position) {
|
||||||
|
pixels.forEach((color, i) => {
|
||||||
|
completed[config.global_start_position + i] = color;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pixels.forEach((color, i) => {
|
||||||
|
completed[config.global_start_position - i] = color;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return completed.map((color, i) => (
|
||||||
|
<StyledPixel
|
||||||
|
rgb={color}
|
||||||
|
key={i}
|
||||||
|
css={css`
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-column-start: ${i + 1};
|
||||||
|
align-self: flex-start;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [ledCount, borderLedStrips, overrideBorderLedStrips]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<StyledCompletedContainerBorder />
|
||||||
|
{completedPixels}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
119
src/configurator/components/completed-led-strip.tsx
Normal file
119
src/configurator/components/completed-led-strip.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import debug from 'debug';
|
||||||
|
import { lensPath, set, splitEvery, update } from 'ramda';
|
||||||
|
import { FC, useEffect, useMemo, useState } from 'react';
|
||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
import { Borders, borders } from '../../constants/border';
|
||||||
|
import { useLedCount } from '../contents/led-count';
|
||||||
|
import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import { PixelRgb } from '../models/pixel-rgb';
|
||||||
|
import { ScreenshotDto } from '../models/screenshot.dto';
|
||||||
|
import { CompletedContainer } from './completed-container';
|
||||||
|
import { DraggableStrip } from './draggable-strip';
|
||||||
|
|
||||||
|
export const logger = debug('app:completed-led-strip');
|
||||||
|
|
||||||
|
interface CompletedLedStripProps {
|
||||||
|
screenshots: ScreenshotDto[];
|
||||||
|
onDisplayConfigChange?: (value: DisplayConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BorderLedStrip = {
|
||||||
|
pixels: PixelRgb[];
|
||||||
|
config: LedStripConfig | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledContainer = styled.section(
|
||||||
|
({ rows, columns }: { rows: number; columns: number }) => [
|
||||||
|
tw`grid m-4 pb-2 items-center justify-items-center select-none`,
|
||||||
|
tw`overflow-x-auto overflow-y-hidden flex-none`,
|
||||||
|
css`
|
||||||
|
grid-template-columns: repeat(${columns}, 0.5em);
|
||||||
|
grid-template-rows: auto repeat(${rows}, 0.5em);
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||||
|
screenshots,
|
||||||
|
onDisplayConfigChange,
|
||||||
|
}) => {
|
||||||
|
const borderLedStrips: BorderLedStrip[] = useMemo(() => {
|
||||||
|
return screenshots.flatMap((ss) =>
|
||||||
|
borders.map((b) => ({
|
||||||
|
pixels: splitEvery(3, Array.from(ss.colors[b])) as PixelRgb[],
|
||||||
|
config: ss.config.led_strip_of_borders[b],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}, [screenshots]);
|
||||||
|
const ledCount = useMemo(
|
||||||
|
() => borderLedStrips.reduce((prev, curr) => prev + curr.pixels.length, 0),
|
||||||
|
[borderLedStrips],
|
||||||
|
);
|
||||||
|
const maxIndex = useMemo(
|
||||||
|
() =>
|
||||||
|
Math.max(
|
||||||
|
...borderLedStrips
|
||||||
|
.map((s) => [
|
||||||
|
s.config?.global_end_position ?? 0,
|
||||||
|
s.config?.global_start_position ?? 0,
|
||||||
|
])
|
||||||
|
.flat(),
|
||||||
|
),
|
||||||
|
[borderLedStrips],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { setLedCount } = useLedCount();
|
||||||
|
// setLedCount for context
|
||||||
|
useEffect(() => {
|
||||||
|
setLedCount(ledCount);
|
||||||
|
}, [ledCount, setLedCount]);
|
||||||
|
|
||||||
|
const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
|
||||||
|
useState<BorderLedStrip[]>();
|
||||||
|
|
||||||
|
const strips = useMemo(() => {
|
||||||
|
return borderLedStrips.map(({ config, pixels }, index) =>
|
||||||
|
config ? (
|
||||||
|
<DraggableStrip
|
||||||
|
key={index}
|
||||||
|
{...{ config, pixels, index: index + 1 }}
|
||||||
|
onConfigChange={(c) => {
|
||||||
|
setOverrideBorderLedStrips(
|
||||||
|
update(index, { config: c, pixels }, borderLedStrips),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onConfigFinish={(c) => {
|
||||||
|
const indexOfDisplay = Math.floor(index / borders.length);
|
||||||
|
const xLens = lensPath<LedStripConfigOfBorders, Borders>([
|
||||||
|
borders[index % borders.length],
|
||||||
|
]);
|
||||||
|
const displayConfig: DisplayConfig = {
|
||||||
|
...screenshots[indexOfDisplay].config,
|
||||||
|
led_strip_of_borders: set(
|
||||||
|
xLens,
|
||||||
|
c,
|
||||||
|
screenshots[indexOfDisplay].config.led_strip_of_borders,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
onDisplayConfigChange?.(displayConfig);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div key={index} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [borderLedStrips, screenshots]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOverrideBorderLedStrips(undefined);
|
||||||
|
}, [borderLedStrips]);
|
||||||
|
return (
|
||||||
|
<StyledContainer rows={screenshots.length * borders.length} columns={maxIndex + 1}>
|
||||||
|
<CompletedContainer
|
||||||
|
borderLedStrips={borderLedStrips}
|
||||||
|
overrideBorderLedStrips={overrideBorderLedStrips}
|
||||||
|
/>
|
||||||
|
{strips}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
100
src/configurator/components/display-with-led-strips.tsx
Normal file
100
src/configurator/components/display-with-led-strips.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { HTMLAttributes, useCallback, useMemo } from 'react';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
|
||||||
|
import { LedStrip } from './led-strip';
|
||||||
|
import tw, { css, styled, theme } from 'twin.macro';
|
||||||
|
import { ScreenshotDto } from '../models/screenshot.dto';
|
||||||
|
import { LedStripEditor } from './led-strip-editor';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { lensPath, lensProp, set, view } from 'ramda';
|
||||||
|
|
||||||
|
const logger = debug('app:display-with-led-strips');
|
||||||
|
|
||||||
|
export interface DisplayWithLedStripsProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
|
||||||
|
config: DisplayConfig;
|
||||||
|
screenshot: ScreenshotDto;
|
||||||
|
onChange?: (config: DisplayConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.section(
|
||||||
|
tw`m-4 grid gap-1`,
|
||||||
|
css`
|
||||||
|
grid-template-columns: ${theme`width.5`} ${theme`width.3`} auto ${theme`width.3`} ${theme`width.5`};
|
||||||
|
`,
|
||||||
|
css`
|
||||||
|
grid-template-rows: ${theme`width.5`} ${theme`width.3`} auto ${theme`width.3`} ${theme`width.5`};
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DisplayWithLedStrips: FC<DisplayWithLedStripsProps> = ({
|
||||||
|
config,
|
||||||
|
screenshot,
|
||||||
|
onChange,
|
||||||
|
...htmlAttrs
|
||||||
|
}) => {
|
||||||
|
const screenshotUrl = useMemo(
|
||||||
|
() => `data:image/ico;base64,${screenshot.encode_image}`,
|
||||||
|
[screenshot.encode_image],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLedStripConfigChange = useCallback(
|
||||||
|
(position: keyof LedStripConfigOfBorders, value: LedStripConfig | null) => {
|
||||||
|
const xLens = lensPath<
|
||||||
|
DisplayConfig,
|
||||||
|
'led_strip_of_borders',
|
||||||
|
keyof LedStripConfigOfBorders
|
||||||
|
>(['led_strip_of_borders', position]);
|
||||||
|
const c = set(xLens, value, config);
|
||||||
|
logger('on change. prev: %o, curr: %o', view(xLens, config), value);
|
||||||
|
onChange?.(c);
|
||||||
|
},
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<StyledContainer {...htmlAttrs}>
|
||||||
|
<img src={screenshotUrl} tw="row-start-3 col-start-3 w-full" />
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.top}
|
||||||
|
colors={screenshot.colors.top}
|
||||||
|
tw="row-start-2 col-start-3"
|
||||||
|
/>
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.left}
|
||||||
|
colors={screenshot.colors.left}
|
||||||
|
tw="row-start-3 col-start-2"
|
||||||
|
/>
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.right}
|
||||||
|
colors={screenshot.colors.right}
|
||||||
|
tw="row-start-3 col-start-4"
|
||||||
|
/>
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.bottom}
|
||||||
|
colors={screenshot.colors.bottom}
|
||||||
|
tw="row-start-4 col-start-3"
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.top}
|
||||||
|
tw="row-start-1 col-start-3"
|
||||||
|
onChange={(value) => onLedStripConfigChange('top', value)}
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.left}
|
||||||
|
tw="row-start-3 col-start-1"
|
||||||
|
onChange={(value) => onLedStripConfigChange('left', value)}
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.right}
|
||||||
|
tw="row-start-3 col-start-5"
|
||||||
|
onChange={(value) => onLedStripConfigChange('right', value)}
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.bottom}
|
||||||
|
tw="row-start-5 col-start-3"
|
||||||
|
onChange={(value) => onLedStripConfigChange('bottom', value)}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
199
src/configurator/components/draggable-strip.tsx
Normal file
199
src/configurator/components/draggable-strip.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import {
|
||||||
|
createRef,
|
||||||
|
FC,
|
||||||
|
Fragment,
|
||||||
|
MouseEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { css } from 'twin.macro';
|
||||||
|
import { useLedCount } from '../contents/led-count';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import { PixelRgb } from '../models/pixel-rgb';
|
||||||
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
import { logger } from './completed-led-strip';
|
||||||
|
|
||||||
|
interface DraggableStripProp {
|
||||||
|
config: LedStripConfig;
|
||||||
|
pixels: PixelRgb[];
|
||||||
|
index: number;
|
||||||
|
onConfigChange?: (config: LedStripConfig) => void;
|
||||||
|
onConfigFinish?: (config: LedStripConfig) => void;
|
||||||
|
}
|
||||||
|
export const DraggableStrip: FC<DraggableStripProp> = ({
|
||||||
|
config,
|
||||||
|
pixels,
|
||||||
|
index,
|
||||||
|
onConfigChange,
|
||||||
|
onConfigFinish,
|
||||||
|
}) => {
|
||||||
|
const { ledCount } = useLedCount();
|
||||||
|
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const currentXRef = useRef(0);
|
||||||
|
const configRef = useRef<LedStripConfig>();
|
||||||
|
const [availableConfig, setAvailableConfig] = useState<LedStripConfig>(config);
|
||||||
|
// const currentDiffRef = useRef(0);
|
||||||
|
const isDragRef = useRef(false);
|
||||||
|
const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>();
|
||||||
|
const [boxTranslateX, setBoxTranslateX] = useState(0);
|
||||||
|
|
||||||
|
const ledItems = useMemo(() => {
|
||||||
|
const step = config.global_start_position - config.global_end_position < 0 ? 1 : -1;
|
||||||
|
return pixels.map((rgb, i) => (
|
||||||
|
<StyledPixel
|
||||||
|
key={i}
|
||||||
|
rgb={rgb}
|
||||||
|
css={css`
|
||||||
|
grid-column: ${(availableConfig.global_start_position ?? 0) + i * step + 1} /
|
||||||
|
span 1;
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
pointer-events: none;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [pixels, availableConfig]);
|
||||||
|
|
||||||
|
const [placeholders, placeholderRefs]: [ReactNode[], RefObject<HTMLSpanElement>[]] =
|
||||||
|
useMemo(
|
||||||
|
() =>
|
||||||
|
new Array(ledCount)
|
||||||
|
.fill(undefined)
|
||||||
|
.map((_, i) => {
|
||||||
|
const ref = createRef<HTMLSpanElement>();
|
||||||
|
const n = (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
key={i}
|
||||||
|
tw=" h-full w-full"
|
||||||
|
css={css`
|
||||||
|
grid-column-start: ${i + 1};
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return [n, ref] as [ReactNode, RefObject<HTMLSpanElement>];
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
([nList, refList], [n, ref]) => [
|
||||||
|
[...nList, n],
|
||||||
|
[...refList, ref],
|
||||||
|
],
|
||||||
|
[[], []] as [ReactNode[], RefObject<HTMLSpanElement>[]],
|
||||||
|
),
|
||||||
|
[ledCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
// start and moving
|
||||||
|
const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
startXRef.current = ev.pageX;
|
||||||
|
ev.currentTarget.requestPointerLock();
|
||||||
|
isDragRef.current = true;
|
||||||
|
logger('handleMouseDown, config: %o', config);
|
||||||
|
|
||||||
|
const placeholderPositions = placeholderRefs.map((it) => {
|
||||||
|
if (!it.current) {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
const viewportOffset = it.current.getBoundingClientRect();
|
||||||
|
return [viewportOffset.left, viewportOffset.right] as [number, number];
|
||||||
|
});
|
||||||
|
|
||||||
|
logger('placeholderPositions: %o', placeholderPositions);
|
||||||
|
|
||||||
|
// set init position
|
||||||
|
const initPos = placeholderPositions.findIndex(
|
||||||
|
([l, r]) => l <= ev.pageX && r >= ev.pageX,
|
||||||
|
);
|
||||||
|
let prevMatch = 0;
|
||||||
|
|
||||||
|
if (handleMouseMoveRef.current) {
|
||||||
|
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
|
||||||
|
}
|
||||||
|
handleMouseMoveRef.current = (ev) => {
|
||||||
|
if (!isDragRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentXRef.current = ev.pageX;
|
||||||
|
setBoxTranslateX(currentXRef.current - startXRef.current);
|
||||||
|
const match = placeholderPositions.findIndex(
|
||||||
|
([l, r]) => l <= currentXRef.current && r >= currentXRef.current,
|
||||||
|
);
|
||||||
|
if (match === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match === prevMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevMatch = match;
|
||||||
|
|
||||||
|
const diff = match - initPos;
|
||||||
|
const newValue: LedStripConfig = {
|
||||||
|
...config,
|
||||||
|
global_start_position: config.global_start_position + diff,
|
||||||
|
global_end_position: config.global_end_position + diff,
|
||||||
|
};
|
||||||
|
configRef.current = newValue;
|
||||||
|
setAvailableConfig(newValue);
|
||||||
|
logger('change config. old: %o, new: %o', config, newValue);
|
||||||
|
onConfigChange?.(newValue);
|
||||||
|
};
|
||||||
|
document.body.addEventListener('mousemove', handleMouseMoveRef.current);
|
||||||
|
},
|
||||||
|
[placeholderRefs, availableConfig, setAvailableConfig, config],
|
||||||
|
);
|
||||||
|
|
||||||
|
// move event.
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseUp = (ev: MouseEvent) => {
|
||||||
|
if (configRef.current && isDragRef.current) {
|
||||||
|
onConfigFinish?.(configRef.current);
|
||||||
|
}
|
||||||
|
startXRef.current = 0;
|
||||||
|
isDragRef.current = false;
|
||||||
|
document.exitPointerLock();
|
||||||
|
if (handleMouseMoveRef.current) {
|
||||||
|
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.body.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [onConfigFinish]);
|
||||||
|
// reset translateX when config updated.
|
||||||
|
useEffect(() => {
|
||||||
|
startXRef.current = currentXRef.current;
|
||||||
|
setAvailableConfig(config);
|
||||||
|
setBoxTranslateX(0);
|
||||||
|
logger('useEffect, config: %o', config);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{placeholders}
|
||||||
|
{ledItems}
|
||||||
|
<div
|
||||||
|
tw="border border-gray-700 h-3 w-full rounded-full"
|
||||||
|
css={css`
|
||||||
|
grid-column: ${Math.min(
|
||||||
|
config?.global_start_position ?? 0,
|
||||||
|
config?.global_end_position ?? 0,
|
||||||
|
) + 1} / span
|
||||||
|
${Math.abs(config?.global_start_position - config?.global_end_position) + 1};
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
cursor: ew-resize;
|
||||||
|
transform: translateX(${boxTranslateX}px);
|
||||||
|
`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
></div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
104
src/configurator/components/led-strip-editor.tsx
Normal file
104
src/configurator/components/led-strip-editor.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { HTMLAttributes, MouseEventHandler, useCallback } from 'react';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import tw, { styled } from 'twin.macro';
|
||||||
|
import { faLeftRight, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
export interface LedStripEditorProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
|
||||||
|
config: LedStripConfig | null;
|
||||||
|
onChange?: (config: LedStripConfig | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.section(
|
||||||
|
tw`flex flex-wrap gap-2 self-start justify-self-start`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledButton = styled.button(
|
||||||
|
tw`
|
||||||
|
bg-yellow-500 dark:bg-amber-600 rounded-full h-4 w-4 text-xs shadow select-none`,
|
||||||
|
tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600 active:dark:bg-amber-500`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LedStripEditor: FC<LedStripEditorProps> = ({
|
||||||
|
config,
|
||||||
|
onChange,
|
||||||
|
...htmlAttrs
|
||||||
|
}) => {
|
||||||
|
const addLed: MouseEventHandler = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const delta = ev.button === 2 ? 10 : 1;
|
||||||
|
if (config) {
|
||||||
|
if (config.global_start_position <= config.global_end_position) {
|
||||||
|
onChange?.({
|
||||||
|
...config,
|
||||||
|
global_end_position: config.global_end_position + delta,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onChange?.({
|
||||||
|
...config,
|
||||||
|
global_start_position: config.global_start_position + delta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onChange?.(new LedStripConfig(0, 0, 0));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[config, onChange],
|
||||||
|
);
|
||||||
|
const removeLed: MouseEventHandler = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
const delta = ev.button === 2 ? 10 : 1;
|
||||||
|
if (!config) {
|
||||||
|
onChange?.(null);
|
||||||
|
} else if (
|
||||||
|
Math.abs(config.global_start_position - config.global_end_position) <= delta
|
||||||
|
) {
|
||||||
|
onChange?.(null);
|
||||||
|
} else {
|
||||||
|
if (config.global_start_position <= config.global_end_position) {
|
||||||
|
onChange?.({
|
||||||
|
...config,
|
||||||
|
global_end_position: config.global_end_position - delta,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onChange?.({
|
||||||
|
...config,
|
||||||
|
global_start_position: config.global_start_position - delta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[config, onChange],
|
||||||
|
);
|
||||||
|
const reverse = useCallback(() => {
|
||||||
|
if (!config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onChange?.({
|
||||||
|
...config,
|
||||||
|
global_start_position: config.global_end_position,
|
||||||
|
global_end_position: config.global_start_position,
|
||||||
|
});
|
||||||
|
}, [config, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledContainer {...htmlAttrs}>
|
||||||
|
<StyledButton title="Add LED" onClick={addLed} onContextMenu={addLed}>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton title="Remove LED" onClick={removeLed} onContextMenu={removeLed}>
|
||||||
|
<FontAwesomeIcon icon={faMinus} />
|
||||||
|
</StyledButton>
|
||||||
|
<StyledButton title="Reverse" onClick={reverse}>
|
||||||
|
<FontAwesomeIcon icon={faLeftRight} />
|
||||||
|
</StyledButton>
|
||||||
|
{`s: ${config?.global_start_position ?? 'x'}, e: ${
|
||||||
|
config?.global_end_position ?? 'x'
|
||||||
|
}`}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
28
src/configurator/components/led-strip.tsx
Normal file
28
src/configurator/components/led-strip.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { HTMLAttributes, useMemo } from 'react';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
import { splitEvery } from 'ramda';
|
||||||
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
|
||||||
|
export interface LedStripProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
config: LedStripConfig | null;
|
||||||
|
colors: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.section(
|
||||||
|
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center -mx-px -mt-px`,
|
||||||
|
css``,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LedStrip: FC<LedStripProps> = ({ config, colors, ...htmlAttrs }) => {
|
||||||
|
const pixels = useMemo(() => {
|
||||||
|
const pixels = splitEvery(3, Array.from(colors)) as Array<[number, number, number]>;
|
||||||
|
return pixels.map((rgb, index) => <StyledPixel key={index} rgb={rgb}></StyledPixel>);
|
||||||
|
}, [colors]);
|
||||||
|
return (
|
||||||
|
<StyledContainer {...htmlAttrs} css={[!config && tw`bg-gray-200`]}>
|
||||||
|
{pixels}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
10
src/configurator/components/styled-pixel.tsx
Normal file
10
src/configurator/components/styled-pixel.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
|
||||||
|
export const StyledPixel = styled.span(
|
||||||
|
({ rgb: [r, g, b] }: { rgb: [number, number, number] }) => [
|
||||||
|
tw`rounded-full h-3 w-3 bg-current block border border-gray-700`,
|
||||||
|
css`
|
||||||
|
color: rgb(${r}, ${g}, ${b});
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
);
|
146
src/configurator/configurator.tsx
Normal file
146
src/configurator/configurator.tsx
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api';
|
||||||
|
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
import tw, { styled } from 'twin.macro';
|
||||||
|
import { useAsync, useAsyncCallback } from 'react-async-hook';
|
||||||
|
import { DisplayWithLedStrips } from './components/display-with-led-strips';
|
||||||
|
import { PickerConfiguration } from './models/picker-configuration';
|
||||||
|
import { DisplayConfig } from './models/display-config';
|
||||||
|
import { ScreenshotDto } from './models/screenshot.dto';
|
||||||
|
import { Alert, Fab, Snackbar } from '@mui/material';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { faRotateBack, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { update } from 'ramda';
|
||||||
|
import { CompletedLedStrip } from './components/completed-led-strip';
|
||||||
|
import { LedCountProvider } from './contents/led-count';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { useSnackbar } from 'notistack';
|
||||||
|
import { fillParentCss } from '../styles/fill-parent';
|
||||||
|
|
||||||
|
const logger = debug('app:configurator');
|
||||||
|
|
||||||
|
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
|
||||||
|
const getScreenshotOfDisplays = () =>
|
||||||
|
invoke<ScreenshotDto[]>('take_snapshot').then((items) => {
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
const getScreenshotByConfig = async (config: DisplayConfig) => {
|
||||||
|
return await invoke<ScreenshotDto>('get_screenshot_by_config', {
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const writePickerConfig = async (config: PickerConfiguration) => {
|
||||||
|
return await invoke<void>('write_picker_config', {
|
||||||
|
config,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const StyledConfiguratorContainer = styled.section(
|
||||||
|
tw`flex flex-col items-stretch relative overflow-hidden`,
|
||||||
|
fillParentCss,
|
||||||
|
);
|
||||||
|
|
||||||
|
const StyledDisplayContainer = styled.section(tw`overflow-auto`);
|
||||||
|
|
||||||
|
const ConfiguratorInner: FC = () => {
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
||||||
|
getPickerConfig,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const { loading: pendingScreenshotOfDisplays, result: defaultScreenshotOfDisplays } =
|
||||||
|
useAsync(getScreenshotOfDisplays, []);
|
||||||
|
|
||||||
|
const [screenshotOfDisplays, setScreenshotOfDisplays] = useState<ScreenshotDto[]>([]);
|
||||||
|
|
||||||
|
const { loading: pendingGetLedColorsByConfig, execute: onDisplayConfigChange } =
|
||||||
|
useAsyncCallback(async (value: DisplayConfig) => {
|
||||||
|
const screenshot = await getScreenshotByConfig(value);
|
||||||
|
setScreenshotOfDisplays((old) => {
|
||||||
|
const index = old.findIndex((it) => it.config.id === screenshot.config.id);
|
||||||
|
const newValue = update(index, screenshot, old);
|
||||||
|
savedPickerConfig &&
|
||||||
|
writePickerConfig({
|
||||||
|
...savedPickerConfig,
|
||||||
|
display_configs: newValue.map((it) => it.config),
|
||||||
|
});
|
||||||
|
return newValue;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const [displayConfigs, setDisplayConfigs] = useState<DisplayConfig[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const displayConfigs = savedPickerConfig?.display_configs;
|
||||||
|
if (displayConfigs && defaultScreenshotOfDisplays) {
|
||||||
|
setDisplayConfigs(displayConfigs);
|
||||||
|
setScreenshotOfDisplays(defaultScreenshotOfDisplays);
|
||||||
|
(async () => {
|
||||||
|
for (const config of displayConfigs) {
|
||||||
|
await onDisplayConfigChange(config);
|
||||||
|
}
|
||||||
|
})().then();
|
||||||
|
}
|
||||||
|
}, [savedPickerConfig, onDisplayConfigChange, defaultScreenshotOfDisplays]);
|
||||||
|
|
||||||
|
const resetBackToDefaultConfig = useCallback(() => {
|
||||||
|
if (defaultScreenshotOfDisplays) {
|
||||||
|
setScreenshotOfDisplays(defaultScreenshotOfDisplays);
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar('Default Config was not found. please try again later.', {
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [setScreenshotOfDisplays, defaultScreenshotOfDisplays]);
|
||||||
|
|
||||||
|
const displays = useMemo(() => {
|
||||||
|
if (screenshotOfDisplays) {
|
||||||
|
return screenshotOfDisplays.map((screenshot, index) => (
|
||||||
|
<DisplayWithLedStrips
|
||||||
|
key={index}
|
||||||
|
config={screenshot.config}
|
||||||
|
screenshot={screenshot}
|
||||||
|
onChange={(value) => onDisplayConfigChange(value)}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}, [displayConfigs, screenshotOfDisplays]);
|
||||||
|
|
||||||
|
if (pendingPickerConfig || pendingScreenshotOfDisplays) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
等待 {JSON.stringify({ pendingPickerConfig, pendingScreenshotOfDisplays })}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledConfiguratorContainer>
|
||||||
|
<CompletedLedStrip
|
||||||
|
screenshots={screenshotOfDisplays}
|
||||||
|
onDisplayConfigChange={onDisplayConfigChange}
|
||||||
|
/>
|
||||||
|
<StyledDisplayContainer tw="overflow-y-auto">{displays}</StyledDisplayContainer>
|
||||||
|
<Fab
|
||||||
|
aria-label="reset"
|
||||||
|
size="small"
|
||||||
|
tw="top-2 right-2 absolute"
|
||||||
|
onClick={resetBackToDefaultConfig}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faRotateBack} />
|
||||||
|
</Fab>
|
||||||
|
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
|
||||||
|
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
||||||
|
This is a success message!
|
||||||
|
</Alert>
|
||||||
|
</Snackbar>
|
||||||
|
</StyledConfiguratorContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Configurator = () => {
|
||||||
|
return (
|
||||||
|
<LedCountProvider>
|
||||||
|
<ConfiguratorInner />
|
||||||
|
</LedCountProvider>
|
||||||
|
);
|
||||||
|
};
|
25
src/configurator/contents/led-count.tsx
Normal file
25
src/configurator/contents/led-count.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface LedCountContext {
|
||||||
|
ledCount: number;
|
||||||
|
setLedCount: Dispatch<SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = createContext<LedCountContext>(undefined as any);
|
||||||
|
|
||||||
|
export const LedCountProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [ledCount, setLedCount] = useState(0);
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ ledCount, setLedCount }}>{children}</Context.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLedCount = () => useContext(Context);
|
21
src/configurator/models/display-config.ts
Normal file
21
src/configurator/models/display-config.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
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,
|
||||||
|
public index_of_display: number,
|
||||||
|
public display_width: number,
|
||||||
|
public display_height: number,
|
||||||
|
) {}
|
||||||
|
}
|
7
src/configurator/models/led-strip-config.ts
Normal file
7
src/configurator/models/led-strip-config.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export class LedStripConfig {
|
||||||
|
constructor(
|
||||||
|
public index: number,
|
||||||
|
public global_start_position: number,
|
||||||
|
public global_end_position: number,
|
||||||
|
) {}
|
||||||
|
}
|
8
src/configurator/models/picker-configuration.ts
Normal file
8
src/configurator/models/picker-configuration.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { DisplayConfig } from './display-config';
|
||||||
|
|
||||||
|
export class PickerConfiguration {
|
||||||
|
constructor(
|
||||||
|
public display_configs: DisplayConfig[] = [],
|
||||||
|
public config_version: number = 1,
|
||||||
|
) {}
|
||||||
|
}
|
1
src/configurator/models/pixel-rgb.ts
Normal file
1
src/configurator/models/pixel-rgb.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type PixelRgb = [number, number, number];
|
12
src/configurator/models/screenshot.dto.ts
Normal file
12
src/configurator/models/screenshot.dto.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { DisplayConfig } from './display-config';
|
||||||
|
|
||||||
|
export class ScreenshotDto {
|
||||||
|
encode_image!: string;
|
||||||
|
config!: DisplayConfig;
|
||||||
|
colors!: {
|
||||||
|
top: Uint8Array;
|
||||||
|
bottom: Uint8Array;
|
||||||
|
left: Uint8Array;
|
||||||
|
right: Uint8Array;
|
||||||
|
};
|
||||||
|
}
|
2
src/constants/border.ts
Normal file
2
src/constants/border.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const borders = ['top', 'right', 'bottom', 'left'] as const;
|
||||||
|
export type Borders = typeof borders[number];
|
16
src/main.tsx
16
src/main.tsx
@@ -1,10 +1,14 @@
|
|||||||
import React from "react";
|
import { SnackbarProvider } from 'notistack';
|
||||||
import ReactDOM from "react-dom/client";
|
import React from 'react';
|
||||||
import App from "./App";
|
import ReactDOM from 'react-dom/client';
|
||||||
import "./style.css";
|
import App from './App';
|
||||||
|
import GlobalStyles from './styles/global-styles';
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<GlobalStyles />
|
||||||
|
<SnackbarProvider maxSnack={3}>
|
||||||
|
<App />
|
||||||
|
</SnackbarProvider>
|
||||||
</React.StrictMode>,
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
3
src/styles/fill-parent.ts
Normal file
3
src/styles/fill-parent.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
|
export const fillParentCss = tw`w-full h-full overflow-hidden`;
|
35
src/styles/global-styles.tsx
Normal file
35
src/styles/global-styles.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Global, css } from '@emotion/react';
|
||||||
|
import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro';
|
||||||
|
import { fillParentCss } from './fill-parent';
|
||||||
|
|
||||||
|
const customStyles = css({
|
||||||
|
body: {
|
||||||
|
WebkitTapHighlightColor: theme`colors.purple.500`,
|
||||||
|
...tw`antialiased`,
|
||||||
|
...tw`dark:bg-dark-800 bg-dark-100 dark:text-gray-100 text-gray-800`,
|
||||||
|
...fillParentCss,
|
||||||
|
},
|
||||||
|
html: {
|
||||||
|
...fillParentCss,
|
||||||
|
},
|
||||||
|
'#root': {
|
||||||
|
...fillParentCss,
|
||||||
|
},
|
||||||
|
'::-webkit-scrollbar': tw`w-1.5 h-1.5`,
|
||||||
|
'::-webkit-scrollbar-button': tw`hidden`,
|
||||||
|
'::-webkit-scrollbar-track': tw`bg-transparent`,
|
||||||
|
'::-webkit-scrollbar-track-piece': tw`bg-transparent`,
|
||||||
|
'::-webkit-scrollbar-thumb': tw`bg-gray-300 dark:(bg-gray-700/50 hover:bg-gray-700/90 active:bg-gray-700/100) transition-colors rounded-full`,
|
||||||
|
'::-webkit-scrollbar-corner': tw`bg-gray-600`,
|
||||||
|
'::-webkit-resizer': tw`hidden`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const GlobalStyles = () => (
|
||||||
|
<>
|
||||||
|
<BaseStyles />
|
||||||
|
<Global styles={customStyles} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GlobalStyles;
|
@@ -1,12 +1,15 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
"./index.html",
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
|
||||||
],
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
colors: {
|
||||||
|
dark: {
|
||||||
|
800: '#0f0f0f',
|
||||||
|
100: '#f6f6f6',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
};
|
||||||
|
@@ -14,8 +14,12 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "@emotion/react"
|
||||||
},
|
},
|
||||||
"include": ["src"],
|
"include": [
|
||||||
|
"src",
|
||||||
|
"types"
|
||||||
|
],
|
||||||
"references": [{ "path": "./tsconfig.node.json" }]
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
}
|
}
|
||||||
|
18
types/twin.d.ts
vendored
Normal file
18
types/twin.d.ts
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'twin.macro';
|
||||||
|
import { css as cssImport } from '@emotion/react';
|
||||||
|
import styledImport from '@emotion/styled';
|
||||||
|
import { CSSInterpolation } from '@emotion/serialize';
|
||||||
|
|
||||||
|
declare module 'twin.macro' {
|
||||||
|
// The styled and css imports
|
||||||
|
const styled: typeof styledImport;
|
||||||
|
const css: typeof cssImport;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'react' {
|
||||||
|
// The tw and css prop
|
||||||
|
interface DOMAttributes<T> {
|
||||||
|
tw?: string;
|
||||||
|
css?: CSSInterpolation;
|
||||||
|
}
|
||||||
|
}
|
@@ -3,7 +3,29 @@ import react from "@vitejs/plugin-react";
|
|||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
optimizeDeps: {
|
||||||
|
esbuildOptions: {
|
||||||
|
target: 'es2020',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react({
|
||||||
|
babel: {
|
||||||
|
plugins: [
|
||||||
|
'babel-plugin-macros',
|
||||||
|
[
|
||||||
|
'@emotion/babel-plugin-jsx-pragmatic',
|
||||||
|
{
|
||||||
|
export: 'jsx',
|
||||||
|
import: '__cssprop',
|
||||||
|
module: '@emotion/react',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['@babel/plugin-transform-react-jsx', { pragma: '__cssprop' }, 'twin.macro'],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||||
// prevent vite from obscuring rust errors
|
// prevent vite from obscuring rust errors
|
||||||
@@ -15,12 +37,12 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
// to make use of `TAURI_DEBUG` and other env variables
|
// to make use of `TAURI_DEBUG` and other env variables
|
||||||
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
// https://tauri.studio/v1/api/config#buildconfig.beforedevcommand
|
||||||
envPrefix: ["VITE_", "TAURI_"],
|
envPrefix: ['VITE_', 'TAURI_'],
|
||||||
build: {
|
build: {
|
||||||
// Tauri supports es2021
|
// Tauri supports es2021
|
||||||
target: ["es2021", "chrome100", "safari13"],
|
target: ['es2021', 'chrome100', 'safari13'],
|
||||||
// don't minify for debug builds
|
// don't minify for debug builds
|
||||||
minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
|
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
||||||
// produce sourcemaps for debug builds
|
// produce sourcemaps for debug builds
|
||||||
sourcemap: !!process.env.TAURI_DEBUG,
|
sourcemap: !!process.env.TAURI_DEBUG,
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user