Compare commits
18 Commits
af03b22d05
...
feature/di
Author | SHA1 | Date | |
---|---|---|---|
653729fcc2 | |||
eeddff1dc1 | |||
e09b93432c | |||
6e65ef1a4d | |||
550328ba1e | |||
070100cdbc | |||
5422cca393 | |||
4e3b765059 | |||
6ec61c1bcc | |||
bd025d6d71 | |||
45f3f79a49 | |||
a7137f0d51 | |||
a33659c8c5 | |||
2058220ead | |||
af671a4e63 | |||
d3dfdb4d82 | |||
a55db2553b | |||
1882b8ccdc |
37
package.json
37
package.json
@ -10,44 +10,45 @@
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.5",
|
||||
"@emotion/styled": "^11.10.5",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@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.4",
|
||||
"@mui/material": "^5.11.13",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"clsx": "^1.2.1",
|
||||
"debug": "^4.3.4",
|
||||
"notistack": "^2.0.8",
|
||||
"ramda": "^0.28.0",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"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/serialize": "^1.1.1",
|
||||
"@tauri-apps/cli": "^1.2.2",
|
||||
"@tauri-apps/cli": "^1.2.3",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/ramda": "^0.28.20",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@types/node": "^18.15.3",
|
||||
"@types/ramda": "^0.28.23",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@vitejs/plugin-react": "^2.2.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"eslint-config-prettier": "^8.6.0",
|
||||
"eslint-plugin-import": "^2.27.4",
|
||||
"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-simple-import-sort": "^8.0.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.3",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"prettier": "^2.8.4",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"twin.macro": "^3.1.0",
|
||||
"typescript": "^4.9.4",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^3.2.5"
|
||||
}
|
||||
}
|
||||
|
726
pnpm-lock.yaml
generated
726
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
1326
src-tauri/Cargo.lock
generated
1326
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -28,13 +28,16 @@ tokio = { version = "1.22.0", features = ["full"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
hex = "0.4.3"
|
||||
rumqttc = "0.17.0"
|
||||
time = { version = "0.3.17", features = ["formatting"] }
|
||||
color_space = "0.5.3"
|
||||
futures = "0.3.25"
|
||||
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"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
@ -1,6 +1,6 @@
|
||||
use futures::future::join_all;
|
||||
use once_cell::sync::OnceCell;
|
||||
use paris::{error, info};
|
||||
use paris::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::{
|
||||
@ -13,7 +13,6 @@ use tokio::{
|
||||
sync::mpsc,
|
||||
time::{sleep, Instant},
|
||||
};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
picker::{
|
||||
@ -76,7 +75,8 @@ impl CoreManager {
|
||||
colors.push(color);
|
||||
}
|
||||
hue = (hue + 1.0) % 360.0;
|
||||
match rpc::manager::Manager::global()
|
||||
match rpc::Manager::global()
|
||||
.await
|
||||
.publish_led_colors(&colors)
|
||||
.await
|
||||
{
|
||||
@ -93,6 +93,7 @@ impl CoreManager {
|
||||
}
|
||||
|
||||
pub async fn play_follow(&self) -> anyhow::Result<()> {
|
||||
macos_app_nap::prevent();
|
||||
let mut futs = vec![];
|
||||
let configs = picker::config::Manager::global().reload_config().await;
|
||||
let configs = match configs {
|
||||
@ -116,7 +117,8 @@ impl CoreManager {
|
||||
futs.push(fut);
|
||||
}
|
||||
|
||||
let total_colors_count = configs.iter()
|
||||
let total_colors_count = configs
|
||||
.iter()
|
||||
.flat_map(|c| {
|
||||
vec![
|
||||
c.led_strip_of_borders.top,
|
||||
@ -136,36 +138,62 @@ impl CoreManager {
|
||||
.collect::<HashSet<_>>()
|
||||
.len();
|
||||
tokio::spawn(async move {
|
||||
let mut global_colors = HashMap::new();
|
||||
let mut global_sub_pixels = HashMap::new();
|
||||
while let Some(screenshot) = rx.recv().await {
|
||||
let start_at = Instant::now();
|
||||
let colors = screenshot.get_top_colors();
|
||||
let start = screenshot
|
||||
.get_top_of_led_start_at()
|
||||
.min(screenshot.get_top_of_led_end_at());
|
||||
|
||||
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() {
|
||||
global_colors.insert(index + start, color);
|
||||
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_colors.len(),
|
||||
start_at.elapsed()
|
||||
);
|
||||
// info!(
|
||||
// "led count: {}, spend: {:?}",
|
||||
// global_sub_pixels.len(),
|
||||
// start_at.elapsed()
|
||||
// );
|
||||
|
||||
if global_colors.len() >= total_colors_count {
|
||||
if global_sub_pixels.len() >= total_colors_count * 3 {
|
||||
let mut colors = vec![];
|
||||
for index in 0..global_colors.len() {
|
||||
colors.push(*global_colors.get(&index).unwrap());
|
||||
for index in 0..global_sub_pixels.len() {
|
||||
colors.push(*global_sub_pixels.get(&index).unwrap_or(&100));
|
||||
}
|
||||
global_colors = HashMap::new();
|
||||
match rpc::manager::Manager::global()
|
||||
.publish_led_colors(&colors)
|
||||
// info!("{:?}", colors);
|
||||
global_sub_pixels = HashMap::new();
|
||||
match rpc::Manager::global()
|
||||
.await
|
||||
.publish_led_sub_pixels(colors)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!("publish successful",);
|
||||
// info!("publish successful",);
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("publish led colors failed. {}", error);
|
||||
@ -194,7 +222,7 @@ impl CoreManager {
|
||||
if let AmbientLightMode::Follow = *lock {
|
||||
drop(lock);
|
||||
let screenshot = picker.take_screenshot()?;
|
||||
info!("Take Screenshot Spend: {:?}", start.elapsed());
|
||||
// info!("Take Screenshot Spend: {:?}", start.elapsed());
|
||||
match tx.send(screenshot).await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
|
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::*;
|
||||
|
||||
|
||||
|
@ -5,24 +5,30 @@
|
||||
#![feature(bool_to_option)]
|
||||
|
||||
mod core;
|
||||
mod db;
|
||||
mod display;
|
||||
mod picker;
|
||||
mod rpc;
|
||||
mod models;
|
||||
|
||||
use crate::core::AmbientLightMode;
|
||||
use crate::core::CoreManager;
|
||||
use once_cell::sync::OnceCell;
|
||||
use paris::*;
|
||||
use picker::config::DisplayConfig;
|
||||
use picker::manager::Picker;
|
||||
use picker::screenshot::ScreenshotDto;
|
||||
use tauri::async_runtime::Mutex;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
static GET_SCREENSHOT_LOCK: OnceCell<Mutex<bool>> = OnceCell::new();
|
||||
|
||||
#[tauri::command]
|
||||
async fn take_snapshot() -> Vec<ScreenshotDto> {
|
||||
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!");
|
||||
let manager = Picker::global().await;
|
||||
|
||||
@ -89,7 +95,11 @@ async fn play_mode(target_mode: AmbientLightMode) {
|
||||
|
||||
#[tokio::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()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
take_snapshot,
|
||||
|
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>),
|
||||
}
|
@ -115,7 +115,6 @@ mod tests {
|
||||
async fn write_config_to_disk_should_be_successful() {
|
||||
let temp = TestDir::temp().create("config_dir", test_dir::FileType::Dir);
|
||||
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(
|
||||
config_file_path.clone(),
|
||||
&Configuration::default(),
|
||||
|
@ -9,10 +9,6 @@ pub struct DisplayPicker {
|
||||
}
|
||||
|
||||
impl DisplayPicker {
|
||||
pub fn new(screen: Screen, config: DisplayConfig) -> Self {
|
||||
Self { screen, config }
|
||||
}
|
||||
|
||||
pub fn from_config(config: DisplayConfig) -> anyhow::Result<Self> {
|
||||
let displays = Display::all()
|
||||
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||
|
@ -3,7 +3,6 @@ use std::{io::ErrorKind::WouldBlock, time::Duration, thread};
|
||||
|
||||
pub struct Screen {
|
||||
capturer: Option<Capturer>,
|
||||
init_error: Option<anyhow::Error>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
@ -12,16 +11,6 @@ impl Screen {
|
||||
pub fn new(capturer: Capturer, width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
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,
|
||||
height,
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ impl Screenshot {
|
||||
config.display_width,
|
||||
led_strip_config
|
||||
.global_start_position
|
||||
.abs_diff(led_strip_config.global_end_position),
|
||||
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||
5,
|
||||
),
|
||||
None => {
|
||||
@ -56,7 +56,7 @@ impl Screenshot {
|
||||
config.display_width,
|
||||
led_strip_config
|
||||
.global_start_position
|
||||
.abs_diff(led_strip_config.global_end_position),
|
||||
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||
5,
|
||||
);
|
||||
points
|
||||
@ -81,7 +81,7 @@ impl Screenshot {
|
||||
config.display_height,
|
||||
led_strip_config
|
||||
.global_start_position
|
||||
.abs_diff(led_strip_config.global_end_position),
|
||||
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||
5,
|
||||
);
|
||||
points
|
||||
@ -103,7 +103,7 @@ impl Screenshot {
|
||||
config.display_height,
|
||||
led_strip_config
|
||||
.global_start_position
|
||||
.abs_diff(led_strip_config.global_end_position),
|
||||
.abs_diff(led_strip_config.global_end_position) + 1,
|
||||
5,
|
||||
);
|
||||
points
|
||||
@ -212,23 +212,6 @@ impl Screenshot {
|
||||
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 {
|
||||
let bitmap = &self.bitmap;
|
||||
let stride = bitmap.len() / self.config.display_height;
|
||||
@ -280,6 +263,11 @@ impl Screenshot {
|
||||
colors,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn get_config(&self) -> DisplayConfig {
|
||||
self.config
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
|
@ -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 once_cell::sync::OnceCell;
|
||||
use crate::{display, models, picker::led_color::LedColor};
|
||||
|
||||
use super::mqtt::MqttRpc;
|
||||
|
||||
pub struct Manager {
|
||||
mqtt: MqttConnection,
|
||||
client: MqttRpc,
|
||||
initialized: bool,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn global() -> &'static Self {
|
||||
static RPC_MANAGER: OnceCell<Manager> = OnceCell::new();
|
||||
pub async fn global() -> &'static Self {
|
||||
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 {
|
||||
let mut mqtt = MqttConnection::new();
|
||||
mqtt.initialize();
|
||||
Self { mqtt }
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
let mqtt = MqttRpc::new().await?;
|
||||
let initialized = match mqtt.initialize().await {
|
||||
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<()> {
|
||||
@ -27,15 +46,18 @@ impl Manager {
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
self.mqtt
|
||||
.client
|
||||
.publish(
|
||||
"display-ambient-light/desktop/colors",
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error))
|
||||
self.publish_led_sub_pixels(payload).await
|
||||
}
|
||||
|
||||
pub async fn publish_led_sub_pixels(&self, payload: Vec<u8>) -> anyhow::Result<()> {
|
||||
self.client.publish_led_sub_pixels(payload).await
|
||||
}
|
||||
pub async fn publish_desktop_cmd(&self, field: &str, payload: Vec<u8>) -> anyhow::Result<()>
|
||||
{
|
||||
self.client.publish_desktop_cmd(field, payload).await
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &MqttRpc {
|
||||
&self.client
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,4 @@
|
||||
pub mod manager;
|
||||
mod manager;
|
||||
pub mod mqtt;
|
||||
|
||||
pub use manager::*;
|
@ -1,38 +1,187 @@
|
||||
use rumqttc::{AsyncClient, MqttOptions, QoS};
|
||||
use std::{time::Duration};
|
||||
use crate::{display, models};
|
||||
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 tokio::task;
|
||||
use tracing::warn;
|
||||
use tokio::{sync::broadcast, task, time::sleep};
|
||||
|
||||
pub struct MqttConnection {
|
||||
pub client: AsyncClient,
|
||||
const DISPLAY_TOPIC: &'static str = "display-ambient-light/display";
|
||||
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 {
|
||||
pub fn new() -> Self {
|
||||
let mut options = MqttOptions::new("rumqtt-async", "192.168.31.11", 1883);
|
||||
options.set_keep_alive(Duration::from_secs(5));
|
||||
impl MqttRpc {
|
||||
pub async fn new() -> anyhow::Result<Self> {
|
||||
let client = mqtt::AsyncClient::new("tcp://192.168.31.11:1883")
|
||||
.map_err(|err| anyhow::anyhow!("can not create MQTT client. {:?}", err))?;
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(options, 10);
|
||||
task::spawn(async move {
|
||||
while let Ok(notification) = eventloop.poll().await {
|
||||
// println!("Received = {:?}", notification);
|
||||
}
|
||||
client.set_connected_callback(|client| {
|
||||
info!("MQTT server connected.");
|
||||
|
||||
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) {
|
||||
self.subscribe_board();
|
||||
pub async fn listen(&self) {
|
||||
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();
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn subscribe_board(&self) {
|
||||
fn subscribe_board(&self) -> anyhow::Result<()> {
|
||||
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();
|
||||
task::spawn(async move {
|
||||
loop {
|
||||
@ -40,15 +189,12 @@ impl MqttConnection {
|
||||
.format(&format_description::well_known::Iso8601::DEFAULT)
|
||||
{
|
||||
Ok(now_str) => {
|
||||
match client
|
||||
.publish(
|
||||
let msg = mqtt::Message::new(
|
||||
"display-ambient-light/desktop/online",
|
||||
QoS::AtLeastOnce,
|
||||
false,
|
||||
now_str.as_bytes(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
mqtt::QOS_0,
|
||||
);
|
||||
match client.publish(msg).await {
|
||||
Ok(_) => {}
|
||||
Err(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))
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import './App.css';
|
||||
import { Configurator } from './configurator/configurator';
|
||||
import { ButtonSwitch } from './commons/components/button';
|
||||
import { fillParentCss } from './styles/fill-parent';
|
||||
|
||||
type Mode = 'Flowing' | 'Follow' | null;
|
||||
|
||||
@ -40,7 +41,7 @@ function App() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div css={[fillParentCss]} tw="box-border flex flex-col">
|
||||
<div tw="flex justify-between">
|
||||
{ledStripColors.map((it) => (
|
||||
<span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
||||
@ -64,7 +65,7 @@ function App() {
|
||||
<ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch>
|
||||
</div>
|
||||
|
||||
<div tw="flex gap-5 justify-center">
|
||||
<div css={[fillParentCss]}>
|
||||
<Configurator />
|
||||
</div>
|
||||
</div>
|
||||
|
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>
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
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 tw, { css, styled } from 'twin.macro';
|
||||
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 { PixelRgb } from '../models/pixel-rgb';
|
||||
import { ScreenshotDto } from '../models/screenshot.dto';
|
||||
import { CompletedContainer } from './completed-container';
|
||||
import { DraggableStrip } from './draggable-strip';
|
||||
import { StyledPixel } from './styled-pixel';
|
||||
|
||||
export const logger = debug('app:completed-led-strip');
|
||||
|
||||
@ -18,7 +18,7 @@ interface CompletedLedStripProps {
|
||||
onDisplayConfigChange?: (value: DisplayConfig) => void;
|
||||
}
|
||||
|
||||
type BorderLedStrip = {
|
||||
export type BorderLedStrip = {
|
||||
pixels: PixelRgb[];
|
||||
config: LedStripConfig | null;
|
||||
};
|
||||
@ -26,20 +26,13 @@ type BorderLedStrip = {
|
||||
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}, 1fr);
|
||||
grid-template-rows: auto repeat(${rows}, 1fr);
|
||||
grid-template-columns: repeat(${columns}, 0.5em);
|
||||
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> = ({
|
||||
screenshots,
|
||||
onDisplayConfigChange,
|
||||
@ -56,6 +49,18 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
() => 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
|
||||
@ -66,26 +71,6 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
|
||||
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(() => {
|
||||
return borderLedStrips.map(({ config, pixels }, index) =>
|
||||
config ? (
|
||||
@ -123,11 +108,12 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
setOverrideBorderLedStrips(undefined);
|
||||
}, [borderLedStrips]);
|
||||
return (
|
||||
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}>
|
||||
<StyledCompletedContainer>{completedPixels}</StyledCompletedContainer>
|
||||
<StyledContainer rows={screenshots.length * borders.length} columns={maxIndex + 1}>
|
||||
<CompletedContainer
|
||||
borderLedStrips={borderLedStrips}
|
||||
overrideBorderLedStrips={overrideBorderLedStrips}
|
||||
/>
|
||||
{strips}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
@ -70,7 +70,7 @@ export const DraggableStrip: FC<DraggableStripProp> = ({
|
||||
<span
|
||||
ref={ref}
|
||||
key={i}
|
||||
tw="opacity-30 bg-red-500 h-full w-full border border-yellow-400"
|
||||
tw=" h-full w-full"
|
||||
css={css`
|
||||
grid-column-start: ${i + 1};
|
||||
grid-row-start: ${index + 1};
|
||||
@ -183,8 +183,11 @@ export const DraggableStrip: FC<DraggableStripProp> = ({
|
||||
<div
|
||||
tw="border border-gray-700 h-3 w-full rounded-full"
|
||||
css={css`
|
||||
grid-column-start: ${(config?.global_start_position ?? 0) + 1};
|
||||
grid-column-end: ${(config?.global_end_position ?? 0) + 1};
|
||||
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);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { HTMLAttributes, useCallback } from 'react';
|
||||
import { HTMLAttributes, MouseEventHandler, useCallback } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { LedStripConfig } from '../models/led-strip-config';
|
||||
import tw, { styled } from 'twin.macro';
|
||||
@ -17,8 +17,8 @@ const StyledContainer = styled.section(
|
||||
|
||||
const StyledButton = styled.button(
|
||||
tw`
|
||||
bg-yellow-500 rounded-full h-4 w-4 text-xs shadow select-none`,
|
||||
tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600`,
|
||||
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> = ({
|
||||
@ -26,36 +26,54 @@ export const LedStripEditor: FC<LedStripEditorProps> = ({
|
||||
onChange,
|
||||
...htmlAttrs
|
||||
}) => {
|
||||
const addLed = useCallback(() => {
|
||||
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 + 1 });
|
||||
onChange?.({
|
||||
...config,
|
||||
global_end_position: config.global_end_position + delta,
|
||||
});
|
||||
} else {
|
||||
onChange?.({
|
||||
...config,
|
||||
global_start_position: config.global_start_position + 1,
|
||||
global_start_position: config.global_start_position + delta,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onChange?.(new LedStripConfig(0, 0, 1));
|
||||
onChange?.(new LedStripConfig(0, 0, 0));
|
||||
}
|
||||
}, [config, onChange]);
|
||||
const removeLed = useCallback(() => {
|
||||
},
|
||||
[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) <= 1) {
|
||||
} 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 - 1 });
|
||||
onChange?.({
|
||||
...config,
|
||||
global_end_position: config.global_end_position - delta,
|
||||
});
|
||||
} else {
|
||||
onChange?.({
|
||||
...config,
|
||||
global_start_position: config.global_start_position - 1,
|
||||
global_start_position: config.global_start_position - delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [config, onChange]);
|
||||
},
|
||||
[config, onChange],
|
||||
);
|
||||
const reverse = useCallback(() => {
|
||||
if (!config) {
|
||||
return;
|
||||
@ -69,10 +87,10 @@ export const LedStripEditor: FC<LedStripEditorProps> = ({
|
||||
|
||||
return (
|
||||
<StyledContainer {...htmlAttrs}>
|
||||
<StyledButton title="Add LED" onClick={addLed}>
|
||||
<StyledButton title="Add LED" onClick={addLed} onContextMenu={addLed}>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</StyledButton>
|
||||
<StyledButton title="Remove LED" onClick={removeLed}>
|
||||
<StyledButton title="Remove LED" onClick={removeLed} onContextMenu={removeLed}>
|
||||
<FontAwesomeIcon icon={faMinus} />
|
||||
</StyledButton>
|
||||
<StyledButton title="Reverse" onClick={reverse}>
|
||||
|
@ -1,18 +1,20 @@
|
||||
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 { 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, Snackbar } from '@mui/material';
|
||||
import { Alert, Fab, Snackbar } from '@mui/material';
|
||||
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 { 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');
|
||||
|
||||
@ -31,11 +33,15 @@ const writePickerConfig = async (config: PickerConfiguration) => {
|
||||
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 ConfiguratorInner: FC = () => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
||||
getPickerConfig,
|
||||
[],
|
||||
@ -75,7 +81,16 @@ const ConfiguratorInner: FC = () => {
|
||||
})().then();
|
||||
}
|
||||
}, [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(() => {
|
||||
if (screenshotOfDisplays) {
|
||||
@ -104,7 +119,15 @@ const ConfiguratorInner: FC = () => {
|
||||
screenshots={screenshotOfDisplays}
|
||||
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}>
|
||||
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
||||
This is a success message!
|
||||
|
@ -1,11 +1,14 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import GlobalStyles from './styles/global-styles';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<GlobalStyles />
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</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`;
|
@ -1,13 +1,28 @@
|
||||
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 = () => (
|
||||
|
Reference in New Issue
Block a user