19 Commits

Author SHA1 Message Date
596da96f8c feat: 更改截图逻辑。 2023-03-19 10:17:13 +08:00
653729fcc2 chore: 更新。 2023-03-16 20:13:08 +08:00
eeddff1dc1 feat: 命令执行成功后发布相关状态到 MQ. #5. 2023-02-19 17:38:09 +08:00
e09b93432c feat: MQTT client 改为 paho-mqtt. #5. 2023-02-19 17:37:29 +08:00
6e65ef1a4d feat: 通知当前亮度,重构 mqtt 相关代码。#5. 2023-02-19 10:13:45 +08:00
550328ba1e pref: 缓存显示器参数读数。#5. 2023-02-18 14:47:41 +08:00
070100cdbc build: 清理代码并且升级依赖。 2023-02-12 15:03:40 +08:00
5422cca393 chore: 细节调整。#5. 2023-01-31 16:29:56 +08:00
4e3b765059 feat: 通过增量、绝对量亮度控制。#5. 2023-01-29 20:53:59 +08:00
6ec61c1bcc chore: log mqtt error. 2023-01-29 04:18:57 +08:00
bd025d6d71 feat: 右键点击每一边的 LED 增减按钮时,按增减十个计。 2023-01-29 00:41:11 +08:00
45f3f79a49 fix: 全局 LED 配置样式问题。 2023-01-29 00:39:51 +08:00
a7137f0d51 feat: reset config button. 2023-01-28 22:19:32 +08:00
a33659c8c5 fix: 切换到后台后一分钟内,截图效率降低的问题。 2023-01-26 23:49:54 +08:00
2058220ead Merge pull request 'feature/gui-configuration:支持从 GUI 配置程序。' (#4) from feature/gui-configuration into master
Reviewed-on: #4
2023-01-26 23:44:19 +08:00
af671a4e63 style: 外观改进。 2023-01-19 23:54:10 +08:00
d3dfdb4d82 fix: 当配置反向时,颜色错误的问题。 2023-01-17 00:41:03 +08:00
a55db2553b fix: 输出信号的 led 顺序问题。 2023-01-16 23:39:59 +08:00
1882b8ccdc feat: 使用左开右闭区间定义每边的 LEDs。 2023-01-16 23:39:33 +08:00
35 changed files with 2158 additions and 1125 deletions

View File

@ -10,44 +10,45 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.10.5", "@emotion/react": "^11.10.6",
"@emotion/styled": "^11.10.5", "@emotion/styled": "^11.10.6",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.3.0",
"@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-regular-svg-icons": "^6.3.0",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@mui/material": "^5.11.4", "@mui/material": "^5.11.13",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"notistack": "^2.0.8",
"ramda": "^0.28.0", "ramda": "^0.28.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-react-jsx": "^7.20.7", "@babel/plugin-transform-react-jsx": "^7.21.0",
"@emotion/babel-plugin-jsx-pragmatic": "^0.2.0", "@emotion/babel-plugin-jsx-pragmatic": "^0.2.0",
"@emotion/serialize": "^1.1.1", "@emotion/serialize": "^1.1.1",
"@tauri-apps/cli": "^1.2.2", "@tauri-apps/cli": "^1.2.3",
"@types/debug": "^4.1.7", "@types/debug": "^4.1.7",
"@types/node": "^18.11.18", "@types/node": "^18.15.3",
"@types/ramda": "^0.28.20", "@types/ramda": "^0.28.23",
"@types/react": "^18.0.26", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.14",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"eslint-config-prettier": "^8.6.0", "eslint-config-prettier": "^8.7.0",
"eslint-plugin-import": "^2.27.4", "eslint-plugin-import": "^2.27.5",
"eslint-plugin-jsx-a11y": "^6.7.1", "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.21", "postcss": "^8.4.21",
"prettier": "^2.8.3", "prettier": "^2.8.4",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.7",
"twin.macro": "^3.1.0", "twin.macro": "^3.1.0",
"typescript": "^4.9.4", "typescript": "^4.9.5",
"vite": "^3.2.5" "vite": "^3.2.5"
} }
} }

