feat: GUI 配置支持添加和减少 LED。

This commit is contained in:
Ivan Li 2023-01-02 16:53:20 +08:00
parent 4ad78ae5cc
commit 366b137258
21 changed files with 732 additions and 106 deletions

View File

@ -12,8 +12,14 @@
"dependencies": {
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@mui/material": "^5.11.1",
"@tauri-apps/api": "^1.1.0",
"clsx": "^1.2.1",
"ramda": "^0.28.0",
"react": "^18.2.0",
"react-async-hook": "^4.0.0",
"react-dom": "^18.2.0"
@ -24,6 +30,7 @@
"@emotion/serialize": "^1.1.1",
"@tauri-apps/cli": "^1.1.0",
"@types/node": "^18.7.10",
"@types/ramda": "^0.28.20",
"@types/react": "^18.0.15",
"@types/react-dom": "^18.0.6",
"@vitejs/plugin-react": "^2.0.0",

281
pnpm-lock.yaml generated
View File

@ -6,9 +6,15 @@ specifiers:
'@emotion/react': ^11.10.5
'@emotion/serialize': ^1.1.1
'@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.1
'@tauri-apps/api': ^1.1.0
'@tauri-apps/cli': ^1.1.0
'@types/node': ^18.7.10
'@types/ramda': ^0.28.20
'@types/react': ^18.0.15
'@types/react-dom': ^18.0.6
'@vitejs/plugin-react': ^2.0.0
@ -22,6 +28,7 @@ specifiers:
eslint-plugin-simple-import-sort: ^8.0.0
postcss: ^8.4.19
prettier: ^2.7.1
ramda: ^0.28.0
react: ^18.2.0
react-async-hook: ^4.0.0
react-dom: ^18.2.0
@ -33,8 +40,14 @@ specifiers:
dependencies:
'@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4
'@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu
'@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_z27bm67dtmuyyvss23ckjdrcuy
'@mui/material': 5.11.1_lskpmcsdi7ipu6qpuapyu56ihm
'@tauri-apps/api': 1.2.0
clsx: 1.2.1
ramda: 0.28.0
react: 18.2.0
react-async-hook: 4.0.0_react@18.2.0
react-dom: 18.2.0_react@18.2.0
@ -45,6 +58,7 @@ devDependencies:
'@emotion/serialize': 1.1.1
'@tauri-apps/cli': 1.2.2
'@types/node': 18.11.16
'@types/ramda': 0.28.20
'@types/react': 18.0.26
'@types/react-dom': 18.0.9
'@vitejs/plugin-react': 2.2.0_vite@3.2.5
@ -505,6 +519,47 @@ packages:
dev: true
optional: true
/@fortawesome/fontawesome-common-types/6.2.1:
resolution: {integrity: sha512-Sz07mnQrTekFWLz5BMjOzHl/+NooTdW8F8kDQxjWwbpOJcnoSg4vUDng8d/WR1wOxM0O+CY9Zw0nR054riNYtQ==}
engines: {node: '>=6'}
requiresBuild: true
dev: false
/@fortawesome/fontawesome-svg-core/6.2.1:
resolution: {integrity: sha512-HELwwbCz6C1XEcjzyT1Jugmz2NNklMrSPjZOWMlc+ZsHIVk+XOvOXLGGQtFBwSyqfJDNgRq4xBCwWOaZ/d9DEA==}
engines: {node: '>=6'}
requiresBuild: true
dependencies:
'@fortawesome/fontawesome-common-types': 6.2.1
dev: false
/@fortawesome/free-regular-svg-icons/6.2.1:
resolution: {integrity: sha512-wiqcNDNom75x+pe88FclpKz7aOSqS2lOivZeicMV5KRwOAeypxEYWAK/0v+7r+LrEY30+qzh8r2XDaEHvoLsMA==}
engines: {node: '>=6'}
requiresBuild: true
dependencies:
'@fortawesome/fontawesome-common-types': 6.2.1
dev: false
/@fortawesome/free-solid-svg-icons/6.2.1:
resolution: {integrity: sha512-oKuqrP5jbfEPJWTij4sM+/RvgX+RMFwx3QZCZcK9PrBDgxC35zuc7AOFsyMjMd/PIFPeB2JxyqDr5zs/DZFPPw==}
engines: {node: '>=6'}
requiresBuild: true
dependencies:
'@fortawesome/fontawesome-common-types': 6.2.1
dev: false
/@fortawesome/react-fontawesome/0.2.0_z27bm67dtmuyyvss23ckjdrcuy:
resolution: {integrity: sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6
react: '>=16.3'
dependencies:
'@fortawesome/fontawesome-svg-core': 6.2.1
prop-types: 15.8.1
react: 18.2.0
dev: false
/@jridgewell/gen-mapping/0.1.1:
resolution: {integrity: sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==}
engines: {node: '>=6.0.0'}
@ -543,6 +598,164 @@ packages:
'@jridgewell/sourcemap-codec': 1.4.14
dev: true
/@mui/base/5.0.0-alpha.111_ib3m5ricvtkl2cll7qpr2f6lvq:
resolution: {integrity: sha512-2wfIPpl97S4dPzD0QOM3UIzQ/EuXCYQvHmXxTpfKxev/cfkzOe7Ik/McoYUBbtM1bSOqH3W276R/L2LF9cyXqQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.6
'@emotion/is-prop-valid': 1.2.0
'@mui/types': 7.2.3_@types+react@18.0.26
'@mui/utils': 5.11.1_react@18.2.0
'@popperjs/core': 2.11.6
'@types/react': 18.0.26
clsx: 1.2.1
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-is: 18.2.0
dev: false
/@mui/core-downloads-tracker/5.11.1:
resolution: {integrity: sha512-QVqVNlZ2K+LqUDE5kFgYd0r4KekR/dv2cNYbAutQWbfOA8VPVUVrDz0ELrEcoe8TjM/CwnsmGvaDh/YSNl/ALA==}
dev: false
/@mui/material/5.11.1_lskpmcsdi7ipu6qpuapyu56ihm:
resolution: {integrity: sha512-yaZiXvcrl2vgUK+VO24780BWRgwdAMmAyuMVZnRTts1Yu0tWd6PjIYq2ZtaOlpj6/LbaSS+Q2kSfxYnDQ20CEQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
react-dom: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.6
'@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4
'@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu
'@mui/base': 5.0.0-alpha.111_ib3m5ricvtkl2cll7qpr2f6lvq
'@mui/core-downloads-tracker': 5.11.1
'@mui/system': 5.11.1_ogriz7mfahdh34qnfautfro5yu
'@mui/types': 7.2.3_@types+react@18.0.26
'@mui/utils': 5.11.1_react@18.2.0
'@types/react': 18.0.26
'@types/react-transition-group': 4.4.5
clsx: 1.2.1
csstype: 3.1.1
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
react-is: 18.2.0
react-transition-group: 4.4.5_biqbaboplfbrettd7655fr4n2y
dev: false
/@mui/private-theming/5.11.1_kzbn2opkn2327fwg5yzwzya5o4:
resolution: {integrity: sha512-nnHg7kA5RwFRhy0wiDYe59sLCVGORpPypL1JcEdhv0+N0Zbmc2E/y4z2zqMRZ62MAEscpro7cQbvv244ThA84A==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.6
'@mui/utils': 5.11.1_react@18.2.0
'@types/react': 18.0.26
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/styled-engine/5.11.0_dovxhg2tvkkxkdnqyoum6wzcxm:
resolution: {integrity: sha512-AF06K60Zc58qf0f7X+Y/QjaHaZq16znliLnGc9iVrV/+s8Ln/FCoeNuFvhlCbZZQ5WQcJvcy59zp0nXrklGGPQ==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.4.1
'@emotion/styled': ^11.3.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
dependencies:
'@babel/runtime': 7.20.6
'@emotion/cache': 11.10.5
'@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4
'@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu
csstype: 3.1.1
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/system/5.11.1_ogriz7mfahdh34qnfautfro5yu:
resolution: {integrity: sha512-BEA2S0hay8n8CcZftkeAVsi0nsb5ZjdnZRCahv5lX7QJYwDjO4ucJ6lnvxHe2v/9Te1LLjTO7ojxu/qM6CE5Cg==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@emotion/react': ^11.5.0
'@emotion/styled': ^11.3.0
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@emotion/react':
optional: true
'@emotion/styled':
optional: true
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.20.6
'@emotion/react': 11.10.5_kzbn2opkn2327fwg5yzwzya5o4
'@emotion/styled': 11.10.5_qvatmowesywn4ye42qoh247szu
'@mui/private-theming': 5.11.1_kzbn2opkn2327fwg5yzwzya5o4
'@mui/styled-engine': 5.11.0_dovxhg2tvkkxkdnqyoum6wzcxm
'@mui/types': 7.2.3_@types+react@18.0.26
'@mui/utils': 5.11.1_react@18.2.0
'@types/react': 18.0.26
clsx: 1.2.1
csstype: 3.1.1
prop-types: 15.8.1
react: 18.2.0
dev: false
/@mui/types/7.2.3_@types+react@18.0.26:
resolution: {integrity: sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw==}
peerDependencies:
'@types/react': '*'
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@types/react': 18.0.26
dev: false
/@mui/utils/5.11.1_react@18.2.0:
resolution: {integrity: sha512-lMAPgIJoil8V9ZxsMbEflMsvZmWcHbRVMc4JDY9jPO9V4welpF43h/O267b1RqlcRnC5MEbVQV605GYkTZY29Q==}
engines: {node: '>=12.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.20.6
'@types/prop-types': 15.7.5
'@types/react-is': 17.0.3
prop-types: 15.8.1
react: 18.2.0
react-is: 18.2.0
dev: false
/@nodelib/fs.scandir/2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -564,6 +777,10 @@ packages:
fastq: 1.14.0
dev: true
/@popperjs/core/2.11.6:
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
dev: false
/@tauri-apps/api/1.2.0:
resolution: {integrity: sha512-lsI54KI6HGf7VImuf/T9pnoejfgkNoXveP14pVV7XarrQ46rOejIVJLFqHI9sRReJMGdh2YuCoI3cc/yCWCsrw==}
engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
@ -680,12 +897,30 @@ packages:
/@types/prop-types/15.7.5:
resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==}
/@types/ramda/0.28.20:
resolution: {integrity: sha512-MeUhzGSXQTRsY19JGn5LIBTLxVEnyF6HDNr08KSJqybsm4DlfLIgK1jBHjhpiSyk252tXYmp+UOe0UFg0UiFsA==}
dependencies:
ts-toolbelt: 6.15.5
dev: true
/@types/react-dom/18.0.9:
resolution: {integrity: sha512-qnVvHxASt/H7i+XG1U1xMiY5t+IHcPGUK7TDMDzom08xa7e86eCeKOiLZezwCKVxJn6NEiiy2ekgX8aQssjIKg==}
dependencies:
'@types/react': 18.0.26
dev: true
/@types/react-is/17.0.3:
resolution: {integrity: sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw==}
dependencies:
'@types/react': 18.0.26
dev: false
/@types/react-transition-group/4.4.5:
resolution: {integrity: sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA==}
dependencies:
'@types/react': 18.0.26
dev: false
/@types/react/18.0.26:
resolution: {integrity: sha512-hCR3PJQsAIXyxhTNSiDFY//LhnMZWpNNr5etoCqx/iUfGc5gXWtQR2Phl908jVR6uPXacojQWTg4qRpkxTuGug==}
dependencies:
@ -1039,6 +1274,13 @@ packages:
esutils: 2.0.3
dev: true
/dom-helpers/5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
'@babel/runtime': 7.20.6
csstype: 3.1.1
dev: false
/electron-to-chromium/1.4.284:
resolution: {integrity: sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==}
dev: true
@ -1840,6 +2082,11 @@ packages:
engines: {node: '>=0.10.0'}
dev: true
/object-assign/4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
dev: false
/object-hash/3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
@ -1992,6 +2239,14 @@ packages:
hasBin: true
dev: true
/prop-types/15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
dev: false
/queue-microtask/1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
dev: true
@ -2001,6 +2256,10 @@ packages:
engines: {node: '>=10'}
dev: true
/ramda/0.28.0:
resolution: {integrity: sha512-9QnLuG/kPVgWvMQ4aODhsBUFKOUmnbUnsSXACv+NCQZcHbeb+v8Lodp8OVxtRULN1/xOyYLLaL6npE6dMq5QTA==}
dev: false
/react-async-hook/4.0.0_react@18.2.0:
resolution: {integrity: sha512-97lgjFkOcHCTYSrsKBpsXg3iVWM0LnzedB749iP76sb3/8Ouu4nHIkCLEOrQWHVYqrYxjF05NN6GHoXWFkB3Kw==}
engines: {node: '>=8', npm: '>=5'}
@ -2024,11 +2283,29 @@ packages:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
/react-is/18.2.0:
resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==}
dev: false
/react-refresh/0.14.0:
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==}
engines: {node: '>=0.10.0'}
dev: true
/react-transition-group/4.4.5_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
peerDependencies:
react: '>=16.6.0'
react-dom: '>=16.6.0'
dependencies:
'@babel/runtime': 7.20.6
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
dev: false
/react/18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@ -2221,6 +2498,10 @@ packages:
is-number: 7.0.0
dev: true
/ts-toolbelt/6.15.5:
resolution: {integrity: sha512-FZIXf1ksVyLcfr7M317jbB67XFJhOO1YqdTcuGaq9q5jLUoTikukZ+98TPjKiP2jC5CgmYdWWYs0s2nLSU0/1A==}
dev: true
/tsconfig-paths/3.14.1:
resolution: {integrity: sha512-fxDhWnFSLt3VuTwtvJt5fpwxBHg5AdKWMsgcPOOIilyjymcYVZoCQF8fvFRezCNfblEXmi+PcM1eYHeOAgXCOQ==}
dependencies:

View File

@ -1,9 +1,9 @@
use futures::{future::join_all, stream::FuturesUnordered, StreamExt};
use futures::future::join_all;
use once_cell::sync::OnceCell;
use paris::info;
use serde::{Deserialize, Serialize};
use serde_json::value::Index;
use std::{collections::HashMap, iter::Map, sync::Arc, thread, time::Duration};
use std::{collections::HashMap, sync::Arc, time::Duration};
use tauri::async_runtime::RwLock;
use tokio::{
join,

View File

@ -11,6 +11,7 @@ mod rpc;
use crate::core::AmbientLightMode;
use crate::core::CoreManager;
use paris::*;
use picker::config::DisplayConfig;
use picker::manager::Picker;
use picker::screenshot::ScreenshotDto;
use std::vec;
@ -35,6 +36,26 @@ async fn take_snapshot() -> Vec<ScreenshotDto> {
base64_bitmap_list
}
#[tauri::command]
async fn get_screenshot_by_config(config: DisplayConfig) -> Result<ScreenshotDto, String> {
info!("Hi");
let manager = Picker::global();
let start = time::Instant::now();
let screenshot_dto = manager.get_screenshot_by_config(config).await;
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
match screenshot_dto {
Ok(screenshot_dto) => {
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
Ok(screenshot_dto)
}
Err(error) => {
error!("get_screenshot_by_config failed. {}", error);
Err(format!("get_screenshot_by_config failed. {}", error))
}
}
}
#[tauri::command]
fn get_picker_config() -> picker::config::Configuration {
let configuration = picker::config::Manager::global().get_config();
@ -57,6 +78,7 @@ async fn main() {
take_snapshot,
play_mode,
get_picker_config,
get_screenshot_by_config,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -9,6 +9,7 @@ pub struct LedStripConfig {
#[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,
@ -19,8 +20,9 @@ pub struct DisplayConfig {
}
impl DisplayConfig {
pub fn default(index_of_display: usize, display_width: usize, display_height: usize) -> Self {
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,

View File

@ -2,7 +2,7 @@ use futures::{stream::FuturesUnordered, StreamExt};
use once_cell::sync::OnceCell;
use paris::info;
use scrap::Display;
use std::{borrow::Borrow, sync::Arc};
use std::sync::Arc;
use tokio::{sync::Mutex, task};
use crate::picker::{config, screen::Screen};
@ -44,7 +44,7 @@ impl Picker {
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);
}
@ -76,4 +76,16 @@ impl Picker {
anyhow::Ok(screenshot.to_dto().await)
}
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)
}
}

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

View File

@ -51,7 +51,7 @@ impl Screenshot {
}
};
let bottom: Vec<LedSamplePoints> = match config.top_led_strip {
let bottom: Vec<LedSamplePoints> = match config.bottom_led_strip {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_height / 9,
@ -76,7 +76,7 @@ impl Screenshot {
}
};
let left: Vec<LedSamplePoints> = match config.top_led_strip {
let left: Vec<LedSamplePoints> = match config.left_led_strip {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_width / 16,
@ -98,7 +98,7 @@ impl Screenshot {
}
};
let right: Vec<LedSamplePoints> = match config.top_led_strip {
let right: Vec<LedSamplePoints> = match config.right_led_strip {
Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points(
config.display_width / 16,
@ -113,7 +113,7 @@ impl Screenshot {
.map(|groups| -> Vec<Point> {
groups
.into_iter()
.map(|(x, y)| (y, config.display_width - x))
.map(|(x, y)| (config.display_width - y, x))
.collect()
})
.collect()
@ -145,14 +145,15 @@ impl Screenshot {
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 < (width as f64)).then_some(next)
(next < (length as f64)).then_some(next)
})
.map(|i| i as usize)
.collect();
let points: Vec<Point> = point_x_list
.into_iter()
.zip(point_y_list.into_iter())
.iter()
.map(|&x| point_y_list.iter().map(move |&y| (x, y)))
.flatten()
.collect();
points
.chunks(single_axis_points * single_axis_points)

View File

@ -1,9 +1,10 @@
import { useCallback, useEffect, useState } from 'react';
import reactLogo from './assets/react.svg';
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;
@ -39,63 +40,30 @@ 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={() => readPickerConfig()}
>
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">
<div tw="flex gap-5 justify-center">
<Configurator />
</div>
</div>

View 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`,
]);

View File

@ -1,25 +1,97 @@
import { HTMLAttributes } from 'react';
import { HTMLAttributes, useCallback, useMemo } from 'react';
import { FC } from 'react';
import { DisplayConfig } 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';
export interface DisplayWithLedStripsProps extends HTMLAttributes<HTMLElement> {
export interface DisplayWithLedStripsProps
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
config: DisplayConfig;
screenshot: string;
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/png;base64,${screenshot.encode_image}`,
[screenshot.encode_image],
);
const onLedStripConfigChange = useCallback(
(
position:
| 'top_led_strip'
| 'left_led_strip'
| 'right_led_strip'
| 'bottom_led_strip',
value: LedStripConfig | null,
) => {
const c = { ...config, [position]: value };
onChange?.(c);
},
[config],
);
return (
<section className="m-4 grid grid-rows-3 grid-cols-3 gr" {...htmlAttrs}>
<img src={screenshot} className="row-start-2 col-start-2" />
<LedStrip config={config.top_led_strip} className="row-start-1 col-start-2 h-1" />
<LedStrip config={config.left_led_strip} className="row-start-2 col-start-1 w-1" />
<LedStrip config={config.right_led_strip} className="row-start-2 col-start-3" />
<LedStrip config={config.bottom_led_strip} className="row-start-3 col-start-2" />
</section>
<StyledContainer {...htmlAttrs}>
<img src={screenshotUrl} tw="row-start-3 col-start-3" />
<LedStrip
config={config.top_led_strip}
colors={screenshot.colors.top}
tw="row-start-2 col-start-3"
/>
<LedStrip
config={config.left_led_strip}
colors={screenshot.colors.left}
tw="row-start-3 col-start-2"
/>
<LedStrip
config={config.right_led_strip}
colors={screenshot.colors.right}
tw="row-start-3 col-start-4"
/>
<LedStrip
config={config.bottom_led_strip}
colors={screenshot.colors.bottom}
tw="row-start-4 col-start-3"
/>
<LedStripEditor
config={config.top_led_strip}
tw="row-start-1 col-start-3"
onChange={(value) => onLedStripConfigChange('top_led_strip', value)}
/>
<LedStripEditor
config={config.left_led_strip}
tw="row-start-3 col-start-1"
onChange={(value) => onLedStripConfigChange('left_led_strip', value)}
/>
<LedStripEditor
config={config.right_led_strip}
tw="row-start-3 col-start-5"
onChange={(value) => onLedStripConfigChange('right_led_strip', value)}
/>
<LedStripEditor
config={config.bottom_led_strip}
tw="row-start-5 col-start-3"
onChange={(value) => onLedStripConfigChange('bottom_led_strip', value)}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,57 @@
import { HTMLAttributes, useCallback } from 'react';
import { FC } from 'react';
import { LedStripConfig } from '../models/led-strip-config';
import tw, { css, styled, theme } 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 rounded-full h-4 w-4 text-xs shadow select-none`,
tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600`,
);
export const LedStripEditor: FC<LedStripEditorProps> = ({
config,
onChange,
...htmlAttrs
}) => {
const addLed = useCallback(() => {
if (config) {
onChange?.({ ...config, global_end_position: config.global_end_position + 1 });
} else {
onChange?.(new LedStripConfig(0, 0, 1));
}
}, [config, onChange]);
const removeLed = useCallback(() => {
if (!config) {
onChange?.(null);
} else {
onChange?.({ ...config, global_end_position: config.global_end_position - 1 });
}
}, [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">
<FontAwesomeIcon icon={faLeftRight} />
</StyledButton>
</StyledContainer>
);
};

View File

@ -1,11 +1,36 @@
import { HTMLAttributes } from 'react';
import { HTMLAttributes, useMemo } from 'react';
import { FC } from 'react';
import { LedStripConfig } from '../models/led-strip-config';
import tw, { css, styled, theme } from 'twin.macro';
import { splitEvery } from 'ramda';
export interface LedStripProps extends HTMLAttributes<HTMLElement> {
config: LedStripConfig;
config: LedStripConfig | null;
colors: Uint8Array;
}
export const LedStrip: FC<LedStripProps> = ({ config, ...htmlAttrs }) => {
return <section {...htmlAttrs}>...</section>;
const StyledContainer = styled.section(
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center`,
css``,
);
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});
`,
],
);
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>
);
};

View File

@ -1,46 +1,96 @@
import { invoke } from '@tauri-apps/api';
import { FC, useMemo } from 'react';
import { useAsync } from 'react-async-hook';
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import tw 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';
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
const getScreenshotOfDisplays = () =>
invoke<string[]>('take_snapshot').then((items) =>
items?.map((it) => `data:image/webp;base64,${it}`),
);
invoke<ScreenshotDto[]>('take_snapshot').then((items) => {
console.log(items);
return items;
});
const getScreenshotByConfig = async (config: DisplayConfig) => {
return await invoke<ScreenshotDto>('get_screenshot_by_config', {
config,
});
};
export const Configurator: FC = () => {
const { loading: pendingPickerConfig, result: pickerConfig } = useAsync(
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
getPickerConfig,
[],
);
const { loading: pendingScreenshotOfDisplays, result: screenshotOfDisplays } = useAsync(
getScreenshotOfDisplays,
[],
);
const { loading: pendingScreenshotOfDisplays, result: defaultScreenshotOfDisplays } =
useAsync(getScreenshotOfDisplays, []);
const [screenshotOfDisplays, setScreenshotOfDisplays] = useState<ScreenshotDto[]>([]);
const { loading: pendingGetLedColorsByConfig, execute: onPickerChange } =
useAsyncCallback(async (value: DisplayConfig) => {
console.log(value);
const screenshot = await getScreenshotByConfig(value);
setScreenshotOfDisplays((old) => {
const index = old.findIndex((it) => it.config.id === screenshot.config.id);
console.log({ old, n: update(index, screenshot, old) });
return update(index, screenshot, old);
});
console.log('screenshot', screenshot);
});
const [displayConfigs, setDisplayConfigs] = useState<DisplayConfig[]>([]);
useEffect(() => {
const displayConfigs = savedPickerConfig?.display_configs;
if (displayConfigs) {
setDisplayConfigs(displayConfigs);
}
}, [savedPickerConfig]);
useEffect(() => {
if (defaultScreenshotOfDisplays) {
setScreenshotOfDisplays(defaultScreenshotOfDisplays);
}
}, [defaultScreenshotOfDisplays]);
const displays = useMemo(() => {
if (pickerConfig && screenshotOfDisplays) {
if (screenshotOfDisplays) {
console.log({ c: screenshotOfDisplays });
return screenshotOfDisplays.map((screenshot, index) => (
<DisplayWithLedStrips
key={index}
config={pickerConfig.display_configs[index] ?? {}}
config={screenshot.config}
screenshot={screenshot}
onChange={(value) => onPickerChange(value)}
/>
));
}
}, [pickerConfig, screenshotOfDisplays]);
}, [displayConfigs, screenshotOfDisplays]);
if (pendingPickerConfig || pendingScreenshotOfDisplays) {
return (
<section>
{JSON.stringify({ pendingPickerConfig, pendingScreenshotOfDisplays })}
{displays}
</section>
);
}
return <section>{displays}</section>;
return (
<Fragment>
<section>{displays}</section>;
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
This is a success message!
</Alert>
</Snackbar>
</Fragment>
);
};

View File

@ -1,11 +1,15 @@
import { LedStripConfig } from './led-strip-config';
export class DisplayConfig {
index_of_display!: number;
display_width!: number;
display_height!: number;
top_led_strip!: LedStripConfig;
bottom_led_strip!: LedStripConfig;
left_led_strip!: LedStripConfig;
right_led_strip!: LedStripConfig;
top_led_strip: LedStripConfig | null = null;
bottom_led_strip: LedStripConfig | null = null;
left_led_strip: LedStripConfig | null = null;
right_led_strip: LedStripConfig | null = null;
constructor(
public id: number,
public index_of_display: number,
public display_width: number,
public display_height: number,
) {}
}

View File

@ -1,5 +1,7 @@
export class LedStripConfig {
index!: number;
global_start_position!: number;
global_end_position!: number;
constructor(
public index: number,
public global_start_position: number,
public global_end_position: number,
) {}
}

View File

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

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

View File

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

View File

@ -5,7 +5,8 @@ import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro';
const customStyles = css({
body: {
WebkitTapHighlightColor: theme`colors.purple.500`,
...tw`antialiased dark:bg-dark-800 bg-dark-100`,
...tw`antialiased`,
...tw`dark:bg-dark-800 bg-dark-100 dark:text-gray-100 text-gray-800`,
},
});

View File

@ -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: [],
}
};