Merge pull request 'feature/gui-configuration:支持从 GUI 配置程序。' (#4) from feature/gui-configuration into master
Reviewed-on: #4
This commit is contained in:
		
							
								
								
									
										5
									
								
								babel-plugin-macros.config.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								babel-plugin-macros.config.cjs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| module.exports = { | ||||
|   twin: { | ||||
|     preset: 'emotion', | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										43
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								package.json
									
									
									
									
									
								
							| @@ -10,27 +10,44 @@ | ||||
|     "tauri": "tauri" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "@tauri-apps/api": "^1.1.0", | ||||
|     "@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", | ||||
|     "@fortawesome/react-fontawesome": "^0.2.0", | ||||
|     "@mui/material": "^5.11.4", | ||||
|     "@tauri-apps/api": "^1.2.0", | ||||
|     "clsx": "^1.2.1", | ||||
|     "debug": "^4.3.4", | ||||
|     "ramda": "^0.28.0", | ||||
|     "react": "^18.2.0", | ||||
|     "react-async-hook": "^4.0.0", | ||||
|     "react-dom": "^18.2.0" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@tauri-apps/cli": "^1.1.0", | ||||
|     "@types/node": "^18.7.10", | ||||
|     "@types/react": "^18.0.15", | ||||
|     "@types/react-dom": "^18.0.6", | ||||
|     "@vitejs/plugin-react": "^2.0.0", | ||||
|     "@babel/plugin-transform-react-jsx": "^7.20.7", | ||||
|     "@emotion/babel-plugin-jsx-pragmatic": "^0.2.0", | ||||
|     "@emotion/serialize": "^1.1.1", | ||||
|     "@tauri-apps/cli": "^1.2.2", | ||||
|     "@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", | ||||
|     "@vitejs/plugin-react": "^2.2.0", | ||||
|     "autoprefixer": "^10.4.13", | ||||
|     "eslint-config-prettier": "^8.5.0", | ||||
|     "eslint-plugin-import": "^2.26.0", | ||||
|     "eslint-plugin-jsx-a11y": "^6.6.1", | ||||
|     "babel-plugin-macros": "^3.1.0", | ||||
|     "eslint-config-prettier": "^8.6.0", | ||||
|     "eslint-plugin-import": "^2.27.4", | ||||
|     "eslint-plugin-jsx-a11y": "^6.7.1", | ||||
|     "eslint-plugin-prettier": "^4.2.1", | ||||
|     "eslint-plugin-simple-import-sort": "^8.0.0", | ||||
|     "postcss": "^8.4.19", | ||||
|     "prettier": "^2.7.1", | ||||
|     "postcss": "^8.4.21", | ||||
|     "prettier": "^2.8.3", | ||||
|     "tailwindcss": "^3.2.4", | ||||
|     "typescript": "^4.6.4", | ||||
|     "vite": "^3.0.2" | ||||
|     "twin.macro": "^3.1.0", | ||||
|     "typescript": "^4.9.4", | ||||
|     "vite": "^3.2.5" | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										1461
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1461
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,6 +0,0 @@ | ||||
| module.exports = { | ||||
|   plugins: { | ||||
|     tailwindcss: {}, | ||||
|     autoprefixer: {}, | ||||
|   }, | ||||
| } | ||||
							
								
								
									
										887
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										887
									
								
								src-tauri/Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -33,6 +33,8 @@ 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" | ||||
|  | ||||
| [features] | ||||
| # by default Tauri runs in production mode | ||||
| @@ -41,3 +43,6 @@ default = [ "custom-protocol" ] | ||||
| # this feature is used used for production builds where `devPath` points to the filesystem | ||||
| # DO NOT remove this | ||||
| custom-protocol = [ "tauri/custom-protocol" ] | ||||
|  | ||||
| [dev-dependencies] | ||||
| test_dir = "0.2.0" | ||||
|   | ||||
| @@ -1,27 +1,28 @@ | ||||
| use futures::{future::join_all, stream::FuturesUnordered, StreamExt}; | ||||
| use futures::future::join_all; | ||||
| use once_cell::sync::OnceCell; | ||||
| use paris::info; | ||||
| use paris::{error, info, warn}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use serde_json::value::Index; | ||||
| use std::{collections::HashMap, iter::Map, sync::Arc, thread, time::Duration}; | ||||
|  | ||||
| use std::{ | ||||
|     collections::{HashMap, HashSet}, | ||||
|     sync::Arc, | ||||
|     time::Duration, | ||||
| }; | ||||
| use tauri::async_runtime::RwLock; | ||||
| use tokio::{ | ||||
|     join, | ||||
|     sync::mpsc, | ||||
|     task, | ||||
|     time::{sleep, Instant}, | ||||
| }; | ||||
| use tracing::warn; | ||||
|  | ||||
| use crate::{ | ||||
|     picker::{ | ||||
|         config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor, manager::Picker, | ||||
|         self, config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor, | ||||
|         screenshot::Screenshot, | ||||
|     }, | ||||
|     rpc, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Serialize, Deserialize)] | ||||
| #[derive(Debug, Serialize, Deserialize, Clone, Copy)] | ||||
| pub enum AmbientLightMode { | ||||
|     None, | ||||
|     Follow, | ||||
| @@ -40,25 +41,25 @@ impl CoreManager { | ||||
|             ambient_light_mode: Arc::new(RwLock::new(AmbientLightMode::None)), | ||||
|         }); | ||||
|  | ||||
|         tokio::spawn(async { | ||||
|             loop { | ||||
|                 core.play_flowing_light().await; | ||||
|                 match core.play_follow().await { | ||||
|                     Ok(_) => {} | ||||
|                     Err(error) => { | ||||
|                         warn!("Can not following displays. {}", error); | ||||
|                         sleep(Duration::from_millis(1000)).await; | ||||
|                     } | ||||
|                 }; | ||||
|                 sleep(Duration::from_millis(10)).await; | ||||
|             } | ||||
|         }); | ||||
|         core | ||||
|     } | ||||
|  | ||||
|     pub async fn set_ambient_light(&self, target_mode: AmbientLightMode) { | ||||
|         let mut mode = self.ambient_light_mode.write().await; | ||||
|         *mode = target_mode; | ||||
|  | ||||
|         drop(mode); | ||||
|  | ||||
|         match target_mode { | ||||
|             AmbientLightMode::Flowing => self.play_flowing_light().await, | ||||
|             AmbientLightMode::None => {} | ||||
|             AmbientLightMode::Follow => match self.play_follow().await { | ||||
|                 Ok(_) => {} | ||||
|                 Err(error) => { | ||||
|                     warn!("Can not following displays. {}", error); | ||||
|                 } | ||||
|             }, | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub async fn play_flowing_light(&self) { | ||||
| @@ -91,74 +92,117 @@ impl CoreManager { | ||||
|     } | ||||
|  | ||||
|     pub async fn play_follow(&self) -> anyhow::Result<()> { | ||||
|         let lock = self.ambient_light_mode.read().await; | ||||
|         let mut futs = vec![]; | ||||
|         if let AmbientLightMode::Follow = *lock { | ||||
|             drop(lock); | ||||
|             let configs = Picker::global().display_configs.lock().await; | ||||
|         let configs = picker::config::Manager::global().reload_config().await; | ||||
|         let configs = match configs { | ||||
|             Ok(c) => c.display_configs, | ||||
|             Err(err) => anyhow::bail!("can not get display configs. {:?}", err), | ||||
|         }; | ||||
|         info!("piker display configs: {:?}", configs); | ||||
|  | ||||
|             let (tx, mut rx) = mpsc::channel(10); | ||||
|         let (tx, mut rx) = mpsc::channel(10); | ||||
|  | ||||
|             for config in configs.to_owned() { | ||||
|                 let tx = tx.clone(); | ||||
|                 let fut = tokio::spawn(async move { | ||||
|                     match Self::follow_display_by_config(config, tx).await { | ||||
|                         Ok(_) => {} | ||||
|                         Err(error) => { | ||||
|                             warn!("following failed. {}", error); | ||||
|                         } | ||||
|         for config in configs.clone() { | ||||
|             let tx = tx.clone(); | ||||
|             let fut = tokio::spawn(async move { | ||||
|                 match Self::follow_display_by_config(config, tx).await { | ||||
|                     Ok(_) => {} | ||||
|                     Err(error) => { | ||||
|                         warn!("following failed. {}", error); | ||||
|                     } | ||||
|                 }); | ||||
|                 futs.push(fut); | ||||
|             } | ||||
|  | ||||
|             let configs = configs.clone(); | ||||
|  | ||||
|             tokio::spawn(async move { | ||||
|                 let mut global_colors = HashMap::new(); | ||||
|                 while let Some(screenshot) = rx.recv().await { | ||||
|                     let start_at = Instant::now(); | ||||
|                     match screenshot.get_top_colors().await { | ||||
|                         Ok(colors) => { | ||||
|                             let start = screenshot.get_top_of_led_start_at().min(screenshot.get_top_of_led_end_at()); | ||||
|  | ||||
|                             let colors_len = colors.len(); | ||||
|                             for (index, color) in colors.into_iter().enumerate() { | ||||
|                                 global_colors.insert(index + start, color); | ||||
|                             } | ||||
|  | ||||
|                             info!("led count: {}, spend: {:?}", global_colors.len(), start_at.elapsed()); | ||||
|  | ||||
|                             if global_colors.len() == 60 { | ||||
|                                 let mut colors = vec![]; | ||||
|                                 for index in 0..global_colors.len() { | ||||
|                                     colors.push(*global_colors.get(&index).unwrap()); | ||||
|                                 } | ||||
|                                 global_colors = HashMap::new(); | ||||
|                                 match rpc::manager::Manager::global() | ||||
|                                     .publish_led_colors(&colors) | ||||
|                                     .await | ||||
|                                 { | ||||
|                                     Ok(_) => { | ||||
|                                         info!("publish successful",); | ||||
|                                     } | ||||
|                                     Err(error) => { | ||||
|                                         warn!("publish led colors failed. {}", error); | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         Err(_) => {} | ||||
|                     }; | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             join_all(futs).await; | ||||
|         } else { | ||||
|             drop(lock); | ||||
|             return Ok(()); | ||||
|             futs.push(fut); | ||||
|         } | ||||
|  | ||||
|         let total_colors_count = configs | ||||
|             .iter() | ||||
|             .flat_map(|c| { | ||||
|                 vec![ | ||||
|                     c.led_strip_of_borders.top, | ||||
|                     c.led_strip_of_borders.bottom, | ||||
|                     c.led_strip_of_borders.left, | ||||
|                     c.led_strip_of_borders.right, | ||||
|                 ] | ||||
|             }) | ||||
|             .flat_map(|l| match l { | ||||
|                 Some(l) => (l.global_start_position.min(l.global_end_position) | ||||
|                     ..l.global_start_position.max(l.global_end_position)) | ||||
|                     .collect(), | ||||
|                 None => { | ||||
|                     vec![] | ||||
|                 } | ||||
|             }) | ||||
|             .collect::<HashSet<_>>() | ||||
|             .len(); | ||||
|         tokio::spawn(async move { | ||||
|             let mut global_sub_pixels = HashMap::new(); | ||||
|             while let Some(screenshot) = rx.recv().await { | ||||
|                 let start_at = Instant::now(); | ||||
|                 let colors = screenshot.get_colors(); | ||||
|                 let config = screenshot.get_config(); | ||||
|                 for (colors, config) in vec![ | ||||
|                     (colors.top, config.led_strip_of_borders.top), | ||||
|                     (colors.right, config.led_strip_of_borders.right), | ||||
|                     (colors.bottom, config.led_strip_of_borders.bottom), | ||||
|                     (colors.left, config.led_strip_of_borders.left), | ||||
|                 ] { | ||||
|                     match config { | ||||
|                         Some(config) => { | ||||
|                             let (sign, start) = | ||||
|                                 if config.global_start_position <= config.global_end_position { | ||||
|                                     (1, config.global_start_position as isize * 3) | ||||
|                                 } else { | ||||
|                                     (-1, (config.global_start_position as isize + 1) * 3 - 1) | ||||
|                                 }; | ||||
|                             for (index, color) in colors.into_iter().enumerate() { | ||||
|                                 let pixel_index = index / 3; | ||||
|                                 let sub_pixel_index = index % 3; | ||||
|                                 let offset = if sign < 0 { | ||||
|                                     2 - sub_pixel_index | ||||
|                                 } else { | ||||
|                                     sub_pixel_index | ||||
|                                 }; | ||||
|                                 let global_sub_pixel_index = | ||||
|                                     (sign * (pixel_index as isize * 3 + offset as isize) + start) | ||||
|                                         as usize; | ||||
|                                 global_sub_pixels.insert(global_sub_pixel_index, color); | ||||
|                             } | ||||
|                         } | ||||
|                         None => {} | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
|                 // info!( | ||||
|                 //     "led count: {}, spend: {:?}", | ||||
|                 //     global_sub_pixels.len(), | ||||
|                 //     start_at.elapsed() | ||||
|                 // ); | ||||
|  | ||||
|                 if global_sub_pixels.len() >= total_colors_count * 3 { | ||||
|                     let mut colors = vec![]; | ||||
|                     for index in 0..global_sub_pixels.len() { | ||||
|                         colors.push(*global_sub_pixels.get(&index).unwrap()); | ||||
|                     } | ||||
|                     // info!("{:?}", colors); | ||||
|                     global_sub_pixels = HashMap::new(); | ||||
|                     match rpc::manager::Manager::global() | ||||
|                         .publish_led_sub_pixels(colors) | ||||
|                         .await | ||||
|                     { | ||||
|                         Ok(_) => { | ||||
|                             // info!("publish successful",); | ||||
|                         } | ||||
|                         Err(error) => { | ||||
|                             warn!("publish led colors failed. {}", error); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         join_all(futs).await; | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
| @@ -176,18 +220,18 @@ impl CoreManager { | ||||
|             if let AmbientLightMode::Follow = *lock { | ||||
|                 drop(lock); | ||||
|                 let screenshot = picker.take_screenshot()?; | ||||
|                 info!("Take Screenshot Spend: {:?}", start.elapsed()); | ||||
|                 tx.send(screenshot).await; | ||||
|                 // info!("Take Screenshot Spend: {:?}", start.elapsed()); | ||||
|                 match tx.send(screenshot).await { | ||||
|                     Ok(_) => {} | ||||
|                     Err(err) => { | ||||
|                         error!("send screenshot to main thread was failed. {:?}", err); | ||||
|                     } | ||||
|                 }; | ||||
|             } else { | ||||
|                 break; | ||||
|             } | ||||
|             tokio::time::sleep_until(next_tick).await; | ||||
|         } | ||||
|  | ||||
|         // // Picker::global().take_screenshots_for_all().await?; | ||||
|         // // let colors = Picker::global().get_led_strip_colors().await?; | ||||
|  | ||||
|         // // let colors = colors.into_iter().rev().collect(); | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|     all(not(debug_assertions), target_os = "windows"), | ||||
|     windows_subsystem = "windows" | ||||
| )] | ||||
| #![feature(bool_to_option)] | ||||
|  | ||||
| mod core; | ||||
| mod picker; | ||||
| @@ -10,23 +11,20 @@ mod rpc; | ||||
| use crate::core::AmbientLightMode; | ||||
| use crate::core::CoreManager; | ||||
| use paris::*; | ||||
| use picker::led_color::LedColor; | ||||
| use picker::config::DisplayConfig; | ||||
| use picker::manager::Picker; | ||||
| use std::vec; | ||||
| 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 refresh_displays() { | ||||
|     match Picker::global().refresh_displays().await { | ||||
|         Ok(_) => {} | ||||
|         Err(error) => { | ||||
|             error!("{}", error) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| async fn take_snapshot() -> Vec<String> { | ||||
|     let manager = Picker::global(); | ||||
| async fn take_snapshot() -> Vec<ScreenshotDto> { | ||||
|     info!("Hi?"); | ||||
|     let _lock = GET_SCREENSHOT_LOCK.get_or_init(|| Mutex::new(false)).lock().await; | ||||
|     info!("Hi!"); | ||||
|     let manager = Picker::global().await; | ||||
|  | ||||
|     let start = time::Instant::now(); | ||||
|     let base64_bitmap_list = match manager.list_displays().await { | ||||
| @@ -40,21 +38,45 @@ async fn take_snapshot() -> Vec<String> { | ||||
|             vec![] | ||||
|         } | ||||
|     }; | ||||
|     info!("截图花费 {} s", start.elapsed().as_seconds_f32()); | ||||
|     info!("截图耗时 {} s", start.elapsed().as_seconds_f32()); | ||||
|     base64_bitmap_list | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| async fn get_led_strip_colors() -> Result<Vec<LedColor>, String> { | ||||
|     let colors = Picker::global().get_led_strip_colors().await; | ||||
|     match colors { | ||||
|         Ok(colors) => { | ||||
|             rpc::manager::Manager::global() | ||||
|                 .publish_led_colors(&colors.to_vec()) | ||||
|                 .await; | ||||
|             Ok(colors) | ||||
| async fn get_screenshot_by_config(config: DisplayConfig) -> Result<ScreenshotDto, String> { | ||||
|     info!("Hi?"); | ||||
|     // let _lock = GET_SCREENSHOT_LOCK.get_or_init(|| Mutex::new(false)).lock().await; | ||||
|     // info!("Hi!"); | ||||
|     let start = time::Instant::now(); | ||||
|     let screenshot_dto = Picker::preview_display_by_config(config).await; | ||||
|     info!("截图耗时 {} s", start.elapsed().as_seconds_f32()); | ||||
|     match screenshot_dto { | ||||
|         Ok(screenshot_dto) => Ok(screenshot_dto), | ||||
|         Err(error) => { | ||||
|             error!("preview_display_by_config failed. {}", error); | ||||
|             Err(format!("preview_display_by_config failed. {}", error)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[tauri::command] | ||||
| async fn get_picker_config() -> picker::config::Configuration { | ||||
|     let configuration = picker::config::Manager::global().get_config().await; | ||||
|     info!("configuration: {:?}", configuration); | ||||
|     configuration | ||||
| } | ||||
| #[tauri::command] | ||||
| async fn write_picker_config(config: picker::config::Configuration) -> Result<(), String> { | ||||
|     let manager = picker::config::Manager::global(); | ||||
|     let path = picker::config::Manager::get_config_file_path(); | ||||
|     info!("log save in {:?}", path.to_str()); | ||||
|     manager.set_config(&config).await; | ||||
|     match picker::config::Manager::write_config_to_disk(path, &config) { | ||||
|         Ok(_) => Ok(()), | ||||
|         Err(err) => { | ||||
|             error!("can not write picker config. {:?}", err); | ||||
|             Err(format!("can not write picker config. {:?}", err)) | ||||
|         } | ||||
|         Err(error) => Err(format!("{}", error)), | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -62,7 +84,7 @@ async fn get_led_strip_colors() -> Result<Vec<LedColor>, String> { | ||||
| async fn play_mode(target_mode: AmbientLightMode) { | ||||
|     info!("target mode: {:?}", target_mode); | ||||
|  | ||||
|     CoreManager::global().set_ambient_light(target_mode).await; | ||||
|     tokio::spawn(async move { CoreManager::global().set_ambient_light(target_mode).await }); | ||||
| } | ||||
|  | ||||
| #[tokio::main] | ||||
| @@ -71,9 +93,10 @@ async fn main() { | ||||
|     tauri::Builder::default() | ||||
|         .invoke_handler(tauri::generate_handler![ | ||||
|             take_snapshot, | ||||
|             refresh_displays, | ||||
|             get_led_strip_colors, | ||||
|             play_mode, | ||||
|             get_picker_config, | ||||
|             get_screenshot_by_config, | ||||
|             write_picker_config, | ||||
|         ]) | ||||
|         .run(tauri::generate_context!()) | ||||
|         .expect("error while running tauri application"); | ||||
|   | ||||
| @@ -1,58 +0,0 @@ | ||||
| #[derive(Clone, Copy)] | ||||
| pub struct LedStripConfig { | ||||
|     pub index: usize, | ||||
|     pub global_start_position: usize, | ||||
|     pub global_end_position: usize, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Copy)] | ||||
| pub struct DisplayConfig { | ||||
|     pub index_of_display: usize, | ||||
|     pub display_width: usize, | ||||
|     pub display_height: usize, | ||||
|     pub top_led_strip: LedStripConfig, | ||||
|     pub bottom_led_strip: LedStripConfig, | ||||
|     pub left_led_strip: LedStripConfig, | ||||
|     pub right_led_strip: LedStripConfig, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Copy)] | ||||
| pub enum LedFlowX { | ||||
|     LR, // from left to right | ||||
|     RL, // from right to left | ||||
| } | ||||
| #[derive(Clone, Copy)] | ||||
| pub enum LedFlowY { | ||||
|     TB, // from top to bottom | ||||
|     BT, // from bottom to top | ||||
| } | ||||
|  | ||||
| impl DisplayConfig { | ||||
|     pub fn default(index_of_display: usize, display_width: usize, display_height: usize) -> Self { | ||||
|         Self { | ||||
|             index_of_display, | ||||
|             display_width, | ||||
|             display_height, | ||||
|             top_led_strip: LedStripConfig { | ||||
|                 index: 0, | ||||
|                 global_start_position: 0, | ||||
|                 global_end_position: 0, | ||||
|             }, | ||||
|             bottom_led_strip: LedStripConfig { | ||||
|                 index: 0, | ||||
|                 global_start_position: 0, | ||||
|                 global_end_position: 0, | ||||
|             }, | ||||
|             left_led_strip: LedStripConfig { | ||||
|                 index: 0, | ||||
|                 global_start_position: 0, | ||||
|                 global_end_position: 0, | ||||
|             }, | ||||
|             right_led_strip: LedStripConfig { | ||||
|                 index: 0, | ||||
|                 global_start_position: 0, | ||||
|                 global_end_position: 0, | ||||
|             }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										53
									
								
								src-tauri/src/picker/config/display_config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src-tauri/src/picker/config/display_config.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Clone, Copy, Serialize, Deserialize, Debug)] | ||||
| pub struct LedStripConfigOfBorders { | ||||
|     pub top: Option<LedStripConfig>, | ||||
|     pub bottom: Option<LedStripConfig>, | ||||
|     pub left: Option<LedStripConfig>, | ||||
|     pub right: Option<LedStripConfig>, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Copy, Serialize, Deserialize, Debug)] | ||||
| pub struct LedStripConfig { | ||||
|     pub index: usize, | ||||
|     pub global_start_position: usize, | ||||
|     pub global_end_position: usize, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Copy, Serialize, Deserialize, Debug)] | ||||
| pub struct DisplayConfig { | ||||
|     pub id: usize, | ||||
|     pub index_of_display: usize, | ||||
|     pub display_width: usize, | ||||
|     pub display_height: usize, | ||||
|     pub led_strip_of_borders: LedStripConfigOfBorders, | ||||
| } | ||||
|  | ||||
| impl LedStripConfigOfBorders { | ||||
|     pub fn default() -> Self { | ||||
|         Self { | ||||
|             top: None, | ||||
|             bottom: None, | ||||
|             left: None, | ||||
|             right: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl DisplayConfig { | ||||
|     pub fn default( | ||||
|         id: usize, | ||||
|         index_of_display: usize, | ||||
|         display_width: usize, | ||||
|         display_height: usize, | ||||
|     ) -> Self { | ||||
|         Self { | ||||
|             id, | ||||
|             index_of_display, | ||||
|             display_width, | ||||
|             display_height, | ||||
|             led_strip_of_borders: LedStripConfigOfBorders::default(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										147
									
								
								src-tauri/src/picker/config/manger.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src-tauri/src/picker/config/manger.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| use std::{ | ||||
|     env::current_dir, | ||||
|     fs::{self, File}, | ||||
|     io::Read, | ||||
|     path::PathBuf, | ||||
|     sync::Arc, | ||||
| }; | ||||
|  | ||||
| use once_cell::sync::OnceCell; | ||||
| use paris::info; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use tauri::{api::path::config_dir, async_runtime::Mutex}; | ||||
|  | ||||
| use super::DisplayConfig; | ||||
|  | ||||
| #[derive(Serialize, Deserialize, Clone, Debug)] | ||||
| pub struct Configuration { | ||||
|     pub config_version: u8, | ||||
|     pub display_configs: Vec<DisplayConfig>, | ||||
| } | ||||
|  | ||||
| impl Configuration { | ||||
|     pub fn default() -> Self { | ||||
|         Self { | ||||
|             config_version: 1, | ||||
|             display_configs: vec![], | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub struct Manager { | ||||
|     config: Arc<Mutex<Configuration>>, | ||||
| } | ||||
|  | ||||
| impl Manager { | ||||
|     pub fn global() -> &'static Manager { | ||||
|         static DISPLAY_CONFIG_MANAGE: OnceCell<Manager> = OnceCell::new(); | ||||
|  | ||||
|         DISPLAY_CONFIG_MANAGE.get_or_init(|| Self::init_from_disk()) | ||||
|     } | ||||
|  | ||||
|     pub fn default() -> Self { | ||||
|         Self::new(Configuration::default()) | ||||
|     } | ||||
|  | ||||
|     pub fn new(config: Configuration) -> Self { | ||||
|         Self { | ||||
|             config: Arc::new(Mutex::new(config)), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_config_file_path() -> PathBuf { | ||||
|         config_dir() | ||||
|             .unwrap_or(current_dir().unwrap()) | ||||
|             .join("display_config.json") | ||||
|     } | ||||
|  | ||||
|     pub fn init_from_disk() -> Self { | ||||
|         let config_file_path = Self::get_config_file_path(); | ||||
|         match Self::read_config_from_disk(config_file_path) { | ||||
|             Ok(config) => Self::new(config), | ||||
|             Err(_) => Self::default(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn read_config_from_disk(config_file_path: PathBuf) -> anyhow::Result<Configuration> { | ||||
|         let mut file = File::open(config_file_path) | ||||
|             .map_err(|error| anyhow::anyhow!("config file is not existed. {}", error))?; | ||||
|         let mut contents = String::new(); | ||||
|         file.read_to_string(&mut contents) | ||||
|             .map_err(|error| anyhow::anyhow!("can not read config file. {}", error))?; | ||||
|         serde_json::from_str(&contents) | ||||
|             .map_err(|error| anyhow::anyhow!("can not parse config file contents. {}", error)) | ||||
|     } | ||||
|  | ||||
|     pub fn write_config_to_disk( | ||||
|         config_file_path: PathBuf, | ||||
|         config: &Configuration, | ||||
|     ) -> anyhow::Result<()> { | ||||
|         let contents = serde_json::to_string(config) | ||||
|             .map_err(|error| anyhow::anyhow!("can not serialize config. {}", error))?; | ||||
|         info!("contents: {}", contents); | ||||
|         fs::write(config_file_path, contents.as_bytes()) | ||||
|             .map_err(|error| anyhow::anyhow!("can not write config file. {}", error))?; | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub async fn get_config(&self) -> Configuration { | ||||
|         self.config.lock().await.clone() | ||||
|     } | ||||
|     pub async fn set_config(&self, new_config: &Configuration) { | ||||
|         let mut config = self.config.lock().await; | ||||
|         *config = new_config.clone(); | ||||
|     } | ||||
|     pub async fn reload_config(&self) -> anyhow::Result<Configuration> { | ||||
|         let mut config = self.config.lock().await; | ||||
|         let new_config = Self::read_config_from_disk(Self::get_config_file_path()) | ||||
|             .map_err(|err| anyhow::anyhow!("can not reload config. {:?}", err))?; | ||||
|         *config = new_config.clone(); | ||||
|         return anyhow::Ok(new_config); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod tests { | ||||
|  | ||||
|     use std::fs; | ||||
|  | ||||
|     use serde_json::json; | ||||
|     use test_dir::{DirBuilder, TestDir}; | ||||
|  | ||||
|     use crate::picker::config::Configuration; | ||||
|  | ||||
|     #[tokio::test] | ||||
|     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(), | ||||
|         ) | ||||
|         .unwrap(); | ||||
|  | ||||
|         let contents = fs::read_to_string(config_file_path.clone()).unwrap(); | ||||
|         let _config: Configuration = serde_json::from_str(contents.as_str()).unwrap(); | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn read_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"); | ||||
|         fs::write( | ||||
|             config_file_path.clone(), | ||||
|             json!({ | ||||
|                 "config_version": 1, | ||||
|                 "display_configs": [] | ||||
|             }) | ||||
|             .to_string() | ||||
|             .as_bytes(), | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         let _manager = | ||||
|             crate::picker::config::manger::Manager::read_config_from_disk(config_file_path.clone()) | ||||
|                 .unwrap(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										5
									
								
								src-tauri/src/picker/config/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src-tauri/src/picker/config/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| mod display_config; | ||||
| mod manger; | ||||
|  | ||||
| pub use display_config::*; | ||||
| pub use manger::*; | ||||
| @@ -1,7 +1,7 @@ | ||||
| use color_space::{Hsv, Rgb}; | ||||
| use serde::Serialize; | ||||
|  | ||||
| #[derive(Clone, Copy)] | ||||
| #[derive(Clone, Copy, Debug)] | ||||
| pub struct LedColor { | ||||
|     bits: [u8; 3], | ||||
| } | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| use futures::{stream::FuturesUnordered, StreamExt}; | ||||
| use once_cell::sync::OnceCell; | ||||
| use paris::info; | ||||
| use scrap::Display; | ||||
| use std::sync::Arc; | ||||
| use tokio::{sync::Mutex, task}; | ||||
|  | ||||
| use crate::picker::{ | ||||
|     config::{LedFlowX, LedFlowY, LedStripConfig}, | ||||
|     screen::Screen, | ||||
| use tokio::{ | ||||
|     sync::{Mutex, OnceCell}, | ||||
|     task, | ||||
| }; | ||||
|  | ||||
| use crate::picker::{config, screen::Screen}; | ||||
|  | ||||
| use super::{ | ||||
|     config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor, | ||||
|     screenshot::Screenshot, | ||||
|     config::DisplayConfig, | ||||
|     display_picker::DisplayPicker, | ||||
|     screenshot::{Screenshot, ScreenshotDto}, | ||||
| }; | ||||
|  | ||||
| pub struct Picker { | ||||
| @@ -22,90 +22,43 @@ pub struct Picker { | ||||
| } | ||||
|  | ||||
| impl Picker { | ||||
|     pub fn global() -> &'static Picker { | ||||
|         static SCREEN_COLOR_PICKER: OnceCell<Picker> = OnceCell::new(); | ||||
|     pub async fn global() -> &'static Picker { | ||||
|         static SCREEN_COLOR_PICKER: OnceCell<Picker> = OnceCell::const_new(); | ||||
|  | ||||
|         SCREEN_COLOR_PICKER.get_or_init(|| Picker { | ||||
|             screens: Arc::new(Mutex::new(vec![])), | ||||
|             screenshots: Arc::new(Mutex::new(vec![])), | ||||
|             display_configs: Arc::new(Mutex::new(vec![ | ||||
|                 DisplayConfig { | ||||
|                     index_of_display: 1, | ||||
|                     display_width: 1920, | ||||
|                     display_height: 1200, | ||||
|                     top_led_strip: LedStripConfig { | ||||
|                         index: 1, | ||||
|                         global_start_position: 59, | ||||
|                         global_end_position: 32, | ||||
|                     }, | ||||
|                     bottom_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 0, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                     left_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 0, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                     right_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 0, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                 }, | ||||
|                 DisplayConfig { | ||||
|                     index_of_display: 0, | ||||
|                     display_width: 3008, | ||||
|                     display_height: 1692, | ||||
|                     top_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 31, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                     bottom_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 0, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                     left_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 0, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                     right_led_strip: LedStripConfig { | ||||
|                         index: 0, | ||||
|                         global_start_position: 0, | ||||
|                         global_end_position: 0, | ||||
|                     }, | ||||
|                 }, | ||||
|             ])), | ||||
|         }) | ||||
|         SCREEN_COLOR_PICKER | ||||
|             .get_or_init(|| async { | ||||
|                 let configs = config::Manager::global().get_config().await.display_configs; | ||||
|                 info!("Global Picker use configs. {:?}", configs); | ||||
|                 Picker { | ||||
|                     screens: Arc::new(Mutex::new(vec![])), | ||||
|                     screenshots: Arc::new(Mutex::new(vec![])), | ||||
|                     display_configs: Arc::new(Mutex::new( | ||||
|                         configs, | ||||
|                     )), | ||||
|                 } | ||||
|             }) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     pub async fn list_displays(&self) -> anyhow::Result<Vec<String>> { | ||||
|         let mut configs = self.display_configs.lock().await; | ||||
|         let screenshots = self.screenshots.lock().await; | ||||
|     pub async fn list_displays(&self) -> anyhow::Result<Vec<ScreenshotDto>> { | ||||
|         let mut configs = vec![]; | ||||
|  | ||||
|         let displays = Display::all() | ||||
|             .map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?; | ||||
|  | ||||
|         configs.clear(); | ||||
|         // configs.clear(); | ||||
|         let mut futs = FuturesUnordered::new(); | ||||
|  | ||||
|         for (index, display) in displays.iter().enumerate() { | ||||
|             let height = display.height(); | ||||
|             let width = display.width(); | ||||
|             let config = DisplayConfig::default(index, width, height); | ||||
|             let config = DisplayConfig::default(index, index, width, height); | ||||
|             configs.push(config); | ||||
|         } | ||||
|  | ||||
|         for (index, display) in displays.iter().enumerate() { | ||||
|             let height = display.height(); | ||||
|             let width = display.width(); | ||||
|             let config = configs[index]; | ||||
|         for config in configs.iter() { | ||||
|             futs.push(async move { | ||||
|                 let join = task::spawn(Self::preview_display_by_config(config)); | ||||
|                 let join = task::spawn(Self::preview_display_by_config(config.clone())); | ||||
|                 join.await? | ||||
|             }); | ||||
|         } | ||||
| @@ -123,63 +76,12 @@ impl Picker { | ||||
|         Ok(bitmap_string_list) | ||||
|     } | ||||
|  | ||||
|     pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<String> { | ||||
|     pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<ScreenshotDto> { | ||||
|         let start = time::Instant::now(); | ||||
|         let mut picker = DisplayPicker::from_config(config)?; | ||||
|         let screenshot = picker.take_screenshot()?; | ||||
|         info!("Take Screenshot Spend: {}", start.elapsed()); | ||||
|  | ||||
|         anyhow::Ok(screenshot.to_webp_base64().await) | ||||
|     } | ||||
|  | ||||
|     pub async fn refresh_displays(&self) -> anyhow::Result<()> { | ||||
|         // let displays = Display::all() | ||||
|         //     .map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?; | ||||
|         // let mut screens = self.screens.lock().await; | ||||
|         // let mut screenshots = self.screenshots.lock().await; | ||||
|         // screens.clear(); | ||||
|         // info!("number of displays: {}", displays.len()); | ||||
|         // for display in displays { | ||||
|         //     let height = display.height(); | ||||
|         //     let width = display.width(); | ||||
|         //     match Capturer::new(display) { | ||||
|         //         Ok(capturer) => screens.push(Screen::new(capturer, width, height)), | ||||
|         //         Err(error) => screens.push(Screen::new_failed( | ||||
|         //             anyhow::anyhow!("{}", error), | ||||
|         //             width, | ||||
|         //             height, | ||||
|         //         )), | ||||
|         //     }; | ||||
|         //     screenshots.push(Screenshot::new(width, height)); | ||||
|         // } | ||||
|  | ||||
|         // screens.reverse(); | ||||
|         // screenshots.reverse(); | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     pub async fn take_screenshots_for_all(&self) -> anyhow::Result<Vec<Screenshot>> { | ||||
|         let mut screens = self.screens.lock().await; | ||||
|         let screenshots = self.screenshots.lock().await; | ||||
|         for (index, screen) in screens.iter_mut().enumerate() { | ||||
|             let bitmap = screen.take().map_err(|error| { | ||||
|                 anyhow::anyhow!("take screenshot for display failed. {}", error) | ||||
|             })?; | ||||
|         } | ||||
|         Ok(screenshots.to_vec()) | ||||
|     } | ||||
|  | ||||
|     pub async fn get_led_strip_colors(&self) -> anyhow::Result<Vec<LedColor>> { | ||||
|         let screenshots = self.screenshots.lock().await; | ||||
|         let mut colors = Vec::new(); | ||||
|         for screenshot in screenshots.iter() { | ||||
|             let result = screenshot | ||||
|                 .get_top_colors() | ||||
|                 .await | ||||
|                 .map_err(|error| anyhow::anyhow!("get top colors failed. {}", error))?; | ||||
|             colors.extend_from_slice(&result); | ||||
|         } | ||||
|         Ok(colors) | ||||
|         anyhow::Ok(screenshot.to_dto().await) | ||||
|     } | ||||
| } | ||||
|   | ||||
							
								
								
									
										83
									
								
								src-tauri/src/picker/preview_manager.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src-tauri/src/picker/preview_manager.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| use futures::{stream::FuturesUnordered, StreamExt}; | ||||
| use once_cell::sync::OnceCell; | ||||
| use paris::{info, warn}; | ||||
| use scrap::Display; | ||||
| use std::{borrow::Borrow, sync::Arc}; | ||||
| use tokio::{sync::Mutex, task}; | ||||
|  | ||||
| use crate::picker::{config, screen::Screen}; | ||||
|  | ||||
| use super::{ | ||||
|     config::DisplayConfig, | ||||
|     display_picker::DisplayPicker, | ||||
|     manager::Picker, | ||||
|     screenshot::{Screenshot, ScreenshotDto}, | ||||
| }; | ||||
|  | ||||
| pub struct PreviewPicker { | ||||
|     pub pickers: Arc<Mutex<Vec<Arc<Mutex<DisplayPicker>>>>>, | ||||
|     pub screenshots: Arc<Mutex<Vec<Screenshot>>>, | ||||
| } | ||||
|  | ||||
| impl PreviewPicker { | ||||
|     pub fn global() -> &'static PreviewPicker { | ||||
|         static SCREEN_COLOR_PREVIEW_PICKER: OnceCell<PreviewPicker> = OnceCell::new(); | ||||
|  | ||||
|         SCREEN_COLOR_PREVIEW_PICKER.get_or_init(|| PreviewPicker { | ||||
|             pickers: Arc::new(Mutex::new(vec![])), | ||||
|             screenshots: Arc::new(Mutex::new(vec![])), | ||||
|         }) | ||||
|     } | ||||
|  | ||||
|     pub async fn list_displays(&self) { | ||||
|         let mut pickers = self.pickers.lock().await; | ||||
|         let displays = Display::all() | ||||
|             .map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?; | ||||
|  | ||||
|         let mut configs = vec![]; | ||||
|         let mut futs = FuturesUnordered::new(); | ||||
|  | ||||
|         for (index, display) in displays.iter().enumerate() { | ||||
|             let height = display.height(); | ||||
|             let width = display.width(); | ||||
|             let config = DisplayConfig::default(index, width, height); | ||||
|             configs.push(config); | ||||
|         } | ||||
|  | ||||
|         for config in configs.iter() { | ||||
|             let picker = DisplayPicker::from_config(*config); | ||||
|             match picker { | ||||
|                 Ok(picker) => { | ||||
|                     pickers.push(Arc::new(Mutex::new(picker))); | ||||
|                 } | ||||
|                 Err(_) => { | ||||
|                     warn!( | ||||
|                         "can not create DisplayPicker from config. config: {:?}", | ||||
|                         config | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn get_screenshot_by_config( | ||||
|         &self, | ||||
|         config: DisplayConfig, | ||||
|     ) -> anyhow::Result<ScreenshotDto> { | ||||
|         let start = time::Instant::now(); | ||||
|         let mut picker = DisplayPicker::from_config(config)?; | ||||
|         let screenshot = picker.take_screenshot()?; | ||||
|         info!("Take Screenshot Spend: {}", start.elapsed()); | ||||
|  | ||||
|         anyhow::Ok(screenshot.to_dto().await) | ||||
|     } | ||||
|  | ||||
|     pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<ScreenshotDto> { | ||||
|         let start = time::Instant::now(); | ||||
|         let mut picker = DisplayPicker::from_config(config)?; | ||||
|         let screenshot = picker.take_screenshot()?; | ||||
|         info!("Take Screenshot Spend: {}", start.elapsed()); | ||||
|  | ||||
|         anyhow::Ok(screenshot.to_dto().await) | ||||
|     } | ||||
| } | ||||
| @@ -1,127 +1,286 @@ | ||||
| use std::ops::Range; | ||||
| use image::ImageBuffer; | ||||
| use image::{ImageOutputFormat, Rgb}; | ||||
| use paris::{error, info}; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::iter; | ||||
| use std::time::SystemTime; | ||||
|  | ||||
| use color_space::{Hsv, Rgb}; | ||||
| use either::Either; | ||||
| use super::{config::DisplayConfig, led_color::LedColor}; | ||||
|  | ||||
| use super::{ | ||||
|     config::{DisplayConfig, LedStripConfig}, | ||||
|     led_color::LedColor, | ||||
| }; | ||||
| type Point = (usize, usize); | ||||
| type LedSamplePoints = Vec<Point>; | ||||
|  | ||||
| #[derive(Clone)] | ||||
| #[derive(Clone, Serialize, Deserialize, Debug)] | ||||
| struct ScreenSamplePoints { | ||||
|     pub top: Vec<LedSamplePoints>, | ||||
|     pub bottom: Vec<LedSamplePoints>, | ||||
|     pub left: Vec<LedSamplePoints>, | ||||
|     pub right: Vec<LedSamplePoints>, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Serialize, Deserialize, Debug)] | ||||
| pub struct Screenshot { | ||||
|     bitmap: Vec<u8>, | ||||
|     config: DisplayConfig, | ||||
|     sample_points: ScreenSamplePoints, | ||||
| } | ||||
|  | ||||
| impl Screenshot { | ||||
|     pub fn new(bitmap: Vec<u8>, config: DisplayConfig) -> Self { | ||||
|         Self { bitmap, config } | ||||
|     } | ||||
|  | ||||
|     pub async fn get_top_colors(&self) -> anyhow::Result<Vec<LedColor>> { | ||||
|         self.get_x_colors(XPosition::Top, self.config.top_led_strip) | ||||
|             .await | ||||
|     } | ||||
|     pub async fn get_bottom_colors(&self) -> anyhow::Result<Vec<LedColor>> { | ||||
|         self.get_x_colors(XPosition::Bottom, self.config.bottom_led_strip) | ||||
|             .await | ||||
|     } | ||||
|  | ||||
|     pub fn get_top_of_led_start_at(&self) -> usize { | ||||
|         self.config.top_led_strip.global_start_position | ||||
|     } | ||||
|     pub fn get_top_of_led_end_at(&self) -> usize { | ||||
|         self.config.top_led_strip.global_end_position | ||||
|     } | ||||
|  | ||||
|     async fn get_x_colors( | ||||
|         &self, | ||||
|         position: XPosition, | ||||
|         strip_config: LedStripConfig, | ||||
|     ) -> anyhow::Result<Vec<LedColor>> { | ||||
|         let bitmap = &self.bitmap; | ||||
|         let number_of_leds = strip_config | ||||
|             .global_start_position | ||||
|             .abs_diff(strip_config.global_end_position) | ||||
|             + 1; | ||||
|         if number_of_leds == 0 { | ||||
|             return Ok(vec![]); | ||||
|         Self { | ||||
|             bitmap, | ||||
|             config, | ||||
|             sample_points: Self::get_sample_points(config), | ||||
|         } | ||||
|         let cell_size_x = self.config.display_width / number_of_leds; | ||||
|         let cell_size_y = self.config.display_height / 8; | ||||
|         let cell_size = cell_size_x * cell_size_y; | ||||
|         let y_range = match position { | ||||
|             XPosition::Top => 20..cell_size_y + 20, | ||||
|             XPosition::Bottom => { | ||||
|                 self.config.display_height - 20 - cell_size_y..self.config.display_height - 20 | ||||
|     } | ||||
|  | ||||
|     fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints { | ||||
|         let top = match config.led_strip_of_borders.top { | ||||
|             Some(led_strip_config) => Self::get_one_edge_sample_points( | ||||
|                 config.display_height / 8, | ||||
|                 config.display_width, | ||||
|                 led_strip_config | ||||
|                     .global_start_position | ||||
|                     .abs_diff(led_strip_config.global_end_position) + 1, | ||||
|                 5, | ||||
|             ), | ||||
|             None => { | ||||
|                 vec![] | ||||
|             } | ||||
|         } | ||||
|         .step_by(5); | ||||
|  | ||||
|         let x_range = if strip_config.global_start_position < strip_config.global_end_position { | ||||
|             Either::Left(strip_config.global_start_position..=strip_config.global_end_position) | ||||
|         } else { | ||||
|             Either::Right( | ||||
|                 (strip_config.global_end_position..=strip_config.global_start_position).rev(), | ||||
|             ) | ||||
|         }; | ||||
|  | ||||
|         let mut colors = Vec::new(); | ||||
|         let stride = bitmap.len() / self.config.display_height; | ||||
|         let bottom: Vec<LedSamplePoints> = match config.led_strip_of_borders.bottom { | ||||
|             Some(led_strip_config) => { | ||||
|                 let points = Self::get_one_edge_sample_points( | ||||
|                     config.display_height / 9, | ||||
|                     config.display_width, | ||||
|                     led_strip_config | ||||
|                         .global_start_position | ||||
|                         .abs_diff(led_strip_config.global_end_position) + 1, | ||||
|                     5, | ||||
|                 ); | ||||
|                 points | ||||
|                     .into_iter() | ||||
|                     .map(|groups| -> Vec<Point> { | ||||
|                         groups | ||||
|                             .into_iter() | ||||
|                             .map(|(x, y)| (x, config.display_height - y)) | ||||
|                             .collect() | ||||
|                     }) | ||||
|                     .collect() | ||||
|             } | ||||
|             None => { | ||||
|                 vec![] | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         for pos in x_range { | ||||
|         let left: Vec<LedSamplePoints> = match config.led_strip_of_borders.left { | ||||
|             Some(led_strip_config) => { | ||||
|                 let points = Self::get_one_edge_sample_points( | ||||
|                     config.display_width / 16, | ||||
|                     config.display_height, | ||||
|                     led_strip_config | ||||
|                         .global_start_position | ||||
|                         .abs_diff(led_strip_config.global_end_position) + 1, | ||||
|                     5, | ||||
|                 ); | ||||
|                 points | ||||
|                     .into_iter() | ||||
|                     .map(|groups| -> Vec<Point> { | ||||
|                         groups.into_iter().map(|(x, y)| (y, x)).collect() | ||||
|                     }) | ||||
|                     .collect() | ||||
|             } | ||||
|             None => { | ||||
|                 vec![] | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         let right: Vec<LedSamplePoints> = match config.led_strip_of_borders.right { | ||||
|             Some(led_strip_config) => { | ||||
|                 let points = Self::get_one_edge_sample_points( | ||||
|                     config.display_width / 16, | ||||
|                     config.display_height, | ||||
|                     led_strip_config | ||||
|                         .global_start_position | ||||
|                         .abs_diff(led_strip_config.global_end_position) + 1, | ||||
|                     5, | ||||
|                 ); | ||||
|                 points | ||||
|                     .into_iter() | ||||
|                     .map(|groups| -> Vec<Point> { | ||||
|                         groups | ||||
|                             .into_iter() | ||||
|                             .map(|(x, y)| (config.display_width - y, x)) | ||||
|                             .collect() | ||||
|                     }) | ||||
|                     .collect() | ||||
|             } | ||||
|             None => { | ||||
|                 vec![] | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         ScreenSamplePoints { | ||||
|             top, | ||||
|             bottom, | ||||
|             left, | ||||
|             right, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     fn get_one_edge_sample_points( | ||||
|         width: usize, | ||||
|         length: usize, | ||||
|         leds: usize, | ||||
|         single_axis_points: usize, | ||||
|     ) -> Vec<LedSamplePoints> { | ||||
|         let cell_size_x = length as f64 / single_axis_points as f64 / leds as f64; | ||||
|         let cell_size_y = width / single_axis_points; | ||||
|  | ||||
|         let point_start_y = cell_size_y / 2; | ||||
|         let point_start_x = cell_size_x / 2.0; | ||||
|         let point_y_list: Vec<usize> = (point_start_y..width).step_by(cell_size_y).collect(); | ||||
|         let point_x_list: Vec<usize> = iter::successors(Some(point_start_x), |i| { | ||||
|             let next = i + cell_size_x; | ||||
|             (next < (length as f64)).then_some(next) | ||||
|         }) | ||||
|         .map(|i| i as usize) | ||||
|         .collect(); | ||||
|  | ||||
|         let points: Vec<Point> = point_x_list | ||||
|             .iter() | ||||
|             .map(|&x| point_y_list.iter().map(move |&y| (x, y))) | ||||
|             .flatten() | ||||
|             .collect(); | ||||
|         points | ||||
|             .chunks(single_axis_points * single_axis_points) | ||||
|             .into_iter() | ||||
|             .map(|points| Vec::from(points)) | ||||
|             .collect() | ||||
|     } | ||||
|  | ||||
|     pub fn get_colors(&self) -> DisplayColorsOfLedStrips { | ||||
|         let top = self | ||||
|             .get_one_edge_colors(&self.sample_points.top) | ||||
|             .into_iter() | ||||
|             .flat_map(|color| color.get_rgb()) | ||||
|             .collect(); | ||||
|         let bottom = self | ||||
|             .get_one_edge_colors(&self.sample_points.bottom) | ||||
|             .into_iter() | ||||
|             .flat_map(|color| color.get_rgb()) | ||||
|             .collect(); | ||||
|         let left = self | ||||
|             .get_one_edge_colors(&self.sample_points.left) | ||||
|             .into_iter() | ||||
|             .flat_map(|color| color.get_rgb()) | ||||
|             .collect(); | ||||
|         let right = self | ||||
|             .get_one_edge_colors(&self.sample_points.right) | ||||
|             .into_iter() | ||||
|             .flat_map(|color| color.get_rgb()) | ||||
|             .collect(); | ||||
|         DisplayColorsOfLedStrips { | ||||
|             top, | ||||
|             bottom, | ||||
|             left, | ||||
|             right, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub fn get_one_edge_colors( | ||||
|         &self, | ||||
|         sample_points_of_leds: &Vec<LedSamplePoints>, | ||||
|     ) -> Vec<LedColor> { | ||||
|         let mut colors = vec![]; | ||||
|         for led_points in sample_points_of_leds { | ||||
|             let mut r = 0.0; | ||||
|             let mut g = 0.0; | ||||
|             let mut b = 0.0; | ||||
|             let mut count = 0; | ||||
|             for x in (pos * cell_size_x..(pos + 1) * cell_size_x).step_by(5) { | ||||
|                 for y in y_range.to_owned() { | ||||
|                     let i = stride * y + 4 * x; | ||||
|                     r += bitmap[i + 2] as f64; | ||||
|                     g += bitmap[i + 1] as f64; | ||||
|                     b += bitmap[i] as f64; | ||||
|                     count+=1; | ||||
|                 } | ||||
|             let len = led_points.len() as f64; | ||||
|             for (x, y) in led_points { | ||||
|                 let position = (x + y * self.config.display_width) * 4; | ||||
|                 r += self.bitmap[position + 2] as f64; | ||||
|                 g += self.bitmap[position + 1] as f64; | ||||
|                 b += self.bitmap[position] as f64; | ||||
|             } | ||||
|             let rgb = Rgb::new( | ||||
|                 r / count as f64, | ||||
|                 g / count as f64, | ||||
|                 b / count as f64, | ||||
|             ); | ||||
|             let hsv = Hsv::from(rgb); | ||||
|             // info!("HSV: {:?}", [hsv.h, hsv.s, hsv.v]); | ||||
|             let color = LedColor::from_hsv(hsv.h, hsv.s, hsv.v); | ||||
|             let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8); | ||||
|             // paris::info!("color: {:?}", color.get_rgb()); | ||||
|             colors.push(color); | ||||
|         } | ||||
|         return Ok(colors); | ||||
|         colors | ||||
|     } | ||||
|  | ||||
|     pub async fn to_webp_base64(&self) -> String { | ||||
|         let bitmap = &self.bitmap; | ||||
|         let mut bitflipped = | ||||
|             Vec::with_capacity(self.config.display_width * self.config.display_height * 3); | ||||
|         let stride = bitmap.len() / self.config.display_height; | ||||
|  | ||||
|         for y in 0..self.config.display_height { | ||||
|             for x in 0..self.config.display_width { | ||||
|                 let i = stride * y + 4 * x; | ||||
|                 bitflipped.extend_from_slice(&[bitmap[i + 2], bitmap[i + 1], bitmap[i]]); | ||||
|         let mut image_buffer = ImageBuffer::new( | ||||
|             self.config.display_width as u32 / 3, | ||||
|             self.config.display_height as u32 / 3, | ||||
|         ); | ||||
|  | ||||
|         for y in 0..self.config.display_height / 3 { | ||||
|             for x in 0..self.config.display_width / 3 { | ||||
|                 let i = stride * y * 3 + 4 * x * 3; | ||||
|                 image_buffer.put_pixel( | ||||
|                     x as u32, | ||||
|                     y as u32, | ||||
|                     Rgb::<u8>([bitmap[i + 2], bitmap[i + 1], bitmap[i]]), | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         let webp_memory = webp::Encoder::from_rgb( | ||||
|             bitflipped.as_slice(), | ||||
|             self.config.display_width as u32, | ||||
|             self.config.display_height as u32, | ||||
|         ) | ||||
|         .encode(100.0); | ||||
|         return base64::encode(&*webp_memory); | ||||
|         // let webp_memory = | ||||
|         //     webp::Encoder::from_rgb(bitflipped.as_slice(), size_x, size_y).encode(50.0); | ||||
|         // return base64::encode(&*webp_memory); | ||||
|  | ||||
|         let mut cursor = std::io::Cursor::new(vec![]); | ||||
|         match image_buffer.write_to(&mut cursor, ImageOutputFormat::Tiff) { | ||||
|             Ok(_) => { | ||||
|                 return base64::encode(cursor.into_inner()); | ||||
|             } | ||||
|             Err(err) => { | ||||
|                 error!("can not encode image. {:?}", err); | ||||
|                 return String::from(""); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn to_dto(&self) -> ScreenshotDto { | ||||
|         let rk = SystemTime::now(); | ||||
|         info!("[{:?} {:p}] to_dto", rk.elapsed(), &self); | ||||
|         let encode_image = self.to_webp_base64().await; | ||||
|         info!("[{:?} {:p}] image", rk.elapsed(), &self); | ||||
|         let config = self.config.clone(); | ||||
|         info!("[{:?} {:p}] cloned", rk.elapsed(), &self); | ||||
|         let colors = self.get_colors(); | ||||
|         info!("[{:?} {:p}] colors", rk.elapsed(), &self); | ||||
|         ScreenshotDto { | ||||
|             encode_image, | ||||
|             config, | ||||
|             colors, | ||||
|         } | ||||
|     } | ||||
|  | ||||
|  | ||||
|     pub fn get_config(&self) -> DisplayConfig { | ||||
|         self.config | ||||
|     } | ||||
| } | ||||
|  | ||||
| enum XPosition { | ||||
|     Top, | ||||
|     Bottom, | ||||
| #[derive(Clone, Serialize, Deserialize, Debug)] | ||||
| pub struct ScreenshotDto { | ||||
|     pub config: DisplayConfig, | ||||
|     pub encode_image: String, | ||||
|     pub colors: DisplayColorsOfLedStrips, | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Serialize, Deserialize, Debug)] | ||||
| pub struct DisplayColorsOfLedStrips { | ||||
|     pub top: Vec<u8>, | ||||
|     pub bottom: Vec<u8>, | ||||
|     pub left: Vec<u8>, | ||||
|     pub right: Vec<u8>, | ||||
| } | ||||
|   | ||||
| @@ -38,4 +38,18 @@ impl Manager { | ||||
|             .await | ||||
|             .map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error)) | ||||
|     } | ||||
|  | ||||
|     pub async fn publish_led_sub_pixels(&self, payload: Vec<u8>) -> anyhow::Result<()> { | ||||
|  | ||||
|         self.mqtt | ||||
|             .client | ||||
|             .publish( | ||||
|                 "display-ambient-light/desktop/colors", | ||||
|                 rumqttc::QoS::AtLeastOnce, | ||||
|                 false, | ||||
|                 payload, | ||||
|             ) | ||||
|             .await | ||||
|             .map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -28,7 +28,7 @@ | ||||
|         "icons/icon.icns", | ||||
|         "icons/icon.ico" | ||||
|       ], | ||||
|       "identifier": "com.tauri.dev", | ||||
|       "identifier": "cc.ivanli.ambient", | ||||
|       "longDescription": "", | ||||
|       "macOS": { | ||||
|         "entitlements": null, | ||||
|   | ||||
							
								
								
									
										72
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/App.tsx
									
									
									
									
									
								
							| @@ -1,11 +1,14 @@ | ||||
| import { useCallback, useState } from 'react'; | ||||
| import reactLogo from './assets/react.svg'; | ||||
| import { useCallback, useEffect, useState } from 'react'; | ||||
| import tw from 'twin.macro'; | ||||
| import { invoke } from '@tauri-apps/api/tauri'; | ||||
| import './App.css'; | ||||
| import clsx from 'clsx'; | ||||
| import { Configurator } from './configurator/configurator'; | ||||
| import { ButtonSwitch } from './commons/components/button'; | ||||
|  | ||||
| type Mode = 'Flowing' | 'Follow' | null; | ||||
|  | ||||
| localStorage.setItem('debug', '*'); | ||||
|  | ||||
| function App() { | ||||
|   const [screenshots, setScreenshots] = useState<string[]>([]); | ||||
|   const [ledStripColors, setLedStripColors] = useState<string[]>([]); | ||||
| @@ -17,10 +20,6 @@ function App() { | ||||
|     setScreenshots(base64TextList.map((text) => `data:image/webp;base64,${text}`)); | ||||
|   } | ||||
|  | ||||
|   const refreshDisplays = useCallback(async () => { | ||||
|     await invoke('refresh_displays'); | ||||
|   }, []); | ||||
|  | ||||
|   const getLedStripColors = useCallback(async () => { | ||||
|     setLedStripColors(await invoke('get_led_strip_colors')); | ||||
|   }, []); | ||||
| @@ -42,66 +41,31 @@ function App() { | ||||
|  | ||||
|   return ( | ||||
|     <div> | ||||
|       <div className="flex justify-between"> | ||||
|       <div tw="flex justify-between"> | ||||
|         {ledStripColors.map((it) => ( | ||||
|           <span className=" h-8 flex-auto" style={{ backgroundColor: it }}></span> | ||||
|           <span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span> | ||||
|         ))} | ||||
|       </div> | ||||
|  | ||||
|       <div className="flex gap-1 justify-center w-screen overflow-hidden"> | ||||
|       <div tw="flex gap-1 justify-center w-screen overflow-hidden"> | ||||
|         {screenshots.map((screenshot) => ( | ||||
|           <div className="flex-auto"> | ||||
|           <div tw="flex-auto"> | ||||
|             <img src={screenshot} /> | ||||
|           </div> | ||||
|         ))} | ||||
|       </div> | ||||
|  | ||||
|       <div className="flex gap-5 justify-center "> | ||||
|         <button | ||||
|           className="bg-black bg-opacity-20" | ||||
|           type="button" | ||||
|           onClick={() => refreshDisplays()} | ||||
|         > | ||||
|           Refresh Displays | ||||
|         </button> | ||||
|         <button | ||||
|           className="bg-black bg-opacity-20" | ||||
|           type="button" | ||||
|           onClick={() => takeSnapshot()} | ||||
|         > | ||||
|           Take Snapshot | ||||
|         </button> | ||||
|         <button | ||||
|           className="bg-black bg-opacity-20" | ||||
|           type="button" | ||||
|           onClick={() => getLedStripColors()} | ||||
|         > | ||||
|           Get Colors | ||||
|         </button> | ||||
|         <button | ||||
|           className={clsx('bg-black', 'bg-opacity-20', { | ||||
|             'bg-gradient-to-r from-purple-500 to-blue-500': currentMode === 'Flowing', | ||||
|           })} | ||||
|           type="button" | ||||
|           onClick={() => switchCurrentMode('Flowing')} | ||||
|         > | ||||
|       <div tw="flex gap-5 justify-center "> | ||||
|         <ButtonSwitch onClick={() => takeSnapshot()}>Take Snapshot</ButtonSwitch> | ||||
|         <ButtonSwitch onClick={() => getLedStripColors()}>Get Colors</ButtonSwitch> | ||||
|         <ButtonSwitch onClick={() => switchCurrentMode('Flowing')}> | ||||
|           Flowing Light | ||||
|         </button> | ||||
|         <button | ||||
|           className={clsx('bg-black', 'bg-opacity-20', { | ||||
|             'bg-gradient-to-r from-purple-500 to-blue-500': currentMode === 'Follow', | ||||
|           })} | ||||
|           type="button" | ||||
|           onClick={() => switchCurrentMode('Follow')} | ||||
|         > | ||||
|           Follow | ||||
|         </button> | ||||
|         </ButtonSwitch> | ||||
|         <ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch> | ||||
|       </div> | ||||
|  | ||||
|       <div className="flex gap-5 justify-center"> | ||||
|         <img src="/vite.svg" className="logo vite" alt="Vite logo" /> | ||||
|         <img src="/tauri.svg" className="logo tauri" alt="Tauri logo" /> | ||||
|         <img src={reactLogo} className="logo react" alt="React logo" /> | ||||
|       <div tw="flex gap-5 justify-center"> | ||||
|         <Configurator /> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
|   | ||||
							
								
								
									
										21
									
								
								src/commons/components/button.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/commons/components/button.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { FC } from 'react'; | ||||
| import styled from '@emotion/styled'; | ||||
| import tw, { theme } from 'twin.macro'; | ||||
| import { css } from '@emotion/react'; | ||||
|  | ||||
| interface ButtonProps { | ||||
|   value?: boolean; | ||||
|   isSmall?: boolean; | ||||
| } | ||||
|  | ||||
| export const ButtonSwitch = styled.button(({ value, isSmall }: ButtonProps) => [ | ||||
|   // The common button styles | ||||
|   tw`px-8 py-2 rounded-xl transform duration-75 dark:bg-black m-2 shadow-lg text-opacity-95 dark:shadow-gray-800`, | ||||
|  | ||||
|   tw`hover:(scale-105)`, | ||||
|   tw`focus:(scale-100)`, | ||||
|  | ||||
|   value && 'bg-gradient-to-r from-purple-500 to-blue-500', | ||||
|  | ||||
|   isSmall ? tw`text-sm` : tw`text-lg`, | ||||
| ]); | ||||
							
								
								
									
										133
									
								
								src/configurator/components/completed-led-strip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								src/configurator/components/completed-led-strip.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,133 @@ | ||||
| import debug from 'debug'; | ||||
| import { isNil, lensPath, set, splitEvery, update } from 'ramda'; | ||||
| import { FC, useEffect, useMemo, useState } from 'react'; | ||||
| import tw, { css, styled } from 'twin.macro'; | ||||
| import { Borders, borders } from '../../constants/border'; | ||||
| import { useLedCount } from '../contents/led-count'; | ||||
| import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config'; | ||||
| import { LedStripConfig } from '../models/led-strip-config'; | ||||
| import { PixelRgb } from '../models/pixel-rgb'; | ||||
| import { ScreenshotDto } from '../models/screenshot.dto'; | ||||
| import { DraggableStrip } from './draggable-strip'; | ||||
| import { StyledPixel } from './styled-pixel'; | ||||
|  | ||||
| export const logger = debug('app:completed-led-strip'); | ||||
|  | ||||
| interface CompletedLedStripProps { | ||||
|   screenshots: ScreenshotDto[]; | ||||
|   onDisplayConfigChange?: (value: DisplayConfig) => void; | ||||
| } | ||||
|  | ||||
| type BorderLedStrip = { | ||||
|   pixels: PixelRgb[]; | ||||
|   config: LedStripConfig | null; | ||||
| }; | ||||
|  | ||||
| const StyledContainer = styled.section( | ||||
|   ({ rows, columns }: { rows: number; columns: number }) => [ | ||||
|     tw`grid m-4 pb-2 items-center justify-items-center select-none`, | ||||
|     css` | ||||
|       grid-template-columns: repeat(${columns}, 1fr); | ||||
|       grid-template-rows: auto repeat(${rows}, 1fr); | ||||
|     `, | ||||
|   ], | ||||
| ); | ||||
| 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, | ||||
| }) => { | ||||
|   const borderLedStrips: BorderLedStrip[] = useMemo(() => { | ||||
|     return screenshots.flatMap((ss) => | ||||
|       borders.map((b) => ({ | ||||
|         pixels: splitEvery(3, Array.from(ss.colors[b])) as PixelRgb[], | ||||
|         config: ss.config.led_strip_of_borders[b], | ||||
|       })), | ||||
|     ); | ||||
|   }, [screenshots]); | ||||
|   const ledCount = useMemo( | ||||
|     () => borderLedStrips.reduce((prev, curr) => prev + curr.pixels.length, 0), | ||||
|     [borderLedStrips], | ||||
|   ); | ||||
|  | ||||
|   const { setLedCount } = useLedCount(); | ||||
|   // setLedCount for context | ||||
|   useEffect(() => { | ||||
|     setLedCount(ledCount); | ||||
|   }, [ledCount, setLedCount]); | ||||
|  | ||||
|   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 ? ( | ||||
|         <DraggableStrip | ||||
|           key={index} | ||||
|           {...{ config, pixels, index: index + 1 }} | ||||
|           onConfigChange={(c) => { | ||||
|             setOverrideBorderLedStrips( | ||||
|               update(index, { config: c, pixels }, borderLedStrips), | ||||
|             ); | ||||
|           }} | ||||
|           onConfigFinish={(c) => { | ||||
|             const indexOfDisplay = Math.floor(index / borders.length); | ||||
|             const xLens = lensPath<LedStripConfigOfBorders, Borders>([ | ||||
|               borders[index % borders.length], | ||||
|             ]); | ||||
|             const displayConfig: DisplayConfig = { | ||||
|               ...screenshots[indexOfDisplay].config, | ||||
|               led_strip_of_borders: set( | ||||
|                 xLens, | ||||
|                 c, | ||||
|                 screenshots[indexOfDisplay].config.led_strip_of_borders, | ||||
|               ), | ||||
|             }; | ||||
|             onDisplayConfigChange?.(displayConfig); | ||||
|           }} | ||||
|         /> | ||||
|       ) : ( | ||||
|         <div key={index} /> | ||||
|       ), | ||||
|     ); | ||||
|   }, [borderLedStrips, screenshots]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     setOverrideBorderLedStrips(undefined); | ||||
|   }, [borderLedStrips]); | ||||
|   return ( | ||||
|     <StyledContainer rows={screenshots.length * borders.length} columns={ledCount}> | ||||
|       <StyledCompletedContainer>{completedPixels}</StyledCompletedContainer> | ||||
|       {strips} | ||||
|     </StyledContainer> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
|  | ||||
							
								
								
									
										100
									
								
								src/configurator/components/display-with-led-strips.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								src/configurator/components/display-with-led-strips.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,100 @@ | ||||
| import { HTMLAttributes, useCallback, useMemo } from 'react'; | ||||
| import { FC } from 'react'; | ||||
| import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config'; | ||||
| import { LedStrip } from './led-strip'; | ||||
| import tw, { css, styled, theme } from 'twin.macro'; | ||||
| import { ScreenshotDto } from '../models/screenshot.dto'; | ||||
| import { LedStripEditor } from './led-strip-editor'; | ||||
| import { LedStripConfig } from '../models/led-strip-config'; | ||||
| import debug from 'debug'; | ||||
| import { lensPath, lensProp, set, view } from 'ramda'; | ||||
|  | ||||
| const logger = debug('app:display-with-led-strips'); | ||||
|  | ||||
| export interface DisplayWithLedStripsProps | ||||
|   extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> { | ||||
|   config: DisplayConfig; | ||||
|   screenshot: ScreenshotDto; | ||||
|   onChange?: (config: DisplayConfig) => void; | ||||
| } | ||||
|  | ||||
| const StyledContainer = styled.section( | ||||
|   tw`m-4 grid gap-1`, | ||||
|   css` | ||||
|     grid-template-columns: ${theme`width.5`} ${theme`width.3`} auto ${theme`width.3`} ${theme`width.5`}; | ||||
|   `, | ||||
|   css` | ||||
|     grid-template-rows: ${theme`width.5`} ${theme`width.3`} auto ${theme`width.3`} ${theme`width.5`}; | ||||
|   `, | ||||
| ); | ||||
|  | ||||
| export const DisplayWithLedStrips: FC<DisplayWithLedStripsProps> = ({ | ||||
|   config, | ||||
|   screenshot, | ||||
|   onChange, | ||||
|   ...htmlAttrs | ||||
| }) => { | ||||
|   const screenshotUrl = useMemo( | ||||
|     () => `data:image/ico;base64,${screenshot.encode_image}`, | ||||
|     [screenshot.encode_image], | ||||
|   ); | ||||
|  | ||||
|   const onLedStripConfigChange = useCallback( | ||||
|     (position: keyof LedStripConfigOfBorders, value: LedStripConfig | null) => { | ||||
|       const xLens = lensPath< | ||||
|         DisplayConfig, | ||||
|         'led_strip_of_borders', | ||||
|         keyof LedStripConfigOfBorders | ||||
|       >(['led_strip_of_borders', position]); | ||||
|       const c = set(xLens, value, config); | ||||
|       logger('on change. prev: %o, curr: %o', view(xLens, config), value); | ||||
|       onChange?.(c); | ||||
|     }, | ||||
|     [config], | ||||
|   ); | ||||
|   return ( | ||||
|     <StyledContainer {...htmlAttrs}> | ||||
|       <img src={screenshotUrl} tw="row-start-3 col-start-3 w-full" /> | ||||
|       <LedStrip | ||||
|         config={config.led_strip_of_borders.top} | ||||
|         colors={screenshot.colors.top} | ||||
|         tw="row-start-2 col-start-3" | ||||
|       /> | ||||
|       <LedStrip | ||||
|         config={config.led_strip_of_borders.left} | ||||
|         colors={screenshot.colors.left} | ||||
|         tw="row-start-3 col-start-2" | ||||
|       /> | ||||
|       <LedStrip | ||||
|         config={config.led_strip_of_borders.right} | ||||
|         colors={screenshot.colors.right} | ||||
|         tw="row-start-3 col-start-4" | ||||
|       /> | ||||
|       <LedStrip | ||||
|         config={config.led_strip_of_borders.bottom} | ||||
|         colors={screenshot.colors.bottom} | ||||
|         tw="row-start-4 col-start-3" | ||||
|       /> | ||||
|       <LedStripEditor | ||||
|         config={config.led_strip_of_borders.top} | ||||
|         tw="row-start-1 col-start-3" | ||||
|         onChange={(value) => onLedStripConfigChange('top', value)} | ||||
|       /> | ||||
|       <LedStripEditor | ||||
|         config={config.led_strip_of_borders.left} | ||||
|         tw="row-start-3 col-start-1" | ||||
|         onChange={(value) => onLedStripConfigChange('left', value)} | ||||
|       /> | ||||
|       <LedStripEditor | ||||
|         config={config.led_strip_of_borders.right} | ||||
|         tw="row-start-3 col-start-5" | ||||
|         onChange={(value) => onLedStripConfigChange('right', value)} | ||||
|       /> | ||||
|       <LedStripEditor | ||||
|         config={config.led_strip_of_borders.bottom} | ||||
|         tw="row-start-5 col-start-3" | ||||
|         onChange={(value) => onLedStripConfigChange('bottom', value)} | ||||
|       /> | ||||
|     </StyledContainer> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										199
									
								
								src/configurator/components/draggable-strip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								src/configurator/components/draggable-strip.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,199 @@ | ||||
| import { | ||||
|   createRef, | ||||
|   FC, | ||||
|   Fragment, | ||||
|   MouseEventHandler, | ||||
|   ReactNode, | ||||
|   RefObject, | ||||
|   useCallback, | ||||
|   useEffect, | ||||
|   useMemo, | ||||
|   useRef, | ||||
|   useState, | ||||
| } from 'react'; | ||||
| import { css } from 'twin.macro'; | ||||
| import { useLedCount } from '../contents/led-count'; | ||||
| import { LedStripConfig } from '../models/led-strip-config'; | ||||
| import { PixelRgb } from '../models/pixel-rgb'; | ||||
| import { StyledPixel } from './styled-pixel'; | ||||
| import { logger } from './completed-led-strip'; | ||||
|  | ||||
| interface DraggableStripProp { | ||||
|   config: LedStripConfig; | ||||
|   pixels: PixelRgb[]; | ||||
|   index: number; | ||||
|   onConfigChange?: (config: LedStripConfig) => void; | ||||
|   onConfigFinish?: (config: LedStripConfig) => void; | ||||
| } | ||||
| export const DraggableStrip: FC<DraggableStripProp> = ({ | ||||
|   config, | ||||
|   pixels, | ||||
|   index, | ||||
|   onConfigChange, | ||||
|   onConfigFinish, | ||||
| }) => { | ||||
|   const { ledCount } = useLedCount(); | ||||
|  | ||||
|   const startXRef = useRef(0); | ||||
|   const currentXRef = useRef(0); | ||||
|   const configRef = useRef<LedStripConfig>(); | ||||
|   const [availableConfig, setAvailableConfig] = useState<LedStripConfig>(config); | ||||
|   // const currentDiffRef = useRef(0); | ||||
|   const isDragRef = useRef(false); | ||||
|   const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>(); | ||||
|   const [boxTranslateX, setBoxTranslateX] = useState(0); | ||||
|  | ||||
|   const ledItems = useMemo(() => { | ||||
|     const step = config.global_start_position - config.global_end_position < 0 ? 1 : -1; | ||||
|     return pixels.map((rgb, i) => ( | ||||
|       <StyledPixel | ||||
|         key={i} | ||||
|         rgb={rgb} | ||||
|         css={css` | ||||
|           grid-column: ${(availableConfig.global_start_position ?? 0) + i * step + 1} / | ||||
|             span 1; | ||||
|           grid-row-start: ${index + 1}; | ||||
|           pointer-events: none; | ||||
|         `} | ||||
|       /> | ||||
|     )); | ||||
|   }, [pixels, availableConfig]); | ||||
|  | ||||
|   const [placeholders, placeholderRefs]: [ReactNode[], RefObject<HTMLSpanElement>[]] = | ||||
|     useMemo( | ||||
|       () => | ||||
|         new Array(ledCount) | ||||
|           .fill(undefined) | ||||
|           .map((_, i) => { | ||||
|             const ref = createRef<HTMLSpanElement>(); | ||||
|             const n = ( | ||||
|               <span | ||||
|                 ref={ref} | ||||
|                 key={i} | ||||
|                 tw=" h-full w-full" | ||||
|                 css={css` | ||||
|                   grid-column-start: ${i + 1}; | ||||
|                   grid-row-start: ${index + 1}; | ||||
|                 `} | ||||
|               /> | ||||
|             ); | ||||
|             return [n, ref] as [ReactNode, RefObject<HTMLSpanElement>]; | ||||
|           }) | ||||
|           .reduce( | ||||
|             ([nList, refList], [n, ref]) => [ | ||||
|               [...nList, n], | ||||
|               [...refList, ref], | ||||
|             ], | ||||
|             [[], []] as [ReactNode[], RefObject<HTMLSpanElement>[]], | ||||
|           ), | ||||
|       [ledCount], | ||||
|     ); | ||||
|  | ||||
|   // start and moving | ||||
|   const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback( | ||||
|     (ev) => { | ||||
|       startXRef.current = ev.pageX; | ||||
|       ev.currentTarget.requestPointerLock(); | ||||
|       isDragRef.current = true; | ||||
|       logger('handleMouseDown, config: %o', config); | ||||
|  | ||||
|       const placeholderPositions = placeholderRefs.map((it) => { | ||||
|         if (!it.current) { | ||||
|           return [0, 0]; | ||||
|         } | ||||
|         const viewportOffset = it.current.getBoundingClientRect(); | ||||
|         return [viewportOffset.left, viewportOffset.right] as [number, number]; | ||||
|       }); | ||||
|  | ||||
|       logger('placeholderPositions: %o', placeholderPositions); | ||||
|  | ||||
|       // set init position | ||||
|       const initPos = placeholderPositions.findIndex( | ||||
|         ([l, r]) => l <= ev.pageX && r >= ev.pageX, | ||||
|       ); | ||||
|       let prevMatch = 0; | ||||
|  | ||||
|       if (handleMouseMoveRef.current) { | ||||
|         document.body.removeEventListener('mousemove', handleMouseMoveRef.current); | ||||
|       } | ||||
|       handleMouseMoveRef.current = (ev) => { | ||||
|         if (!isDragRef.current) { | ||||
|           return; | ||||
|         } | ||||
|         currentXRef.current = ev.pageX; | ||||
|         setBoxTranslateX(currentXRef.current - startXRef.current); | ||||
|         const match = placeholderPositions.findIndex( | ||||
|           ([l, r]) => l <= currentXRef.current && r >= currentXRef.current, | ||||
|         ); | ||||
|         if (match === -1) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|         if (match === prevMatch) { | ||||
|           return; | ||||
|         } | ||||
|         prevMatch = match; | ||||
|  | ||||
|         const diff = match - initPos; | ||||
|         const newValue: LedStripConfig = { | ||||
|           ...config, | ||||
|           global_start_position: config.global_start_position + diff, | ||||
|           global_end_position: config.global_end_position + diff, | ||||
|         }; | ||||
|         configRef.current = newValue; | ||||
|         setAvailableConfig(newValue); | ||||
|         logger('change config. old: %o, new: %o', config, newValue); | ||||
|         onConfigChange?.(newValue); | ||||
|       }; | ||||
|       document.body.addEventListener('mousemove', handleMouseMoveRef.current); | ||||
|     }, | ||||
|     [placeholderRefs, availableConfig, setAvailableConfig, config], | ||||
|   ); | ||||
|  | ||||
|   // move event. | ||||
|   useEffect(() => { | ||||
|     const handleMouseUp = (ev: MouseEvent) => { | ||||
|       if (configRef.current && isDragRef.current) { | ||||
|         onConfigFinish?.(configRef.current); | ||||
|       } | ||||
|       startXRef.current = 0; | ||||
|       isDragRef.current = false; | ||||
|       document.exitPointerLock(); | ||||
|       if (handleMouseMoveRef.current) { | ||||
|         document.body.removeEventListener('mousemove', handleMouseMoveRef.current); | ||||
|       } | ||||
|     }; | ||||
|     document.body.addEventListener('mouseup', handleMouseUp); | ||||
|     return () => { | ||||
|       document.body.removeEventListener('mouseup', handleMouseUp); | ||||
|     }; | ||||
|   }, [onConfigFinish]); | ||||
|   // reset translateX when config updated. | ||||
|   useEffect(() => { | ||||
|     startXRef.current = currentXRef.current; | ||||
|     setAvailableConfig(config); | ||||
|     setBoxTranslateX(0); | ||||
|     logger('useEffect, config: %o', config); | ||||
|   }, [config]); | ||||
|  | ||||
|   return ( | ||||
|     <Fragment> | ||||
|       {placeholders} | ||||
|       {ledItems} | ||||
|       <div | ||||
|         tw="border border-gray-700 h-3 w-full rounded-full" | ||||
|         css={css` | ||||
|           grid-column: ${Math.min( | ||||
|               config?.global_start_position ?? 0, | ||||
|               config?.global_end_position ?? 0, | ||||
|             ) + 1} / span | ||||
|             ${Math.abs(config?.global_start_position - config?.global_end_position) + 1}; | ||||
|           grid-row-start: ${index + 1}; | ||||
|           cursor: ew-resize; | ||||
|           transform: translateX(${boxTranslateX}px); | ||||
|         `} | ||||
|         onMouseDown={handleMouseDown} | ||||
|       ></div> | ||||
|     </Fragment> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										86
									
								
								src/configurator/components/led-strip-editor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/configurator/components/led-strip-editor.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| import { HTMLAttributes, useCallback } from 'react'; | ||||
| import { FC } from 'react'; | ||||
| import { LedStripConfig } from '../models/led-strip-config'; | ||||
| import tw, { styled } from 'twin.macro'; | ||||
| import { faLeftRight, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; | ||||
| import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||||
|  | ||||
| export interface LedStripEditorProps | ||||
|   extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> { | ||||
|   config: LedStripConfig | null; | ||||
|   onChange?: (config: LedStripConfig | null) => void; | ||||
| } | ||||
|  | ||||
| const StyledContainer = styled.section( | ||||
|   tw`flex flex-wrap gap-2 self-start justify-self-start`, | ||||
| ); | ||||
|  | ||||
| const StyledButton = styled.button( | ||||
|   tw` | ||||
| bg-yellow-500 dark:bg-amber-600 rounded-full h-4 w-4 text-xs shadow select-none`, | ||||
|   tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600 active:dark:bg-amber-500`, | ||||
| ); | ||||
|  | ||||
| export const LedStripEditor: FC<LedStripEditorProps> = ({ | ||||
|   config, | ||||
|   onChange, | ||||
|   ...htmlAttrs | ||||
| }) => { | ||||
|   const addLed = useCallback(() => { | ||||
|     if (config) { | ||||
|       if (config.global_start_position <= config.global_end_position) { | ||||
|         onChange?.({ ...config, global_end_position: config.global_end_position + 1 }); | ||||
|       } else { | ||||
|         onChange?.({ | ||||
|           ...config, | ||||
|           global_start_position: config.global_start_position + 1, | ||||
|         }); | ||||
|       } | ||||
|     } else { | ||||
|       onChange?.(new LedStripConfig(0, 0, 0)); | ||||
|     } | ||||
|   }, [config, onChange]); | ||||
|   const removeLed = useCallback(() => { | ||||
|     if (!config) { | ||||
|       onChange?.(null); | ||||
|     } else if (Math.abs(config.global_start_position - config.global_end_position) <= 1) { | ||||
|       onChange?.(null); | ||||
|     } else { | ||||
|       if (config.global_start_position <= config.global_end_position) { | ||||
|         onChange?.({ ...config, global_end_position: config.global_end_position - 1 }); | ||||
|       } else { | ||||
|         onChange?.({ | ||||
|           ...config, | ||||
|           global_start_position: config.global_start_position - 1, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   }, [config, onChange]); | ||||
|   const reverse = useCallback(() => { | ||||
|     if (!config) { | ||||
|       return; | ||||
|     } | ||||
|     onChange?.({ | ||||
|       ...config, | ||||
|       global_start_position: config.global_end_position, | ||||
|       global_end_position: config.global_start_position, | ||||
|     }); | ||||
|   }, [config, onChange]); | ||||
|  | ||||
|   return ( | ||||
|     <StyledContainer {...htmlAttrs}> | ||||
|       <StyledButton title="Add LED" onClick={addLed}> | ||||
|         <FontAwesomeIcon icon={faPlus} /> | ||||
|       </StyledButton> | ||||
|       <StyledButton title="Remove LED" onClick={removeLed}> | ||||
|         <FontAwesomeIcon icon={faMinus} /> | ||||
|       </StyledButton> | ||||
|       <StyledButton title="Reverse" onClick={reverse}> | ||||
|         <FontAwesomeIcon icon={faLeftRight} /> | ||||
|       </StyledButton> | ||||
|       {`s: ${config?.global_start_position ?? 'x'}, e: ${ | ||||
|         config?.global_end_position ?? 'x' | ||||
|       }`} | ||||
|     </StyledContainer> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										28
									
								
								src/configurator/components/led-strip.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/configurator/components/led-strip.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | ||||
| import { HTMLAttributes, useMemo } from 'react'; | ||||
| import { FC } from 'react'; | ||||
| import { LedStripConfig } from '../models/led-strip-config'; | ||||
| import tw, { css, styled } from 'twin.macro'; | ||||
| import { splitEvery } from 'ramda'; | ||||
| import { StyledPixel } from './styled-pixel'; | ||||
|  | ||||
| export interface LedStripProps extends HTMLAttributes<HTMLElement> { | ||||
|   config: LedStripConfig | null; | ||||
|   colors: Uint8Array; | ||||
| } | ||||
|  | ||||
| const StyledContainer = styled.section( | ||||
|   tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center -mx-px -mt-px`, | ||||
|   css``, | ||||
| ); | ||||
|  | ||||
| export const LedStrip: FC<LedStripProps> = ({ config, colors, ...htmlAttrs }) => { | ||||
|   const pixels = useMemo(() => { | ||||
|     const pixels = splitEvery(3, Array.from(colors)) as Array<[number, number, number]>; | ||||
|     return pixels.map((rgb, index) => <StyledPixel key={index} rgb={rgb}></StyledPixel>); | ||||
|   }, [colors]); | ||||
|   return ( | ||||
|     <StyledContainer {...htmlAttrs} css={[!config && tw`bg-gray-200`]}> | ||||
|       {pixels} | ||||
|     </StyledContainer> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										10
									
								
								src/configurator/components/styled-pixel.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/configurator/components/styled-pixel.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import tw, { css, styled } from 'twin.macro'; | ||||
|  | ||||
| export const StyledPixel = styled.span( | ||||
|   ({ rgb: [r, g, b] }: { rgb: [number, number, number] }) => [ | ||||
|     tw`rounded-full h-3 w-3 bg-current block border border-gray-700`, | ||||
|     css` | ||||
|       color: rgb(${r}, ${g}, ${b}); | ||||
|     `, | ||||
|   ], | ||||
| ); | ||||
							
								
								
									
										123
									
								
								src/configurator/configurator.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/configurator/configurator.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| import { invoke } from '@tauri-apps/api'; | ||||
| import { FC, 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||||
| import { 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'; | ||||
|  | ||||
| const logger = debug('app:configurator'); | ||||
|  | ||||
| const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config'); | ||||
| const getScreenshotOfDisplays = () => | ||||
|   invoke<ScreenshotDto[]>('take_snapshot').then((items) => { | ||||
|     return items; | ||||
|   }); | ||||
| const getScreenshotByConfig = async (config: DisplayConfig) => { | ||||
|   return await invoke<ScreenshotDto>('get_screenshot_by_config', { | ||||
|     config, | ||||
|   }); | ||||
| }; | ||||
| const writePickerConfig = async (config: PickerConfiguration) => { | ||||
|   return await invoke<void>('write_picker_config', { | ||||
|     config, | ||||
|   }); | ||||
| }; | ||||
| const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretch`); | ||||
|  | ||||
| const StyledDisplayContainer = styled.section(tw`overflow-auto`); | ||||
|  | ||||
| const ConfiguratorInner: FC = () => { | ||||
|   const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync( | ||||
|     getPickerConfig, | ||||
|     [], | ||||
|   ); | ||||
|  | ||||
|   const { loading: pendingScreenshotOfDisplays, result: defaultScreenshotOfDisplays } = | ||||
|     useAsync(getScreenshotOfDisplays, []); | ||||
|  | ||||
|   const [screenshotOfDisplays, setScreenshotOfDisplays] = useState<ScreenshotDto[]>([]); | ||||
|  | ||||
|   const { loading: pendingGetLedColorsByConfig, execute: onDisplayConfigChange } = | ||||
|     useAsyncCallback(async (value: DisplayConfig) => { | ||||
|       const screenshot = await getScreenshotByConfig(value); | ||||
|       setScreenshotOfDisplays((old) => { | ||||
|         const index = old.findIndex((it) => it.config.id === screenshot.config.id); | ||||
|         const newValue = update(index, screenshot, old); | ||||
|         savedPickerConfig && | ||||
|           writePickerConfig({ | ||||
|             ...savedPickerConfig, | ||||
|             display_configs: newValue.map((it) => it.config), | ||||
|           }); | ||||
|         return newValue; | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|   const [displayConfigs, setDisplayConfigs] = useState<DisplayConfig[]>([]); | ||||
|  | ||||
|   useEffect(() => { | ||||
|     const displayConfigs = savedPickerConfig?.display_configs; | ||||
|     if (displayConfigs && defaultScreenshotOfDisplays) { | ||||
|       setDisplayConfigs(displayConfigs); | ||||
|       setScreenshotOfDisplays(defaultScreenshotOfDisplays); | ||||
|       (async () => { | ||||
|         for (const config of displayConfigs) { | ||||
|           await onDisplayConfigChange(config); | ||||
|         } | ||||
|       })().then(); | ||||
|     } | ||||
|   }, [savedPickerConfig, onDisplayConfigChange, defaultScreenshotOfDisplays]); | ||||
|   useEffect(() => {}, [defaultScreenshotOfDisplays]); | ||||
|  | ||||
|   const displays = useMemo(() => { | ||||
|     if (screenshotOfDisplays) { | ||||
|       return screenshotOfDisplays.map((screenshot, index) => ( | ||||
|         <DisplayWithLedStrips | ||||
|           key={index} | ||||
|           config={screenshot.config} | ||||
|           screenshot={screenshot} | ||||
|           onChange={(value) => onDisplayConfigChange(value)} | ||||
|         /> | ||||
|       )); | ||||
|     } | ||||
|   }, [displayConfigs, screenshotOfDisplays]); | ||||
|  | ||||
|   if (pendingPickerConfig || pendingScreenshotOfDisplays) { | ||||
|     return ( | ||||
|       <section> | ||||
|         等待 {JSON.stringify({ pendingPickerConfig, pendingScreenshotOfDisplays })} | ||||
|       </section> | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <StyledConfiguratorContainer> | ||||
|       <CompletedLedStrip | ||||
|         screenshots={screenshotOfDisplays} | ||||
|         onDisplayConfigChange={onDisplayConfigChange} | ||||
|       /> | ||||
|       <StyledDisplayContainer>{displays}</StyledDisplayContainer>; | ||||
|       <Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}> | ||||
|         <Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}> | ||||
|           This is a success message! | ||||
|         </Alert> | ||||
|       </Snackbar> | ||||
|     </StyledConfiguratorContainer> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const Configurator = () => { | ||||
|   return ( | ||||
|     <LedCountProvider> | ||||
|       <ConfiguratorInner /> | ||||
|     </LedCountProvider> | ||||
|   ); | ||||
| }; | ||||
							
								
								
									
										25
									
								
								src/configurator/contents/led-count.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/configurator/contents/led-count.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| import { | ||||
|   createContext, | ||||
|   Dispatch, | ||||
|   FC, | ||||
|   ReactNode, | ||||
|   SetStateAction, | ||||
|   useContext, | ||||
|   useState, | ||||
| } from 'react'; | ||||
|  | ||||
| interface LedCountContext { | ||||
|   ledCount: number; | ||||
|   setLedCount: Dispatch<SetStateAction<number>>; | ||||
| } | ||||
|  | ||||
| const Context = createContext<LedCountContext>(undefined as any); | ||||
|  | ||||
| export const LedCountProvider: FC<{ children: ReactNode }> = ({ children }) => { | ||||
|   const [ledCount, setLedCount] = useState(0); | ||||
|   return ( | ||||
|     <Context.Provider value={{ ledCount, setLedCount }}>{children}</Context.Provider> | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const useLedCount = () => useContext(Context); | ||||
							
								
								
									
										21
									
								
								src/configurator/models/display-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/configurator/models/display-config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { Borders } from '../../constants/border'; | ||||
| import { LedStripConfig } from "./led-strip-config"; | ||||
|  | ||||
| export class LedStripConfigOfBorders implements Record<Borders, LedStripConfig | null> { | ||||
|   constructor( | ||||
|     public top: LedStripConfig | null = null, | ||||
|     public bottom: LedStripConfig | null = null, | ||||
|     public left: LedStripConfig | null = null, | ||||
|     public right: LedStripConfig | null = null, | ||||
|   ) {} | ||||
| } | ||||
| export class DisplayConfig { | ||||
|   led_strip_of_borders = new LedStripConfigOfBorders(); | ||||
|  | ||||
|   constructor( | ||||
|     public id: number, | ||||
|     public index_of_display: number, | ||||
|     public display_width: number, | ||||
|     public display_height: number, | ||||
|   ) {} | ||||
| } | ||||
							
								
								
									
										7
									
								
								src/configurator/models/led-strip-config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/configurator/models/led-strip-config.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| export class LedStripConfig { | ||||
|   constructor( | ||||
|     public index: number, | ||||
|     public global_start_position: number, | ||||
|     public global_end_position: number, | ||||
|   ) {} | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/configurator/models/picker-configuration.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/configurator/models/picker-configuration.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import { DisplayConfig } from './display-config'; | ||||
|  | ||||
| export class PickerConfiguration { | ||||
|   constructor( | ||||
|     public display_configs: DisplayConfig[] = [], | ||||
|     public config_version: number = 1, | ||||
|   ) {} | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/configurator/models/pixel-rgb.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/configurator/models/pixel-rgb.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export type PixelRgb = [number, number, number]; | ||||
							
								
								
									
										12
									
								
								src/configurator/models/screenshot.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/configurator/models/screenshot.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { DisplayConfig } from './display-config'; | ||||
|  | ||||
| export class ScreenshotDto { | ||||
|   encode_image!: string; | ||||
|   config!: DisplayConfig; | ||||
|   colors!: { | ||||
|     top: Uint8Array; | ||||
|     bottom: Uint8Array; | ||||
|     left: Uint8Array; | ||||
|     right: Uint8Array; | ||||
|   }; | ||||
| } | ||||
							
								
								
									
										2
									
								
								src/constants/border.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/constants/border.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| export const borders = ['top', 'right', 'bottom', 'left'] as const; | ||||
| export type Borders = typeof borders[number]; | ||||
| @@ -1,10 +1,11 @@ | ||||
| import React from "react"; | ||||
| import ReactDOM from "react-dom/client"; | ||||
| import App from "./App"; | ||||
| import "./style.css"; | ||||
| import GlobalStyles from './styles/global-styles'; | ||||
|  | ||||
| ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( | ||||
|   <React.StrictMode> | ||||
|     <GlobalStyles /> | ||||
|     <App /> | ||||
|   </React.StrictMode>, | ||||
| ); | ||||
							
								
								
									
										20
									
								
								src/styles/global-styles.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/styles/global-styles.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| import React from 'react'; | ||||
| import { Global, css } from '@emotion/react'; | ||||
| import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro'; | ||||
|  | ||||
| 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`, | ||||
|   }, | ||||
| }); | ||||
|  | ||||
| const GlobalStyles = () => ( | ||||
|   <> | ||||
|     <BaseStyles /> | ||||
|     <Global styles={customStyles} /> | ||||
|   </> | ||||
| ); | ||||
|  | ||||
| export default GlobalStyles; | ||||
| @@ -1,12 +1,15 @@ | ||||
| /** @type {import('tailwindcss').Config} */ | ||||
| module.exports = { | ||||
|   content: [ | ||||
|     "./index.html", | ||||
|     "./src/**/*.{js,ts,jsx,tsx}", | ||||
|   ], | ||||
|   content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], | ||||
|   theme: { | ||||
|     extend: { | ||||
|       colors: { | ||||
|         dark: { | ||||
|           800: '#0f0f0f', | ||||
|           100: '#f6f6f6', | ||||
|         }, | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   plugins: [], | ||||
| } | ||||
| }; | ||||
|   | ||||
| @@ -14,8 +14,12 @@ | ||||
|     "resolveJsonModule": true, | ||||
|     "isolatedModules": true, | ||||
|     "noEmit": true, | ||||
|     "jsx": "react-jsx" | ||||
|     "jsx": "react-jsx", | ||||
|     "jsxImportSource": "@emotion/react" | ||||
|   }, | ||||
|   "include": ["src"], | ||||
|   "include": [ | ||||
|     "src", | ||||
|     "types" | ||||
|   ], | ||||
|   "references": [{ "path": "./tsconfig.node.json" }] | ||||
| } | ||||
|   | ||||
							
								
								
									
										18
									
								
								types/twin.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								types/twin.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | ||||
| import 'twin.macro'; | ||||
| import { css as cssImport } from '@emotion/react'; | ||||
| import styledImport from '@emotion/styled'; | ||||
| import { CSSInterpolation } from '@emotion/serialize'; | ||||
|  | ||||
| declare module 'twin.macro' { | ||||
|   // The styled and css imports | ||||
|   const styled: typeof styledImport; | ||||
|   const css: typeof cssImport; | ||||
| } | ||||
|  | ||||
| declare module 'react' { | ||||
|   // The tw and css prop | ||||
|   interface DOMAttributes<T> { | ||||
|     tw?: string; | ||||
|     css?: CSSInterpolation; | ||||
|   } | ||||
| } | ||||
| @@ -3,7 +3,29 @@ import react from "@vitejs/plugin-react"; | ||||
|  | ||||
| // https://vitejs.dev/config/ | ||||
| export default defineConfig({ | ||||
|   plugins: [react()], | ||||
|   optimizeDeps: { | ||||
|     esbuildOptions: { | ||||
|       target: 'es2020', | ||||
|     }, | ||||
|   }, | ||||
|   plugins: [ | ||||
|     react({ | ||||
|       babel: { | ||||
|         plugins: [ | ||||
|           'babel-plugin-macros', | ||||
|           [ | ||||
|             '@emotion/babel-plugin-jsx-pragmatic', | ||||
|             { | ||||
|               export: 'jsx', | ||||
|               import: '__cssprop', | ||||
|               module: '@emotion/react', | ||||
|             }, | ||||
|           ], | ||||
|           ['@babel/plugin-transform-react-jsx', { pragma: '__cssprop' }, 'twin.macro'], | ||||
|         ], | ||||
|       }, | ||||
|     }), | ||||
|   ], | ||||
|  | ||||
|   // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` | ||||
|   // prevent vite from obscuring rust errors | ||||
| @@ -15,12 +37,12 @@ export default defineConfig({ | ||||
|   }, | ||||
|   // to make use of `TAURI_DEBUG` and other env variables | ||||
|   // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand | ||||
|   envPrefix: ["VITE_", "TAURI_"], | ||||
|   envPrefix: ['VITE_', 'TAURI_'], | ||||
|   build: { | ||||
|     // Tauri supports es2021 | ||||
|     target: ["es2021", "chrome100", "safari13"], | ||||
|     target: ['es2021', 'chrome100', 'safari13'], | ||||
|     // don't minify for debug builds | ||||
|     minify: !process.env.TAURI_DEBUG ? "esbuild" : false, | ||||
|     minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, | ||||
|     // produce sourcemaps for debug builds | ||||
|     sourcemap: !!process.env.TAURI_DEBUG, | ||||
|   }, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user