726
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

1362
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -28,21 +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" image = "0.24.5"
mdns = "3.0.0" 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"

View File

@ -1,6 +1,6 @@
use futures::future::join_all; use futures::future::join_all;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use paris::{error, info}; use paris::{error, info, warn};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
@ -13,7 +13,6 @@ use tokio::{
sync::mpsc, sync::mpsc,
time::{sleep, Instant}, time::{sleep, Instant},
}; };
use tracing::warn;
use crate::{ use crate::{
picker::{ picker::{
@ -76,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
{ {
@ -93,6 +93,7 @@ impl CoreManager {
} }
pub async fn play_follow(&self) -> anyhow::Result<()> { pub async fn play_follow(&self) -> anyhow::Result<()> {
macos_app_nap::prevent();
let mut futs = vec![]; let mut futs = vec![];
let configs = picker::config::Manager::global().reload_config().await; let configs = picker::config::Manager::global().reload_config().await;
let configs = match configs { let configs = match configs {
@ -116,7 +117,8 @@ impl CoreManager {
futs.push(fut); futs.push(fut);
} }
let total_colors_count = configs.iter() let total_colors_count = configs
.iter()
.flat_map(|c| { .flat_map(|c| {
vec![ vec![
c.led_strip_of_borders.top, c.led_strip_of_borders.top,
@ -136,36 +138,62 @@ impl CoreManager {
.collect::<HashSet<_>>() .collect::<HashSet<_>>()
.len(); .len();
tokio::spawn(async move { tokio::spawn(async move {
let mut global_colors = HashMap::new(); let mut global_sub_pixels = HashMap::new();
while let Some(screenshot) = rx.recv().await { while let Some(screenshot) = rx.recv().await {
let start_at = Instant::now(); let colors = screenshot.get_colors();
let colors = screenshot.get_top_colors(); let config = screenshot.get_config();
let start = screenshot for (colors, config) in vec![
.get_top_of_led_start_at() (colors.top, config.led_strip_of_borders.top),
.min(screenshot.get_top_of_led_end_at()); (colors.right, config.led_strip_of_borders.right),
(colors.bottom, config.led_strip_of_borders.bottom),
for (index, color) in colors.into_iter().enumerate() { (colors.left, config.led_strip_of_borders.left),
global_colors.insert(index + start, color); ] {
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!( // info!(
"led count: {}, spend: {:?}", // "led count: {}, spend: {:?}",
global_colors.len(), // global_sub_pixels.len(),
start_at.elapsed() // start_at.elapsed()
); // );
if global_colors.len() >= total_colors_count { if global_sub_pixels.len() >= total_colors_count * 3 {
let mut colors = vec![]; let mut colors = vec![];
for index in 0..global_colors.len() { for index in 0..global_sub_pixels.len() {
colors.push(*global_colors.get(&index).unwrap()); colors.push(*global_sub_pixels.get(&index).unwrap_or(&100));
} }
global_colors = HashMap::new(); // info!("{:?}", colors);
match rpc::manager::Manager::global() global_sub_pixels = HashMap::new();
.publish_led_colors(&colors) match rpc::Manager::global()
.await
.publish_led_sub_pixels(colors)
.await .await
{ {
Ok(_) => { Ok(_) => {
info!("publish successful",); // info!("publish successful",);
} }
Err(error) => { Err(error) => {
warn!("publish led colors failed. {}", error); warn!("publish led colors failed. {}", error);
@ -194,7 +222,7 @@ 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());
match tx.send(screenshot).await { match tx.send(screenshot).await {
Ok(_) => {} Ok(_) => {}
Err(err) => { Err(err) => {

25
src-tauri/src/db/db.rs Normal file
View 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
View File

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

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

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

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

View File

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

View File

@ -5,24 +5,30 @@
#![feature(bool_to_option)] #![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::config::DisplayConfig; use picker::config::DisplayConfig;
use picker::manager::Picker; use picker::manager::Picker;
use picker::screenshot::ScreenshotDto; use picker::screenshot::ScreenshotDto;
use tauri::async_runtime::Mutex; use tauri::async_runtime::Mutex;
use once_cell::sync::OnceCell;
static GET_SCREENSHOT_LOCK: OnceCell<Mutex<bool>> = OnceCell::new(); static GET_SCREENSHOT_LOCK: OnceCell<Mutex<bool>> = OnceCell::new();
#[tauri::command] #[tauri::command]
async fn take_snapshot() -> Vec<ScreenshotDto> { async fn take_snapshot() -> Vec<ScreenshotDto> {
info!("Hi?"); info!("Hi?");
let _lock = GET_SCREENSHOT_LOCK.get_or_init(|| Mutex::new(false)).lock().await; let _lock = GET_SCREENSHOT_LOCK
.get_or_init(|| Mutex::new(false))
.lock()
.await;
info!("Hi!"); info!("Hi!");
let manager = Picker::global().await; let manager = Picker::global().await;
@ -89,7 +95,11 @@ async fn play_mode(target_mode: AmbientLightMode) {
#[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,

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

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

View 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),
}

View 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::*;

View 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>),
}

View File

@ -17,11 +17,12 @@ pub struct LedStripConfig {
#[derive(Clone, Copy, Serialize, Deserialize, Debug)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct DisplayConfig { pub struct DisplayConfig {
pub id: usize, 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 led_strip_of_borders: LedStripConfigOfBorders, pub led_strip_of_borders: LedStripConfigOfBorders,
pub scale_factor: f32,
} }
impl LedStripConfigOfBorders { impl LedStripConfigOfBorders {
@ -37,10 +38,11 @@ impl LedStripConfigOfBorders {
impl DisplayConfig { impl DisplayConfig {
pub fn default( pub fn default(
id: usize, id: u32,
index_of_display: usize, index_of_display: usize,
display_width: usize, display_width: usize,
display_height: usize, display_height: usize,
scale_factor: f32,
) -> Self { ) -> Self {
Self { Self {
id, id,
@ -48,6 +50,7 @@ impl DisplayConfig {
display_width, display_width,
display_height, display_height,
led_strip_of_borders: LedStripConfigOfBorders::default(), led_strip_of_borders: LedStripConfigOfBorders::default(),
scale_factor,
} }
} }
} }

View File

@ -115,7 +115,6 @@ mod tests {
async 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( crate::picker::config::manger::Manager::write_config_to_disk(
config_file_path.clone(), config_file_path.clone(),
&Configuration::default(), &Configuration::default(),

View File

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

View File

@ -1,3 +1,5 @@
use core_graphics::display::CGDisplay;
use display_info::DisplayInfo;
use futures::{stream::FuturesUnordered, StreamExt}; use futures::{stream::FuturesUnordered, StreamExt};
use paris::info; use paris::info;
use scrap::Display; use scrap::Display;
@ -43,16 +45,16 @@ impl Picker {
pub async fn list_displays(&self) -> anyhow::Result<Vec<ScreenshotDto>> { pub async fn list_displays(&self) -> anyhow::Result<Vec<ScreenshotDto>> {
let mut configs = vec![]; let mut configs = vec![];
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, index, width, height); let config = DisplayConfig::default(display.id, index, width, height, display.scale_factor);
configs.push(config); configs.push(config);
} }

View File

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

View File

@ -41,7 +41,7 @@ impl Screenshot {
config.display_width, config.display_width,
led_strip_config led_strip_config
.global_start_position .global_start_position
.abs_diff(led_strip_config.global_end_position), .abs_diff(led_strip_config.global_end_position) + 1,
5, 5,
), ),
None => { None => {
@ -56,7 +56,7 @@ impl Screenshot {
config.display_width, config.display_width,
led_strip_config led_strip_config
.global_start_position .global_start_position
.abs_diff(led_strip_config.global_end_position), .abs_diff(led_strip_config.global_end_position) + 1,
5, 5,
); );
points points
@ -81,7 +81,7 @@ impl Screenshot {
config.display_height, config.display_height,
led_strip_config led_strip_config
.global_start_position .global_start_position
.abs_diff(led_strip_config.global_end_position), .abs_diff(led_strip_config.global_end_position) + 1,
5, 5,
); );
points points
@ -103,7 +103,7 @@ impl Screenshot {
config.display_height, config.display_height,
led_strip_config led_strip_config
.global_start_position .global_start_position
.abs_diff(led_strip_config.global_end_position), .abs_diff(led_strip_config.global_end_position) + 1,
5, 5,
); );
points points
@ -212,23 +212,6 @@ impl Screenshot {
colors colors
} }
pub fn get_top_colors(&self) -> Vec<LedColor> {
self.get_one_edge_colors(&self.sample_points.top)
}
pub fn get_top_of_led_start_at(&self) -> usize {
self.config
.led_strip_of_borders.top
.and_then(|c| Some(c.global_start_position))
.unwrap_or(0)
}
pub fn get_top_of_led_end_at(&self) -> usize {
self.config
.led_strip_of_borders.top
.and_then(|c| Some(c.global_end_position))
.unwrap_or(0)
}
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 stride = bitmap.len() / self.config.display_height; let stride = bitmap.len() / self.config.display_height;
@ -280,6 +263,11 @@ impl Screenshot {
colors, colors,
} }
} }
pub fn get_config(&self) -> DisplayConfig {
self.config
}
} }
#[derive(Clone, Serialize, Deserialize, Debug)] #[derive(Clone, Serialize, Deserialize, Debug)]

View File

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

View File

@ -1,2 +1,4 @@
pub mod manager; mod manager;
pub mod mqtt; pub mod mqtt;
pub use manager::*;

View File

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

View File

@ -1,9 +1,10 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useState } from 'react';
import tw from 'twin.macro'; 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 { Configurator } from './configurator/configurator'; import { Configurator } from './configurator/configurator';
import { ButtonSwitch } from './commons/components/button'; import { ButtonSwitch } from './commons/components/button';
import { fillParentCss } from './styles/fill-parent';
type Mode = 'Flowing' | 'Follow' | null; type Mode = 'Flowing' | 'Follow' | null;
@ -40,7 +41,7 @@ function App() {
); );
return ( return (
<div> <div css={[fillParentCss]} tw="box-border flex flex-col">
<div tw="flex justify-between"> <div tw="flex justify-between">
{ledStripColors.map((it) => ( {ledStripColors.map((it) => (
<span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span> <span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span>
@ -55,7 +56,7 @@ function App() {
))} ))}
</div> </div>
<div tw="flex gap-5 justify-center "> <div tw="flex gap-5 justify-center">
<ButtonSwitch onClick={() => takeSnapshot()}>Take Snapshot</ButtonSwitch> <ButtonSwitch onClick={() => takeSnapshot()}>Take Snapshot</ButtonSwitch>
<ButtonSwitch onClick={() => getLedStripColors()}>Get Colors</ButtonSwitch> <ButtonSwitch onClick={() => getLedStripColors()}>Get Colors</ButtonSwitch>
<ButtonSwitch onClick={() => switchCurrentMode('Flowing')}> <ButtonSwitch onClick={() => switchCurrentMode('Flowing')}>
@ -64,7 +65,7 @@ function App() {
<ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch> <ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch>
</div> </div>
<div tw="flex gap-5 justify-center"> <div css={[fillParentCss]}>
<Configurator /> <Configurator />
</div> </div>
</div> </div>

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

View File

@ -1,5 +1,5 @@
import debug from 'debug'; import debug from 'debug';
import { isNil, lensPath, set, splitEvery, update } from 'ramda'; import { lensPath, set, splitEvery, update } from 'ramda';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useEffect, useMemo, useState } from 'react';
import tw, { css, styled } from 'twin.macro'; import tw, { css, styled } from 'twin.macro';
import { Borders, borders } from '../../constants/border'; import { Borders, borders } from '../../constants/border';
@ -8,8 +8,8 @@ import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config
import { LedStripConfig } from '../models/led-strip-config'; import { LedStripConfig } from '../models/led-strip-config';
import { PixelRgb } from '../models/pixel-rgb'; import { PixelRgb } from '../models/pixel-rgb';
import { ScreenshotDto } from '../models/screenshot.dto'; import { ScreenshotDto } from '../models/screenshot.dto';
import { CompletedContainer } from './completed-container';
import { DraggableStrip } from './draggable-strip'; import { DraggableStrip } from './draggable-strip';
import { StyledPixel } from './styled-pixel';
export const logger = debug('app:completed-led-strip'); export const logger = debug('app:completed-led-strip');
@ -18,7 +18,7 @@ interface CompletedLedStripProps {
onDisplayConfigChange?: (value: DisplayConfig) => void; onDisplayConfigChange?: (value: DisplayConfig) => void;
} }
type BorderLedStrip = { export type BorderLedStrip = {
pixels: PixelRgb[]; pixels: PixelRgb[];
config: LedStripConfig | null; config: LedStripConfig | null;
}; };
@ -26,20 +26,13 @@ type BorderLedStrip = {
const StyledContainer = styled.section( const StyledContainer = styled.section(
({ rows, columns }: { rows: number; columns: number }) => [ ({ rows, columns }: { rows: number; columns: number }) => [
tw`grid m-4 pb-2 items-center justify-items-center select-none`, tw`grid m-4 pb-2 items-center justify-items-center select-none`,
tw`overflow-x-auto overflow-y-hidden flex-none`,
css` css`
grid-template-columns: repeat(${columns}, 1fr); grid-template-columns: repeat(${columns}, 0.5em);
grid-template-rows: auto repeat(${rows}, 1fr); grid-template-rows: auto repeat(${rows}, 0.5em);
`, `,
], ],
); );
const StyledCompletedContainer = styled.section(
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center mb-2`,
css`
grid-column: 1 / -1;
justify-self: stretch;
`,
);
export const CompletedLedStrip: FC<CompletedLedStripProps> = ({ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
screenshots, screenshots,
onDisplayConfigChange, onDisplayConfigChange,
@ -56,6 +49,18 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
() => borderLedStrips.reduce((prev, curr) => prev + curr.pixels.length, 0), () => borderLedStrips.reduce((prev, curr) => prev + curr.pixels.length, 0),
[borderLedStrips], [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(); const { setLedCount } = useLedCount();
// setLedCount for context // setLedCount for context
@ -66,26 +71,6 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
const [overrideBorderLedStrips, setOverrideBorderLedStrips] = const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
useState<BorderLedStrip[]>(); useState<BorderLedStrip[]>();
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} />);
}, [ledCount, borderLedStrips, overrideBorderLedStrips]);
const strips = useMemo(() => { const strips = useMemo(() => {
return borderLedStrips.map(({ config, pixels }, index) => return borderLedStrips.map(({ config, pixels }, index) =>
config ? ( config ? (
@ -123,11 +108,12 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
setOverrideBorderLedStrips(undefined); setOverrideBorderLedStrips(undefined);
}, [borderLedStrips]); }, [borderLedStrips]);
return ( return (
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}> <StyledContainer rows={screenshots.length * borders.length} columns={maxIndex + 1}>
<StyledCompletedContainer>{completedPixels}</StyledCompletedContainer> <CompletedContainer
borderLedStrips={borderLedStrips}
overrideBorderLedStrips={overrideBorderLedStrips}
/>
{strips} {strips}
</StyledContainer> </StyledContainer>
); );
}; };

View File

@ -70,7 +70,7 @@ export const DraggableStrip: FC<DraggableStripProp> = ({
<span <span
ref={ref} ref={ref}
key={i} key={i}
tw="opacity-30 bg-red-500 h-full w-full border border-yellow-400" tw=" h-full w-full"
css={css` css={css`
grid-column-start: ${i + 1}; grid-column-start: ${i + 1};
grid-row-start: ${index + 1}; grid-row-start: ${index + 1};
@ -183,8 +183,11 @@ export const DraggableStrip: FC<DraggableStripProp> = ({
<div <div
tw="border border-gray-700 h-3 w-full rounded-full" tw="border border-gray-700 h-3 w-full rounded-full"
css={css` css={css`
grid-column-start: ${(config?.global_start_position ?? 0) + 1}; grid-column: ${Math.min(
grid-column-end: ${(config?.global_end_position ?? 0) + 1}; 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}; grid-row-start: ${index + 1};
cursor: ew-resize; cursor: ew-resize;
transform: translateX(${boxTranslateX}px); transform: translateX(${boxTranslateX}px);

View File

@ -1,4 +1,4 @@
import { HTMLAttributes, useCallback } from 'react'; import { HTMLAttributes, MouseEventHandler, useCallback } from 'react';
import { FC } from 'react'; import { FC } from 'react';
import { LedStripConfig } from '../models/led-strip-config'; import { LedStripConfig } from '../models/led-strip-config';
import tw, { styled } from 'twin.macro'; import tw, { styled } from 'twin.macro';
@ -17,8 +17,8 @@ const StyledContainer = styled.section(
const StyledButton = styled.button( const StyledButton = styled.button(
tw` tw`
bg-yellow-500 rounded-full h-4 w-4 text-xs shadow select-none`, 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`, tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600 active:dark:bg-amber-500`,
); );
export const LedStripEditor: FC<LedStripEditorProps> = ({ export const LedStripEditor: FC<LedStripEditorProps> = ({
@ -26,36 +26,54 @@ export const LedStripEditor: FC<LedStripEditorProps> = ({
onChange, onChange,
...htmlAttrs ...htmlAttrs
}) => { }) => {
const addLed = useCallback(() => { const addLed: MouseEventHandler = useCallback(
if (config) { (ev) => {
if (config.global_start_position <= config.global_end_position) { ev.preventDefault();
onChange?.({ ...config, global_end_position: config.global_end_position + 1 }); 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 { } else {
onChange?.({ onChange?.(new LedStripConfig(0, 0, 0));
...config,
global_start_position: config.global_start_position + 1,
});
} }
} else { },
onChange?.(new LedStripConfig(0, 0, 1)); [config, onChange],
} );
}, [config, onChange]); const removeLed: MouseEventHandler = useCallback(
const removeLed = useCallback(() => { (ev) => {
if (!config) { ev.preventDefault();
onChange?.(null); const delta = ev.button === 2 ? 10 : 1;
} else if (Math.abs(config.global_start_position - config.global_end_position) <= 1) { if (!config) {
onChange?.(null); onChange?.(null);
} else { } else if (
if (config.global_start_position <= config.global_end_position) { Math.abs(config.global_start_position - config.global_end_position) <= delta
onChange?.({ ...config, global_end_position: config.global_end_position - 1 }); ) {
onChange?.(null);
} else { } else {
onChange?.({ if (config.global_start_position <= config.global_end_position) {
...config, onChange?.({
global_start_position: config.global_start_position - 1, ...config,
}); global_end_position: config.global_end_position - delta,
});
} else {
onChange?.({
...config,
global_start_position: config.global_start_position - delta,
});
}
} }
} },
}, [config, onChange]); [config, onChange],
);
const reverse = useCallback(() => { const reverse = useCallback(() => {
if (!config) { if (!config) {
return; return;
@ -69,10 +87,10 @@ export const LedStripEditor: FC<LedStripEditorProps> = ({
return ( return (
<StyledContainer {...htmlAttrs}> <StyledContainer {...htmlAttrs}>
<StyledButton title="Add LED" onClick={addLed}> <StyledButton title="Add LED" onClick={addLed} onContextMenu={addLed}>
<FontAwesomeIcon icon={faPlus} /> <FontAwesomeIcon icon={faPlus} />
</StyledButton> </StyledButton>
<StyledButton title="Remove LED" onClick={removeLed}> <StyledButton title="Remove LED" onClick={removeLed} onContextMenu={removeLed}>
<FontAwesomeIcon icon={faMinus} /> <FontAwesomeIcon icon={faMinus} />
</StyledButton> </StyledButton>
<StyledButton title="Reverse" onClick={reverse}> <StyledButton title="Reverse" onClick={reverse}>

View File

@ -1,18 +1,20 @@
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { FC, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import tw, { styled } from 'twin.macro'; import tw, { styled } from 'twin.macro';
import { useAsync, useAsyncCallback } from 'react-async-hook'; import { useAsync, useAsyncCallback } from 'react-async-hook';
import { DisplayWithLedStrips } from './components/display-with-led-strips'; import { DisplayWithLedStrips } from './components/display-with-led-strips';
import { PickerConfiguration } from './models/picker-configuration'; import { PickerConfiguration } from './models/picker-configuration';
import { DisplayConfig } from './models/display-config'; import { DisplayConfig } from './models/display-config';
import { ScreenshotDto } from './models/screenshot.dto'; import { ScreenshotDto } from './models/screenshot.dto';
import { Alert, Snackbar } from '@mui/material'; import { Alert, Fab, Snackbar } from '@mui/material';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { faRotateBack, faSpinner } from '@fortawesome/free-solid-svg-icons';
import { update } from 'ramda'; import { update } from 'ramda';
import { CompletedLedStrip } from './components/completed-led-strip'; import { CompletedLedStrip } from './components/completed-led-strip';
import { LedCountProvider } from './contents/led-count'; import { LedCountProvider } from './contents/led-count';
import debug from 'debug'; import debug from 'debug';
import { useSnackbar } from 'notistack';
import { fillParentCss } from '../styles/fill-parent';
const logger = debug('app:configurator'); const logger = debug('app:configurator');
@ -31,11 +33,15 @@ const writePickerConfig = async (config: PickerConfiguration) => {
config, config,
}); });
}; };
const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretch`); const StyledConfiguratorContainer = styled.section(
tw`flex flex-col items-stretch relative overflow-hidden`,
fillParentCss,
);
const StyledDisplayContainer = styled.section(tw`overflow-auto`); const StyledDisplayContainer = styled.section(tw`overflow-auto`);
const ConfiguratorInner: FC = () => { const ConfiguratorInner: FC = () => {
const { enqueueSnackbar } = useSnackbar();
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync( const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
getPickerConfig, getPickerConfig,
[], [],
@ -75,7 +81,16 @@ const ConfiguratorInner: FC = () => {
})().then(); })().then();
} }
}, [savedPickerConfig, onDisplayConfigChange, defaultScreenshotOfDisplays]); }, [savedPickerConfig, onDisplayConfigChange, defaultScreenshotOfDisplays]);
useEffect(() => {}, [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(() => { const displays = useMemo(() => {
if (screenshotOfDisplays) { if (screenshotOfDisplays) {
@ -104,7 +119,15 @@ const ConfiguratorInner: FC = () => {
screenshots={screenshotOfDisplays} screenshots={screenshotOfDisplays}
onDisplayConfigChange={onDisplayConfigChange} onDisplayConfigChange={onDisplayConfigChange}
/> />
<StyledDisplayContainer>{displays}</StyledDisplayContainer>; <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}> <Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}> <Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
This is a success message! This is a success message!

View File

@ -1,11 +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 App from './App';
import GlobalStyles from './styles/global-styles'; 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>
<GlobalStyles /> <GlobalStyles />
<App /> <SnackbarProvider maxSnack={3}>
<App />
</SnackbarProvider>
</React.StrictMode>, </React.StrictMode>,
); );

View File

@ -0,0 +1,3 @@
import tw from 'twin.macro';
export const fillParentCss = tw`w-full h-full overflow-hidden`;

View File

@ -1,13 +1,28 @@
import React from 'react'; import React from 'react';
import { Global, css } from '@emotion/react'; import { Global, css } from '@emotion/react';
import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro'; import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro';
import { fillParentCss } from './fill-parent';
const customStyles = css({ const customStyles = css({
body: { body: {
WebkitTapHighlightColor: theme`colors.purple.500`, WebkitTapHighlightColor: theme`colors.purple.500`,
...tw`antialiased`, ...tw`antialiased`,
...tw`dark:bg-dark-800 bg-dark-100 dark:text-gray-100 text-gray-800`, ...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 = () => ( const GlobalStyles = () => (