Compare commits
No commits in common. "master" and "86d4ab6e6adae9191beb036ae90da092d7e7f078" have entirely different histories.
master
...
86d4ab6e6a
@ -1,7 +0,0 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
node_modules/*
|
||||
src-tauri
|
@ -1,52 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ['src'],
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx'],
|
||||
},
|
||||
},
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
amd: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
'plugin:prettier/recommended', // Make sure this is always the last element in the array.
|
||||
],
|
||||
plugins: ['simple-import-sort', 'prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'jsx-a11y/accessible-emoji': 'off',
|
||||
'react/prop-types': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'simple-import-sort/imports': 'error',
|
||||
'simple-import-sort/exports': 'error',
|
||||
'jsx-a11y/anchor-is-valid': [
|
||||
'error',
|
||||
{
|
||||
components: ['Link'],
|
||||
specialLink: ['hrefLeft', 'hrefRight'],
|
||||
aspects: ['invalidHref', 'preferButton'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
22
.gitignore
vendored
22
.gitignore
vendored
@ -1,24 +1,2 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"files.autoSave": "onWindowChange"
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
# Tauri + React + Typescript
|
||||
# Tauri + Solid + Typescript
|
||||
|
||||
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||
This template should help get you started developing with Tauri, Solid and Typescript in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
twin: {
|
||||
preset: 'emotion',
|
||||
},
|
||||
};
|
13
index.html
13
index.html
@ -1,14 +1,17 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tauri + React + TS</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
||||
<title>Tauri + Solid + Typescript App</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
|
||||
<script src="/src/index.tsx" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
53
package.json
53
package.json
@ -1,54 +1,27 @@
|
||||
{
|
||||
"name": "display-ambient-light-desktop",
|
||||
"private": true,
|
||||
"name": "test-demo",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"license": "MIT",
|
||||
"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.4",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"clsx": "^1.2.1",
|
||||
"debug": "^4.3.4",
|
||||
"notistack": "^2.0.8",
|
||||
"ramda": "^0.28.0",
|
||||
"react": "^18.2.0",
|
||||
"react-async-hook": "^4.0.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"solid-js": "^1.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"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",
|
||||
"@types/node": "^18.7.10",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.8.3",
|
||||
"tailwindcss": "^3.2.4",
|
||||
"twin.macro": "^3.1.0",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^3.2.5"
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^4.7.4",
|
||||
"vite": "^4.0.0",
|
||||
"vite-plugin-solid": "^2.3.0"
|
||||
}
|
||||
}
|
||||
|
2364
pnpm-lock.yaml
2364
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
1914
src-tauri/Cargo.lock
generated
1914
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,49 +1,33 @@
|
||||
[package]
|
||||
name = "display-ambient-light-desktop"
|
||||
name = "test-demo"
|
||||
version = "0.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
rust-version = "1.57"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "1.1", features = [] }
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "1.2", features = ["shell-open"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
tauri = { version = "1.1", features = ["api-all"] }
|
||||
scrap = "0.5"
|
||||
bmp = "0.5.0"
|
||||
webp = "0.2.2"
|
||||
base64 = "0.13.1"
|
||||
anyhow = "1.0.66"
|
||||
once_cell = "1.16.0"
|
||||
base64 = "0.21.0"
|
||||
core-graphics = "0.22.3"
|
||||
display-info = "0.4.1"
|
||||
png = "0.17.7"
|
||||
anyhow = "1.0.69"
|
||||
tokio = {version = "1.26.0", features = ["full"] }
|
||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||
tokio = { version = "1.22.0", features = ["full"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
hex = "0.4.3"
|
||||
rumqttc = "0.17.0"
|
||||
time = { version = "0.3.17", features = ["formatting"] }
|
||||
color_space = "0.5.3"
|
||||
futures = "0.3.25"
|
||||
either = "1.8.0"
|
||||
image = "0.24.5"
|
||||
mdns = "3.0.0"
|
||||
macos-app-nap = "0.0.1"
|
||||
log = "0.4.17"
|
||||
env_logger = "0.10.0"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||
default = [ "custom-protocol" ]
|
||||
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||
# DO NOT remove this
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
# DO NOT REMOVE!!
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[dev-dependencies]
|
||||
test_dir = "0.2.0"
|
||||
|
@ -1,238 +0,0 @@
|
||||
use futures::future::join_all;
|
||||
use once_cell::sync::OnceCell;
|
||||
use paris::{error, info, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use tauri::async_runtime::RwLock;
|
||||
use tokio::{
|
||||
sync::mpsc,
|
||||
time::{sleep, Instant},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
picker::{
|
||||
self, config::DisplayConfig, display_picker::DisplayPicker, led_color::LedColor,
|
||||
screenshot::Screenshot,
|
||||
},
|
||||
rpc,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
|
||||
pub enum AmbientLightMode {
|
||||
None,
|
||||
Follow,
|
||||
Flowing,
|
||||
}
|
||||
|
||||
pub struct CoreManager {
|
||||
ambient_light_mode: Arc<RwLock<AmbientLightMode>>,
|
||||
}
|
||||
|
||||
impl CoreManager {
|
||||
pub fn global() -> &'static CoreManager {
|
||||
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
|
||||
|
||||
let core = CORE_MANAGER.get_or_init(|| CoreManager {
|
||||
ambient_light_mode: Arc::new(RwLock::new(AmbientLightMode::None)),
|
||||
});
|
||||
|
||||
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) {
|
||||
let mut hue = 0f64;
|
||||
let step_length = 2.0;
|
||||
loop {
|
||||
let lock = self.ambient_light_mode.read().await;
|
||||
if let AmbientLightMode::Flowing = *lock {
|
||||
let mut colors = Vec::<LedColor>::new();
|
||||
for i in 0..60 {
|
||||
let color =
|
||||
LedColor::from_hsv((hue + i as f64 * step_length) % 360.0, 1.0, 0.5);
|
||||
colors.push(color);
|
||||
}
|
||||
hue = (hue + 1.0) % 360.0;
|
||||
match rpc::manager::Manager::global()
|
||||
.publish_led_colors(&colors)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
warn!("publish led colors failed. {}", error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn play_follow(&self) -> anyhow::Result<()> {
|
||||
macos_app_nap::prevent();
|
||||
let mut futs = vec![];
|
||||
let configs = picker::config::Manager::global().reload_config().await;
|
||||
let configs = match configs {
|
||||
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);
|
||||
|
||||
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 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(())
|
||||
}
|
||||
|
||||
async fn follow_display_by_config(
|
||||
config: DisplayConfig,
|
||||
tx: mpsc::Sender<Screenshot>,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut picker = DisplayPicker::from_config(config)?;
|
||||
info!("width: {}", picker.config.display_width);
|
||||
|
||||
loop {
|
||||
let start = Instant::now();
|
||||
let next_tick = start + Duration::from_millis(16);
|
||||
let lock = Self::global().ambient_light_mode.read().await;
|
||||
if let AmbientLightMode::Follow = *lock {
|
||||
drop(lock);
|
||||
let screenshot = picker.take_screenshot()?;
|
||||
// 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;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
mod core;
|
||||
|
||||
pub use self::core::*;
|
0
src-tauri/src/logger.rs
Normal file
0
src-tauri/src/logger.rs
Normal file
@ -1,102 +1,169 @@
|
||||
#![cfg_attr(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
#![feature(bool_to_option)]
|
||||
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
mod core;
|
||||
mod picker;
|
||||
mod rpc;
|
||||
pub mod screenshot;
|
||||
mod screenshot_manager;
|
||||
|
||||
use crate::core::AmbientLightMode;
|
||||
use crate::core::CoreManager;
|
||||
use paris::*;
|
||||
use picker::config::DisplayConfig;
|
||||
use picker::manager::Picker;
|
||||
use picker::screenshot::ScreenshotDto;
|
||||
use tauri::async_runtime::Mutex;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
static GET_SCREENSHOT_LOCK: OnceCell<Mutex<bool>> = OnceCell::new();
|
||||
|
||||
#[tauri::command]
|
||||
async fn take_snapshot() -> Vec<ScreenshotDto> {
|
||||
info!("Hi?");
|
||||
let _lock = GET_SCREENSHOT_LOCK.get_or_init(|| Mutex::new(false)).lock().await;
|
||||
info!("Hi!");
|
||||
let manager = Picker::global().await;
|
||||
|
||||
let start = time::Instant::now();
|
||||
let base64_bitmap_list = match manager.list_displays().await {
|
||||
Ok(base64_bitmap_list) => {
|
||||
info!("screenshots len: {}", base64_bitmap_list.len());
|
||||
base64_bitmap_list
|
||||
}
|
||||
Err(error) => {
|
||||
error!("can not take screenshots for all. {}", error);
|
||||
|
||||
vec![]
|
||||
}
|
||||
use base64::Engine;
|
||||
use core_graphics::display::{
|
||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||
};
|
||||
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
|
||||
base64_bitmap_list
|
||||
use display_info::DisplayInfo;
|
||||
use paris::error;
|
||||
use screenshot_manager::ScreenshotManager;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_string;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(remote = "DisplayInfo")]
|
||||
struct DisplayInfoDef {
|
||||
pub id: u32,
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub rotation: f32,
|
||||
pub scale_factor: f32,
|
||||
pub is_primary: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DisplayInfoWrapper<'a>(#[serde(with = "DisplayInfoDef")] &'a DisplayInfo);
|
||||
|
||||
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
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))
|
||||
}
|
||||
}
|
||||
fn list_display_info() -> Result<String, String> {
|
||||
let displays = display_info::DisplayInfo::all().map_err(|e| {
|
||||
error!("can not list display info: {}", e);
|
||||
e.to_string()
|
||||
})?;
|
||||
let displays: Vec<DisplayInfoWrapper> =
|
||||
displays.iter().map(|v| DisplayInfoWrapper(v)).collect();
|
||||
let json_str = to_string(&displays).map_err(|e| {
|
||||
error!("can not list display info: {}", e);
|
||||
e.to_string()
|
||||
})?;
|
||||
Ok(json_str)
|
||||
}
|
||||
|
||||
#[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))
|
||||
fn take_screenshot(display_id: u32, scale_factor: f32) -> Result<String, String> {
|
||||
let exec = || {
|
||||
println!("take_screenshot");
|
||||
let start_at = std::time::Instant::now();
|
||||
|
||||
let cg_display = CGDisplay::new(display_id);
|
||||
let cg_image = CGDisplay::screenshot(
|
||||
cg_display.bounds(),
|
||||
kCGWindowListOptionOnScreenOnly,
|
||||
kCGNullWindowID,
|
||||
kCGWindowImageDefault,
|
||||
)
|
||||
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
|
||||
// println!("take screenshot took {}ms", start_at.elapsed().as_millis());
|
||||
|
||||
let buffer = cg_image.data();
|
||||
let bytes_per_row = cg_image.bytes_per_row() as f32;
|
||||
|
||||
let height = cg_image.height();
|
||||
let width = cg_image.width();
|
||||
|
||||
let image_height = (height as f32 / scale_factor) as u32;
|
||||
let image_width = (width as f32 / scale_factor) as u32;
|
||||
|
||||
// println!(
|
||||
// "raw image: {}x{}, output image: {}x{}",
|
||||
// width, height, image_width, image_height
|
||||
// );
|
||||
// // from bitmap vec
|
||||
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
|
||||
|
||||
for y in 0..image_height {
|
||||
for x in 0..image_width {
|
||||
let offset =
|
||||
(((y as f32) * bytes_per_row + (x as f32) * 4.0) * scale_factor) as usize;
|
||||
let b = buffer[offset];
|
||||
let g = buffer[offset + 1];
|
||||
let r = buffer[offset + 2];
|
||||
let offset = (y * image_width + x) as usize;
|
||||
image_buffer[offset * 3] = r;
|
||||
image_buffer[offset * 3 + 1] = g;
|
||||
image_buffer[offset * 3 + 2] = b;
|
||||
}
|
||||
}
|
||||
println!(
|
||||
"convert to image buffer took {}ms",
|
||||
start_at.elapsed().as_millis()
|
||||
);
|
||||
|
||||
// to png image
|
||||
// let mut image_png = Vec::new();
|
||||
// let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
|
||||
// encoder.set_color(png::ColorType::Rgb);
|
||||
// encoder.set_depth(png::BitDepth::Eight);
|
||||
|
||||
// let mut writer = encoder
|
||||
// .write_header()
|
||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
||||
// writer
|
||||
// .write_image_data(&image_buffer)
|
||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
||||
// writer
|
||||
// .finish()
|
||||
// .map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
||||
// println!("encode to png took {}ms", start_at.elapsed().as_millis());
|
||||
let image_webp =
|
||||
webp::Encoder::from_rgb(&image_buffer, image_width, image_height).encode(90f32);
|
||||
// // base64 image
|
||||
let mut image_base64 = String::new();
|
||||
image_base64.push_str("data:image/webp;base64,");
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_webp);
|
||||
image_base64.push_str(encoded.as_str());
|
||||
|
||||
println!("took {}ms", start_at.elapsed().as_millis());
|
||||
println!("image_base64: {}", image_base64.len());
|
||||
|
||||
Ok(image_base64)
|
||||
};
|
||||
|
||||
exec().map_err(|e: anyhow::Error| {
|
||||
println!("error: {}", e);
|
||||
e.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn play_mode(target_mode: AmbientLightMode) {
|
||||
info!("target mode: {:?}", target_mode);
|
||||
|
||||
tokio::spawn(async move { CoreManager::global().set_ambient_light(target_mode).await });
|
||||
async fn subscribe_encoded_screenshot_updated(
|
||||
window: tauri::Window,
|
||||
display_id: u32,
|
||||
) -> Result<(), String> {
|
||||
let screenshot_manager = ScreenshotManager::global().await;
|
||||
screenshot_manager
|
||||
.subscribe_encoded_screenshot_updated(window, display_id)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("subscribe_encoded_screenshot_updated: {}", err);
|
||||
err.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
rpc::manager::Manager::global();
|
||||
env_logger::init();
|
||||
|
||||
let screenshot_manager = ScreenshotManager::global().await;
|
||||
screenshot_manager.start().unwrap();
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
take_snapshot,
|
||||
play_mode,
|
||||
get_picker_config,
|
||||
get_screenshot_by_config,
|
||||
write_picker_config,
|
||||
greet,
|
||||
take_screenshot,
|
||||
list_display_info,
|
||||
subscribe_encoded_screenshot_updated
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
|
@ -1,53 +0,0 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
@ -1,147 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
mod display_config;
|
||||
mod manger;
|
||||
|
||||
pub use display_config::*;
|
||||
pub use manger::*;
|
@ -1,51 +0,0 @@
|
||||
use paris::info;
|
||||
use scrap::{Capturer, Display};
|
||||
|
||||
use super::{config::DisplayConfig, screen::Screen, screenshot::Screenshot};
|
||||
|
||||
pub struct DisplayPicker {
|
||||
pub screen: Screen,
|
||||
pub config: DisplayConfig,
|
||||
}
|
||||
|
||||
impl DisplayPicker {
|
||||
pub fn new(screen: Screen, config: DisplayConfig) -> Self {
|
||||
Self { screen, config }
|
||||
}
|
||||
|
||||
pub fn from_config(config: DisplayConfig) -> anyhow::Result<Self> {
|
||||
let displays = Display::all()
|
||||
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||
let display = displays
|
||||
.into_iter()
|
||||
.skip(config.index_of_display)
|
||||
.next();
|
||||
|
||||
match display {
|
||||
Some(display) => {
|
||||
let height = display.height();
|
||||
let width = display.width();
|
||||
info!("dw: {}, cw: {}", width, config.display_height);
|
||||
assert_eq!(width, config.display_width);
|
||||
let capturer = Capturer::new(display)?;
|
||||
let screen = Screen::new(capturer, width, height);
|
||||
|
||||
Ok(Self { screen, config })
|
||||
}
|
||||
None => {
|
||||
anyhow::bail!("Index out of displays range.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_screenshot(&mut self) -> anyhow::Result<Screenshot> {
|
||||
let bitmap = self
|
||||
.screen
|
||||
.take()
|
||||
.map_err(|error| anyhow::anyhow!("take screenshot for display failed. {}", error))?;
|
||||
|
||||
// info!("bitmap size {}", bitmap.len());
|
||||
let screenshot = Screenshot::new(bitmap, self.config);
|
||||
Ok(screenshot)
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
use color_space::{Hsv, Rgb};
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct LedColor {
|
||||
bits: [u8; 3],
|
||||
}
|
||||
|
||||
impl LedColor {
|
||||
pub fn default() -> Self {
|
||||
Self { bits: [0, 0, 0] }
|
||||
}
|
||||
|
||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||
Self { bits: [r, g, b] }
|
||||
}
|
||||
|
||||
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
||||
let rgb = Rgb::from(Hsv::new(h, s, v));
|
||||
Self { bits: [rgb.r as u8, rgb.g as u8, rgb.b as u8] }
|
||||
}
|
||||
|
||||
pub fn get_rgb(&self) -> [u8; 3] {
|
||||
self.bits
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.bits.iter().any(|bit| *bit == 0)
|
||||
}
|
||||
|
||||
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||
self.bits = [r, g, b];
|
||||
self
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||
self.bits = [
|
||||
(self.bits[0] / 2 + r / 2),
|
||||
(self.bits[1] / 2 + g / 2),
|
||||
(self.bits[2] / 2 + b / 2),
|
||||
];
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for LedColor {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
let hex = format!("#{}", hex::encode(self.bits));
|
||||
serializer.serialize_str(hex.as_str())
|
||||
}
|
||||
}
|
@ -1,87 +0,0 @@
|
||||
use futures::{stream::FuturesUnordered, StreamExt};
|
||||
use paris::info;
|
||||
use scrap::Display;
|
||||
use std::sync::Arc;
|
||||
use tokio::{
|
||||
sync::{Mutex, OnceCell},
|
||||
task,
|
||||
};
|
||||
|
||||
use crate::picker::{config, screen::Screen};
|
||||
|
||||
use super::{
|
||||
config::DisplayConfig,
|
||||
display_picker::DisplayPicker,
|
||||
screenshot::{Screenshot, ScreenshotDto},
|
||||
};
|
||||
|
||||
pub struct Picker {
|
||||
pub screens: Arc<Mutex<Vec<Screen>>>,
|
||||
pub screenshots: Arc<Mutex<Vec<Screenshot>>>,
|
||||
pub display_configs: Arc<Mutex<Vec<DisplayConfig>>>,
|
||||
}
|
||||
|
||||
impl Picker {
|
||||
pub async fn global() -> &'static Picker {
|
||||
static SCREEN_COLOR_PICKER: OnceCell<Picker> = OnceCell::const_new();
|
||||
|
||||
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<ScreenshotDto>> {
|
||||
let mut configs = vec![];
|
||||
|
||||
let displays = Display::all()
|
||||
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||
|
||||
// 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, index, width, height);
|
||||
configs.push(config);
|
||||
}
|
||||
|
||||
for config in configs.iter() {
|
||||
futs.push(async move {
|
||||
let join = task::spawn(Self::preview_display_by_config(config.clone()));
|
||||
join.await?
|
||||
});
|
||||
}
|
||||
let mut bitmap_string_list = vec![];
|
||||
while let Some(bitmap_string) = futs.next().await {
|
||||
match bitmap_string {
|
||||
Ok(bitmap_string) => {
|
||||
bitmap_string_list.push(bitmap_string);
|
||||
}
|
||||
Err(error) => {
|
||||
anyhow::bail!("can not convert to base64 image. {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(bitmap_string_list)
|
||||
}
|
||||
|
||||
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,6 +0,0 @@
|
||||
pub mod led_color;
|
||||
pub mod screen;
|
||||
pub mod manager;
|
||||
pub mod screenshot;
|
||||
pub mod display_picker;
|
||||
pub mod config;
|
@ -1,83 +0,0 @@
|
||||
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,52 +0,0 @@
|
||||
use scrap::Capturer;
|
||||
use std::{io::ErrorKind::WouldBlock, time::Duration, thread};
|
||||
|
||||
pub struct Screen {
|
||||
capturer: Option<Capturer>,
|
||||
init_error: Option<anyhow::Error>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub fn new(capturer: Capturer, width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
capturer: Some(capturer),
|
||||
init_error: None,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_failed(init_error: anyhow::Error, width: usize, height: usize) -> Self {
|
||||
Self {
|
||||
capturer: None,
|
||||
init_error: Some(init_error),
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take(&mut self) -> anyhow::Result<Vec<u8>> {
|
||||
match self.capturer.as_mut() {
|
||||
Some(capturer) => loop {
|
||||
match capturer.frame() {
|
||||
Ok(buffer) => {
|
||||
return anyhow::Ok(buffer.to_vec());
|
||||
}
|
||||
Err(error) => {
|
||||
if error.kind() == WouldBlock {
|
||||
thread::sleep(Duration::from_millis(16));
|
||||
continue;
|
||||
} else {
|
||||
anyhow::bail!("failed to frame of display. {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
None => anyhow::bail!("Do not initialized"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for Screen {}
|
@ -1,286 +0,0 @@
|
||||
use image::ImageBuffer;
|
||||
use image::{ImageOutputFormat, Rgb};
|
||||
use paris::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::iter;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use super::{config::DisplayConfig, led_color::LedColor};
|
||||
|
||||
type Point = (usize, usize);
|
||||
type LedSamplePoints = Vec<Point>;
|
||||
|
||||
#[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,
|
||||
sample_points: Self::get_sample_points(config),
|
||||
}
|
||||
}
|
||||
|
||||
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![]
|
||||
}
|
||||
};
|
||||
|
||||
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![]
|
||||
}
|
||||
};
|
||||
|
||||
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 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 color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
||||
// paris::info!("color: {:?}", color.get_rgb());
|
||||
colors.push(color);
|
||||
}
|
||||
colors
|
||||
}
|
||||
|
||||
pub async fn to_webp_base64(&self) -> String {
|
||||
let bitmap = &self.bitmap;
|
||||
let stride = bitmap.len() / self.config.display_height;
|
||||
|
||||
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(), 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
|
||||
}
|
||||
}
|
||||
|
||||
#[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>,
|
||||
}
|
@ -1,55 +0,0 @@
|
||||
use crate::picker::led_color::LedColor;
|
||||
|
||||
use super::mqtt::MqttConnection;
|
||||
use once_cell::sync::OnceCell;
|
||||
|
||||
pub struct Manager {
|
||||
mqtt: MqttConnection,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
pub fn global() -> &'static Self {
|
||||
static RPC_MANAGER: OnceCell<Manager> = OnceCell::new();
|
||||
|
||||
RPC_MANAGER.get_or_init(|| Manager::new())
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let mut mqtt = MqttConnection::new();
|
||||
mqtt.initialize();
|
||||
Self { mqtt }
|
||||
}
|
||||
|
||||
pub async fn publish_led_colors(&self, colors: &Vec<LedColor>) -> anyhow::Result<()> {
|
||||
let payload = colors
|
||||
.iter()
|
||||
.map(|c| c.get_rgb().clone())
|
||||
.flatten()
|
||||
.collect::<Vec<u8>>();
|
||||
|
||||
self.mqtt
|
||||
.client
|
||||
.publish(
|
||||
"display-ambient-light/desktop/colors",
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
payload,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| anyhow::anyhow!("mqtt publish failed. {}", error))
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
pub mod manager;
|
||||
pub mod mqtt;
|
@ -1,72 +0,0 @@
|
||||
use rumqttc::{AsyncClient, MqttOptions, QoS};
|
||||
use std::time::Duration;
|
||||
use time::{format_description, OffsetDateTime};
|
||||
use tokio::task;
|
||||
use tracing::warn;
|
||||
|
||||
pub struct MqttConnection {
|
||||
pub client: AsyncClient,
|
||||
}
|
||||
|
||||
impl MqttConnection {
|
||||
pub fn new() -> Self {
|
||||
let mut options = MqttOptions::new("rumqtt-async", "192.168.31.11", 1883);
|
||||
options.set_keep_alive(Duration::from_secs(5));
|
||||
|
||||
let (client, mut eventloop) = AsyncClient::new(options, 10);
|
||||
task::spawn(async move {
|
||||
loop {
|
||||
match eventloop.poll().await {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
println!("MQTT Error Event = {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Self { client }
|
||||
}
|
||||
|
||||
pub fn initialize(&mut self) {
|
||||
self.subscribe_board();
|
||||
self.broadcast_desktop_online();
|
||||
}
|
||||
|
||||
async fn subscribe_board(&self) {
|
||||
self.client
|
||||
.subscribe("display-ambient-light/board/#", QoS::AtMostOnce)
|
||||
.await;
|
||||
}
|
||||
|
||||
fn broadcast_desktop_online(&mut self) {
|
||||
let client = self.client.to_owned();
|
||||
task::spawn(async move {
|
||||
loop {
|
||||
match OffsetDateTime::now_utc()
|
||||
.format(&format_description::well_known::Iso8601::DEFAULT)
|
||||
{
|
||||
Ok(now_str) => {
|
||||
match client
|
||||
.publish(
|
||||
"display-ambient-light/desktop/online",
|
||||
QoS::AtLeastOnce,
|
||||
false,
|
||||
now_str.as_bytes(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {}
|
||||
Err(error) => {
|
||||
warn!("can not publish last online time. {}", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
warn!("can not get time for now. {}", error);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
42
src-tauri/src/screenshot.rs
Normal file
42
src-tauri/src/screenshot.rs
Normal file
@ -0,0 +1,42 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::async_runtime::RwLock;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Screenshot {
|
||||
pub display_id: u32,
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
pub bytes_per_row: usize,
|
||||
pub bytes: Arc<RwLock<Vec<u8>>>,
|
||||
pub scale_factor: f32,
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
pub fn new(
|
||||
display_id: u32,
|
||||
height: u32,
|
||||
width: u32,
|
||||
bytes_per_row: usize,
|
||||
bytes: Vec<u8>,
|
||||
scale_factor: f32,
|
||||
) -> Self {
|
||||
Self {
|
||||
display_id,
|
||||
height,
|
||||
width,
|
||||
bytes_per_row,
|
||||
bytes: Arc::new(RwLock::new(bytes)),
|
||||
scale_factor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct ScreenshotPayload {
|
||||
pub display_id: u32,
|
||||
pub height: u32,
|
||||
pub width: u32,
|
||||
pub base64_image: String,
|
||||
}
|
252
src-tauri/src/screenshot_manager.rs
Normal file
252
src-tauri/src/screenshot_manager.rs
Normal file
@ -0,0 +1,252 @@
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use base64::Engine;
|
||||
use core_graphics::display::{
|
||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
||||
};
|
||||
use paris::{error, info, warn};
|
||||
use tauri::{async_runtime::RwLock, Window};
|
||||
use tokio::sync::{watch, OnceCell};
|
||||
|
||||
use crate::screenshot::{Screenshot, ScreenshotPayload};
|
||||
|
||||
pub fn take_screenshot(display_id: u32, scale_factor: f32) -> anyhow::Result<Screenshot> {
|
||||
log::debug!("take_screenshot");
|
||||
// let start_at = std::time::Instant::now();
|
||||
|
||||
let cg_display = CGDisplay::new(display_id);
|
||||
let cg_image = CGDisplay::screenshot(
|
||||
cg_display.bounds(),
|
||||
kCGWindowListOptionOnScreenOnly,
|
||||
kCGNullWindowID,
|
||||
kCGWindowImageDefault,
|
||||
)
|
||||
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
|
||||
// println!("take screenshot took {}ms", start_at.elapsed().as_millis());
|
||||
|
||||
let buffer = cg_image.data();
|
||||
let bytes_per_row = cg_image.bytes_per_row();
|
||||
|
||||
let height = cg_image.height();
|
||||
let width = cg_image.width();
|
||||
|
||||
let mut bytes = vec![0u8; buffer.len() as usize];
|
||||
bytes.copy_from_slice(&buffer);
|
||||
|
||||
Ok(Screenshot::new(
|
||||
display_id,
|
||||
height as u32,
|
||||
width as u32,
|
||||
bytes_per_row,
|
||||
bytes,
|
||||
scale_factor,
|
||||
))
|
||||
}
|
||||
|
||||
pub struct ScreenshotManager {
|
||||
channels: Arc<RwLock<HashMap<u32, watch::Receiver<Screenshot>>>>,
|
||||
encode_listeners: Arc<RwLock<HashMap<u32, Vec<Window>>>>,
|
||||
}
|
||||
|
||||
impl ScreenshotManager {
|
||||
pub async fn global() -> &'static Self {
|
||||
static SCREENSHOT_MANAGER: OnceCell<ScreenshotManager> = OnceCell::const_new();
|
||||
|
||||
SCREENSHOT_MANAGER
|
||||
.get_or_init(|| async {
|
||||
let channels = Arc::new(RwLock::new(HashMap::new()));
|
||||
let encode_listeners = Arc::new(RwLock::new(HashMap::new()));
|
||||
Self {
|
||||
channels,
|
||||
encode_listeners,
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn start(&self) -> anyhow::Result<()> {
|
||||
let displays = display_info::DisplayInfo::all()?;
|
||||
for display in displays {
|
||||
self.start_one(display.id, display.scale_factor)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> {
|
||||
let channels = self.channels.to_owned();
|
||||
tokio::spawn(async move {
|
||||
let screenshot = take_screenshot(display_id, scale_factor);
|
||||
|
||||
if screenshot.is_err() {
|
||||
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
|
||||
return;
|
||||
}
|
||||
|
||||
let screenshot = screenshot.unwrap();
|
||||
let (tx, rx) = watch::channel(screenshot);
|
||||
{
|
||||
let mut channels = channels.write().await;
|
||||
channels.insert(display_id, rx);
|
||||
}
|
||||
loop {
|
||||
Self::take_screenshot_loop(display_id, scale_factor, &tx).await;
|
||||
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn subscribe_encoded_screenshot_updated(
|
||||
&self,
|
||||
window: Window,
|
||||
display_id: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
let channels = self.channels.to_owned();
|
||||
let encode_listeners = self.encode_listeners.to_owned();
|
||||
log::info!("subscribe_encoded_screenshot_updated. {}", display_id);
|
||||
|
||||
{
|
||||
let encode_listeners = encode_listeners.read().await;
|
||||
let listening_windows = encode_listeners.get(&display_id);
|
||||
if listening_windows.is_some() && listening_windows.unwrap().contains(&window) {
|
||||
log::debug!("subscribe_encoded_screenshot_updated: already listening. display#{}, window#{}", display_id, window.label());
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
{
|
||||
encode_listeners
|
||||
.write()
|
||||
.await
|
||||
.entry(display_id)
|
||||
.or_default()
|
||||
.push(window);
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
info!("subscribe_encoded_screenshot_updated: start");
|
||||
let channels = channels.read().await;
|
||||
let rx = channels.get(&display_id);
|
||||
if rx.is_none() {
|
||||
error!(
|
||||
"subscribe_encoded_screenshot_updated: can not find display_id {}",
|
||||
display_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
let mut rx = rx.unwrap().clone();
|
||||
loop {
|
||||
if let Err(err) = rx.changed().await {
|
||||
error!(
|
||||
"subscribe_encoded_screenshot_updated: can not wait rx {}",
|
||||
err
|
||||
);
|
||||
break;
|
||||
}
|
||||
let encode_listeners = encode_listeners.read().await;
|
||||
let windows = encode_listeners.get(&display_id);
|
||||
if windows.is_none() || windows.unwrap().is_empty() {
|
||||
info!("subscribe_encoded_screenshot_updated: no listener, stop");
|
||||
break;
|
||||
}
|
||||
let screenshot = rx.borrow().clone();
|
||||
let base64_image = Self::encode_screenshot_to_base64(&screenshot).await;
|
||||
let height = screenshot.height;
|
||||
let width = screenshot.width;
|
||||
|
||||
if base64_image.is_err() {
|
||||
error!(
|
||||
"subscribe_encoded_screenshot_updated: encode_screenshot_to_base64 error {}",
|
||||
base64_image.err().unwrap()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let base64_image = base64_image.unwrap();
|
||||
for window in windows.unwrap().into_iter() {
|
||||
let base64_image = base64_image.clone();
|
||||
let payload = ScreenshotPayload {
|
||||
display_id,
|
||||
base64_image,
|
||||
height,
|
||||
width,
|
||||
};
|
||||
if let Err(err) = window.emit("encoded-screenshot-updated", payload) {
|
||||
error!("subscribe_encoded_screenshot_updated: emit error {}", err)
|
||||
} else {
|
||||
info!(
|
||||
"subscribe_encoded_screenshot_updated: emit success. display#{}",
|
||||
display_id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unsubscribe_encoded_screenshot_updated(&self, display_id: u32) -> anyhow::Result<()> {
|
||||
let channels = self.channels.to_owned();
|
||||
let mut channels = channels.write().await;
|
||||
channels.remove(&display_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn take_screenshot_loop(
|
||||
display_id: u32,
|
||||
scale_factor: f32,
|
||||
tx: &watch::Sender<Screenshot>,
|
||||
) {
|
||||
let screenshot = take_screenshot(display_id, scale_factor);
|
||||
if let Ok(screenshot) = screenshot {
|
||||
tx.send(screenshot).unwrap();
|
||||
} else {
|
||||
warn!("take_screenshot_loop: {}", screenshot.err().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
async fn encode_screenshot_to_base64(screenshot: &Screenshot) -> anyhow::Result<String> {
|
||||
let bytes = screenshot.bytes.read().await;
|
||||
|
||||
let scale_factor = screenshot.scale_factor;
|
||||
|
||||
let image_height = (screenshot.height as f32 / scale_factor) as u32;
|
||||
let image_width = (screenshot.width as f32 / scale_factor) as u32;
|
||||
let mut image_buffer = vec![0u8; (image_width * image_height * 3) as usize];
|
||||
|
||||
for y in 0..image_height {
|
||||
for x in 0..image_width {
|
||||
let offset = (((y as f32) * screenshot.bytes_per_row as f32 + (x as f32) * 4.0)
|
||||
* scale_factor) as usize;
|
||||
let b = bytes[offset];
|
||||
let g = bytes[offset + 1];
|
||||
let r = bytes[offset + 2];
|
||||
let offset = (y * image_width + x) as usize;
|
||||
image_buffer[offset * 3] = r;
|
||||
image_buffer[offset * 3 + 1] = g;
|
||||
image_buffer[offset * 3 + 2] = b;
|
||||
}
|
||||
}
|
||||
|
||||
let mut image_png = Vec::new();
|
||||
let mut encoder = png::Encoder::new(&mut image_png, image_width, image_height);
|
||||
encoder.set_color(png::ColorType::Rgb);
|
||||
encoder.set_depth(png::BitDepth::Eight);
|
||||
|
||||
let mut writer = encoder
|
||||
.write_header()
|
||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
||||
writer
|
||||
.write_image_data(&image_buffer)
|
||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
||||
writer
|
||||
.finish()
|
||||
.map_err(|e| anyhow::anyhow!("png: {}", anyhow::anyhow!(e.to_string())))?;
|
||||
|
||||
let mut base64_image = String::new();
|
||||
base64_image.push_str("data:image/webp;base64,");
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(&*image_png);
|
||||
base64_image.push_str(encoded.as_str());
|
||||
Ok(base64_image)
|
||||
}
|
||||
}
|
@ -3,24 +3,23 @@
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:1420",
|
||||
"distDir": "../dist"
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": false
|
||||
},
|
||||
"package": {
|
||||
"productName": "display-ambient-light-desktop",
|
||||
"productName": "test-demo",
|
||||
"version": "0.0.0"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
"all": true
|
||||
"all": false,
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "DeveloperTool",
|
||||
"copyright": "",
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"externalBin": [],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
@ -28,23 +27,8 @@
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"identifier": "cc.ivanli.ambient",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"entitlements": null,
|
||||
"exceptionDomain": "",
|
||||
"frameworks": [],
|
||||
"providerShortName": null,
|
||||
"signingIdentity": null
|
||||
},
|
||||
"resources": [],
|
||||
"shortDescription": "",
|
||||
"targets": "all",
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
"identifier": "cc.ivanli.take-screenshot-test-demo",
|
||||
"targets": "all"
|
||||
},
|
||||
"security": {
|
||||
"csp": null
|
||||
@ -55,10 +39,10 @@
|
||||
"windows": [
|
||||
{
|
||||
"fullscreen": false,
|
||||
"height": 600,
|
||||
"resizable": true,
|
||||
"title": "display-ambient-light-desktop",
|
||||
"width": 800
|
||||
"title": "test-demo",
|
||||
"width": 800,
|
||||
"height": 600
|
||||
}
|
||||
]
|
||||
}
|
||||
|
11
src/App.css
11
src/App.css
@ -1,11 +0,0 @@
|
||||
.logo.vite:hover {
|
||||
filter: drop-shadow(0 0 2em #747bff);
|
||||
}
|
||||
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafb);
|
||||
}
|
||||
|
||||
img {
|
||||
pointer-event: none !important;
|
||||
}
|
79
src/App.tsx
79
src/App.tsx
@ -1,73 +1,24 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { createEffect, createSignal } from "solid-js";
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import './App.css';
|
||||
import { Configurator } from './configurator/configurator';
|
||||
import { ButtonSwitch } from './commons/components/button';
|
||||
import { fillParentCss } from './styles/fill-parent';
|
||||
|
||||
type Mode = 'Flowing' | 'Follow' | null;
|
||||
|
||||
localStorage.setItem('debug', '*');
|
||||
import { DisplayInfo } from './models/display-info.model';
|
||||
import { DisplayView } from './components/\u0016display-view';
|
||||
|
||||
function App() {
|
||||
const [screenshots, setScreenshots] = useState<string[]>([]);
|
||||
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
||||
const [currentMode, setCurrentMode] = useState<Mode>(null);
|
||||
const [displays, setDisplays] = createSignal<DisplayInfo[]>([]);
|
||||
|
||||
async function takeSnapshot() {
|
||||
const base64TextList: string[] = await invoke('take_snapshot');
|
||||
|
||||
setScreenshots(base64TextList.map((text) => `data:image/webp;base64,${text}`));
|
||||
}
|
||||
|
||||
const getLedStripColors = useCallback(async () => {
|
||||
setLedStripColors(await invoke('get_led_strip_colors'));
|
||||
}, []);
|
||||
|
||||
const switchCurrentMode = useCallback(
|
||||
async (targetMode: Mode) => {
|
||||
console.log(targetMode, currentMode, currentMode === targetMode);
|
||||
if (currentMode === targetMode) {
|
||||
await invoke('play_mode', { targetMode: 'None' });
|
||||
setCurrentMode(null);
|
||||
} else {
|
||||
await invoke('play_mode', { targetMode });
|
||||
setCurrentMode(targetMode);
|
||||
}
|
||||
console.log(targetMode, currentMode, currentMode === targetMode);
|
||||
},
|
||||
[currentMode, setCurrentMode],
|
||||
);
|
||||
createEffect(() => {
|
||||
invoke<string>('list_display_info').then((displays) => {
|
||||
setDisplays(JSON.parse(displays));
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div css={[fillParentCss]} tw="box-border flex flex-col">
|
||||
<div tw="flex justify-between">
|
||||
{ledStripColors.map((it) => (
|
||||
<span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div tw="flex gap-1 justify-center w-screen overflow-hidden">
|
||||
{screenshots.map((screenshot) => (
|
||||
<div tw="flex-auto">
|
||||
<img src={screenshot} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<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
|
||||
</ButtonSwitch>
|
||||
<ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch>
|
||||
</div>
|
||||
|
||||
<div css={[fillParentCss]}>
|
||||
<Configurator />
|
||||
</div>
|
||||
<div class="container">
|
||||
<ol>
|
||||
{displays().map((display) => {
|
||||
return <DisplayView display={display} />;
|
||||
})}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
Before Width: | Height: | Size: 4.0 KiB |
@ -1,21 +0,0 @@
|
||||
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`,
|
||||
]);
|
20
src/components/display-view.tsx
Normal file
20
src/components/display-view.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { Component } from 'solid-js';
|
||||
import { DisplayInfo } from '../models/display-info.model';
|
||||
import { DisplayInfoPanel } from './display-info-panel';
|
||||
import { ScreenView } from './screen-view';
|
||||
|
||||
type DisplayViewProps = {
|
||||
display: DisplayInfo;
|
||||
};
|
||||
|
||||
export const DisplayView: Component<DisplayViewProps> = (props) => {
|
||||
return (
|
||||
<section class="relative">
|
||||
<ScreenView displayId={props.display.id} />
|
||||
<DisplayInfoPanel
|
||||
display={props.display}
|
||||
class="absolute bg-slate-50/10 top-1/4 left-1/4 rounded backdrop-blur w-1/3 min-w-fit text-black"
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
};
|
42
src/components/display-info-panel.tsx
Normal file
42
src/components/display-info-panel.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { Component, JSX, ParentComponent, splitProps } from 'solid-js';
|
||||
import { DisplayInfo } from '../models/display-info.model';
|
||||
|
||||
type DisplayInfoItemProps = {
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const DisplayInfoItem: ParentComponent<DisplayInfoItemProps> = (props) => {
|
||||
return (
|
||||
<dl class="px-3 py-1 flex hover:bg-gray-100/50 gap-2 text-black rounded">
|
||||
<dt class="uppercase w-1/2 select-all">{props.label}</dt>
|
||||
<dd class="select-all">{props.children}</dd>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
type DisplayInfoPanelProps = {
|
||||
display: DisplayInfo;
|
||||
} & JSX.HTMLAttributes<HTMLElement>;
|
||||
|
||||
export const DisplayInfoPanel: Component<DisplayInfoPanelProps> = (props) => {
|
||||
const [localProps, rootProps] = splitProps(props, ['display']);
|
||||
return (
|
||||
<section {...rootProps} class={'m-2 flex flex-col gap-1 py-2 ' + rootProps.class}>
|
||||
<DisplayInfoItem label="ID">
|
||||
<code>{localProps.display.id}</code>
|
||||
</DisplayInfoItem>
|
||||
<DisplayInfoItem label="Position">
|
||||
({localProps.display.x}, {localProps.display.y})
|
||||
</DisplayInfoItem>
|
||||
<DisplayInfoItem label="Size">
|
||||
{localProps.display.width} x {localProps.display.height}
|
||||
</DisplayInfoItem>
|
||||
<DisplayInfoItem label="Scale Factor">
|
||||
{localProps.display.scale_factor}
|
||||
</DisplayInfoItem>
|
||||
<DisplayInfoItem label="is Primary">
|
||||
{localProps.display.is_primary ? 'True' : 'False'}
|
||||
</DisplayInfoItem>
|
||||
</section>
|
||||
);
|
||||
};
|
46
src/components/screen-view.tsx
Normal file
46
src/components/screen-view.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import {
|
||||
Component,
|
||||
createEffect,
|
||||
createSignal,
|
||||
JSX,
|
||||
onCleanup,
|
||||
splitProps,
|
||||
} from 'solid-js';
|
||||
|
||||
type ScreenViewProps = {
|
||||
displayId: number;
|
||||
} & JSX.HTMLAttributes<HTMLImageElement>;
|
||||
|
||||
async function subscribeScreenshotUpdate(displayId: number) {
|
||||
await invoke('subscribe_encoded_screenshot_updated', {
|
||||
displayId,
|
||||
});
|
||||
}
|
||||
|
||||
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
||||
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
||||
const [image, setImage] = createSignal<string>();
|
||||
createEffect(() => {
|
||||
const unlisten = listen<{ base64_image: string; display_id: number }>(
|
||||
'encoded-screenshot-updated',
|
||||
(event) => {
|
||||
if (event.payload.display_id === localProps.displayId) {
|
||||
setImage(event.payload.base64_image);
|
||||
}
|
||||
|
||||
console.log(event.payload.display_id, localProps.displayId);
|
||||
},
|
||||
);
|
||||
subscribeScreenshotUpdate(localProps.displayId);
|
||||
|
||||
onCleanup(() => {
|
||||
unlisten.then((unlisten) => {
|
||||
unlisten();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return <img src={image()} class="object-contain" {...rootProps} />;
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
import { isNil } from 'ramda';
|
||||
import { FC, Fragment, useMemo } from 'react';
|
||||
import tw, { css, styled } from 'twin.macro';
|
||||
import { useLedCount } from '../contents/led-count';
|
||||
import { PixelRgb } from '../models/pixel-rgb';
|
||||
import { StyledPixel } from './styled-pixel';
|
||||
import { BorderLedStrip } from './completed-led-strip';
|
||||
|
||||
const StyledCompletedContainerBorder = styled.section(
|
||||
tw`dark:shadow-xl border-gray-600 bg-black border rounded-full flex flex-nowrap justify-around items-center p-px h-3 mb-1`,
|
||||
css`
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1 / span 1;
|
||||
justify-self: stretch;
|
||||
`,
|
||||
);
|
||||
interface StyledCompletedContainerProps {
|
||||
borderLedStrips: BorderLedStrip[];
|
||||
overrideBorderLedStrips?: BorderLedStrip[];
|
||||
}
|
||||
export const CompletedContainer: FC<StyledCompletedContainerProps> = ({
|
||||
borderLedStrips,
|
||||
overrideBorderLedStrips,
|
||||
}) => {
|
||||
const { ledCount } = useLedCount();
|
||||
const completedPixels = useMemo(() => {
|
||||
const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]);
|
||||
(overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => {
|
||||
if (isNil(config)) {
|
||||
return;
|
||||
}
|
||||
if (config.global_start_position <= config.global_end_position) {
|
||||
pixels.forEach((color, i) => {
|
||||
completed[config.global_start_position + i] = color;
|
||||
});
|
||||
} else {
|
||||
pixels.forEach((color, i) => {
|
||||
completed[config.global_start_position - i] = color;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return completed.map((color, i) => (
|
||||
<StyledPixel
|
||||
rgb={color}
|
||||
key={i}
|
||||
css={css`
|
||||
grid-row-start: 1;
|
||||
grid-column-start: ${i + 1};
|
||||
align-self: flex-start;
|
||||
`}
|
||||
/>
|
||||
));
|
||||
}, [ledCount, borderLedStrips, overrideBorderLedStrips]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<StyledCompletedContainerBorder />
|
||||
{completedPixels}
|
||||
</Fragment>
|
||||
);
|
||||
};
|
@ -1,119 +0,0 @@
|
||||
import debug from 'debug';
|
||||
import { lensPath, set, splitEvery, update } from 'ramda';
|
||||
import { FC, useEffect, useMemo, useState } from 'react';
|
||||
import tw, { css, styled } from 'twin.macro';
|
||||
import { Borders, borders } from '../../constants/border';
|
||||
import { useLedCount } from '../contents/led-count';
|
||||
import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
|
||||
import { LedStripConfig } from '../models/led-strip-config';
|
||||
import { PixelRgb } from '../models/pixel-rgb';
|
||||
import { ScreenshotDto } from '../models/screenshot.dto';
|
||||
import { CompletedContainer } from './completed-container';
|
||||
import { DraggableStrip } from './draggable-strip';
|
||||
|
||||
export const logger = debug('app:completed-led-strip');
|
||||
|
||||
interface CompletedLedStripProps {
|
||||
screenshots: ScreenshotDto[];
|
||||
onDisplayConfigChange?: (value: DisplayConfig) => void;
|
||||
}
|
||||
|
||||
export type BorderLedStrip = {
|
||||
pixels: PixelRgb[];
|
||||
config: LedStripConfig | null;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.section(
|
||||
({ rows, columns }: { rows: number; columns: number }) => [
|
||||
tw`grid m-4 pb-2 items-center justify-items-center select-none`,
|
||||
tw`overflow-x-auto overflow-y-hidden flex-none`,
|
||||
css`
|
||||
grid-template-columns: repeat(${columns}, 0.5em);
|
||||
grid-template-rows: auto repeat(${rows}, 0.5em);
|
||||
`,
|
||||
],
|
||||
);
|
||||
export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
screenshots,
|
||||
onDisplayConfigChange,
|
||||
}) => {
|
||||
const borderLedStrips: BorderLedStrip[] = useMemo(() => {
|
||||
return screenshots.flatMap((ss) =>
|
||||
borders.map((b) => ({
|
||||
pixels: splitEvery(3, Array.from(ss.colors[b])) as PixelRgb[],
|
||||
config: ss.config.led_strip_of_borders[b],
|
||||
})),
|
||||
);
|
||||
}, [screenshots]);
|
||||
const ledCount = useMemo(
|
||||
() => borderLedStrips.reduce((prev, curr) => prev + curr.pixels.length, 0),
|
||||
[borderLedStrips],
|
||||
);
|
||||
const maxIndex = useMemo(
|
||||
() =>
|
||||
Math.max(
|
||||
...borderLedStrips
|
||||
.map((s) => [
|
||||
s.config?.global_end_position ?? 0,
|
||||
s.config?.global_start_position ?? 0,
|
||||
])
|
||||
.flat(),
|
||||
),
|
||||
[borderLedStrips],
|
||||
);
|
||||
|
||||
const { setLedCount } = useLedCount();
|
||||
// setLedCount for context
|
||||
useEffect(() => {
|
||||
setLedCount(ledCount);
|
||||
}, [ledCount, setLedCount]);
|
||||
|
||||
const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
|
||||
useState<BorderLedStrip[]>();
|
||||
|
||||
const strips = useMemo(() => {
|
||||
return borderLedStrips.map(({ config, pixels }, index) =>
|
||||
config ? (
|
||||
<DraggableStrip
|
||||
key={index}
|
||||
{...{ config, pixels, index: index + 1 }}
|
||||
onConfigChange={(c) => {
|
||||
setOverrideBorderLedStrips(
|
||||
update(index, { config: c, pixels }, borderLedStrips),
|
||||
);
|
||||
}}
|
||||
onConfigFinish={(c) => {
|
||||
const indexOfDisplay = Math.floor(index / borders.length);
|
||||
const xLens = lensPath<LedStripConfigOfBorders, Borders>([
|
||||
borders[index % borders.length],
|
||||
]);
|
||||
const displayConfig: DisplayConfig = {
|
||||
...screenshots[indexOfDisplay].config,
|
||||
led_strip_of_borders: set(
|
||||
xLens,
|
||||
c,
|
||||
screenshots[indexOfDisplay].config.led_strip_of_borders,
|
||||
),
|
||||
};
|
||||
onDisplayConfigChange?.(displayConfig);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div key={index} />
|
||||
),
|
||||
);
|
||||
}, [borderLedStrips, screenshots]);
|
||||
|
||||
useEffect(() => {
|
||||
setOverrideBorderLedStrips(undefined);
|
||||
}, [borderLedStrips]);
|
||||
return (
|
||||
<StyledContainer rows={screenshots.length * borders.length} columns={maxIndex + 1}>
|
||||
<CompletedContainer
|
||||
borderLedStrips={borderLedStrips}
|
||||
overrideBorderLedStrips={overrideBorderLedStrips}
|
||||
/>
|
||||
{strips}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,100 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,199 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,104 +0,0 @@
|
||||
import { HTMLAttributes, MouseEventHandler, useCallback } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { LedStripConfig } from '../models/led-strip-config';
|
||||
import tw, { styled } from 'twin.macro';
|
||||
import { faLeftRight, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
export interface LedStripEditorProps
|
||||
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
|
||||
config: LedStripConfig | null;
|
||||
onChange?: (config: LedStripConfig | null) => void;
|
||||
}
|
||||
|
||||
const StyledContainer = styled.section(
|
||||
tw`flex flex-wrap gap-2 self-start justify-self-start`,
|
||||
);
|
||||
|
||||
const StyledButton = styled.button(
|
||||
tw`
|
||||
bg-yellow-500 dark:bg-amber-600 rounded-full h-4 w-4 text-xs shadow select-none`,
|
||||
tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600 active:dark:bg-amber-500`,
|
||||
);
|
||||
|
||||
export const LedStripEditor: FC<LedStripEditorProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
...htmlAttrs
|
||||
}) => {
|
||||
const addLed: MouseEventHandler = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
const delta = ev.button === 2 ? 10 : 1;
|
||||
if (config) {
|
||||
if (config.global_start_position <= config.global_end_position) {
|
||||
onChange?.({
|
||||
...config,
|
||||
global_end_position: config.global_end_position + delta,
|
||||
});
|
||||
} else {
|
||||
onChange?.({
|
||||
...config,
|
||||
global_start_position: config.global_start_position + delta,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
onChange?.(new LedStripConfig(0, 0, 0));
|
||||
}
|
||||
},
|
||||
[config, onChange],
|
||||
);
|
||||
const removeLed: MouseEventHandler = useCallback(
|
||||
(ev) => {
|
||||
ev.preventDefault();
|
||||
const delta = ev.button === 2 ? 10 : 1;
|
||||
if (!config) {
|
||||
onChange?.(null);
|
||||
} else if (
|
||||
Math.abs(config.global_start_position - config.global_end_position) <= delta
|
||||
) {
|
||||
onChange?.(null);
|
||||
} else {
|
||||
if (config.global_start_position <= config.global_end_position) {
|
||||
onChange?.({
|
||||
...config,
|
||||
global_end_position: config.global_end_position - delta,
|
||||
});
|
||||
} else {
|
||||
onChange?.({
|
||||
...config,
|
||||
global_start_position: config.global_start_position - delta,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[config, onChange],
|
||||
);
|
||||
const reverse = useCallback(() => {
|
||||
if (!config) {
|
||||
return;
|
||||
}
|
||||
onChange?.({
|
||||
...config,
|
||||
global_start_position: config.global_end_position,
|
||||
global_end_position: config.global_start_position,
|
||||
});
|
||||
}, [config, onChange]);
|
||||
|
||||
return (
|
||||
<StyledContainer {...htmlAttrs}>
|
||||
<StyledButton title="Add LED" onClick={addLed} onContextMenu={addLed}>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</StyledButton>
|
||||
<StyledButton title="Remove LED" onClick={removeLed} onContextMenu={removeLed}>
|
||||
<FontAwesomeIcon icon={faMinus} />
|
||||
</StyledButton>
|
||||
<StyledButton title="Reverse" onClick={reverse}>
|
||||
<FontAwesomeIcon icon={faLeftRight} />
|
||||
</StyledButton>
|
||||
{`s: ${config?.global_start_position ?? 'x'}, e: ${
|
||||
config?.global_end_position ?? 'x'
|
||||
}`}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,28 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
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});
|
||||
`,
|
||||
],
|
||||
);
|
@ -1,146 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import tw, { styled } from 'twin.macro';
|
||||
import { useAsync, useAsyncCallback } from 'react-async-hook';
|
||||
import { DisplayWithLedStrips } from './components/display-with-led-strips';
|
||||
import { PickerConfiguration } from './models/picker-configuration';
|
||||
import { DisplayConfig } from './models/display-config';
|
||||
import { ScreenshotDto } from './models/screenshot.dto';
|
||||
import { Alert, Fab, Snackbar } from '@mui/material';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faRotateBack, faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { update } from 'ramda';
|
||||
import { CompletedLedStrip } from './components/completed-led-strip';
|
||||
import { LedCountProvider } from './contents/led-count';
|
||||
import debug from 'debug';
|
||||
import { useSnackbar } from 'notistack';
|
||||
import { fillParentCss } from '../styles/fill-parent';
|
||||
|
||||
const logger = debug('app:configurator');
|
||||
|
||||
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
|
||||
const getScreenshotOfDisplays = () =>
|
||||
invoke<ScreenshotDto[]>('take_snapshot').then((items) => {
|
||||
return items;
|
||||
});
|
||||
const getScreenshotByConfig = async (config: DisplayConfig) => {
|
||||
return await invoke<ScreenshotDto>('get_screenshot_by_config', {
|
||||
config,
|
||||
});
|
||||
};
|
||||
const writePickerConfig = async (config: PickerConfiguration) => {
|
||||
return await invoke<void>('write_picker_config', {
|
||||
config,
|
||||
});
|
||||
};
|
||||
const StyledConfiguratorContainer = styled.section(
|
||||
tw`flex flex-col items-stretch relative overflow-hidden`,
|
||||
fillParentCss,
|
||||
);
|
||||
|
||||
const StyledDisplayContainer = styled.section(tw`overflow-auto`);
|
||||
|
||||
const ConfiguratorInner: FC = () => {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
||||
getPickerConfig,
|
||||
[],
|
||||
);
|
||||
|
||||
const { loading: pendingScreenshotOfDisplays, result: defaultScreenshotOfDisplays } =
|
||||
useAsync(getScreenshotOfDisplays, []);
|
||||
|
||||
const [screenshotOfDisplays, setScreenshotOfDisplays] = useState<ScreenshotDto[]>([]);
|
||||
|
||||
const { loading: pendingGetLedColorsByConfig, execute: onDisplayConfigChange } =
|
||||
useAsyncCallback(async (value: DisplayConfig) => {
|
||||
const screenshot = await getScreenshotByConfig(value);
|
||||
setScreenshotOfDisplays((old) => {
|
||||
const index = old.findIndex((it) => it.config.id === screenshot.config.id);
|
||||
const newValue = update(index, screenshot, old);
|
||||
savedPickerConfig &&
|
||||
writePickerConfig({
|
||||
...savedPickerConfig,
|
||||
display_configs: newValue.map((it) => it.config),
|
||||
});
|
||||
return newValue;
|
||||
});
|
||||
});
|
||||
|
||||
const [displayConfigs, setDisplayConfigs] = useState<DisplayConfig[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const displayConfigs = savedPickerConfig?.display_configs;
|
||||
if (displayConfigs && defaultScreenshotOfDisplays) {
|
||||
setDisplayConfigs(displayConfigs);
|
||||
setScreenshotOfDisplays(defaultScreenshotOfDisplays);
|
||||
(async () => {
|
||||
for (const config of displayConfigs) {
|
||||
await onDisplayConfigChange(config);
|
||||
}
|
||||
})().then();
|
||||
}
|
||||
}, [savedPickerConfig, onDisplayConfigChange, defaultScreenshotOfDisplays]);
|
||||
|
||||
const resetBackToDefaultConfig = useCallback(() => {
|
||||
if (defaultScreenshotOfDisplays) {
|
||||
setScreenshotOfDisplays(defaultScreenshotOfDisplays);
|
||||
} else {
|
||||
enqueueSnackbar('Default Config was not found. please try again later.', {
|
||||
variant: 'error',
|
||||
});
|
||||
}
|
||||
}, [setScreenshotOfDisplays, defaultScreenshotOfDisplays]);
|
||||
|
||||
const displays = useMemo(() => {
|
||||
if (screenshotOfDisplays) {
|
||||
return screenshotOfDisplays.map((screenshot, index) => (
|
||||
<DisplayWithLedStrips
|
||||
key={index}
|
||||
config={screenshot.config}
|
||||
screenshot={screenshot}
|
||||
onChange={(value) => onDisplayConfigChange(value)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}, [displayConfigs, screenshotOfDisplays]);
|
||||
|
||||
if (pendingPickerConfig || pendingScreenshotOfDisplays) {
|
||||
return (
|
||||
<section>
|
||||
等待 {JSON.stringify({ pendingPickerConfig, pendingScreenshotOfDisplays })}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledConfiguratorContainer>
|
||||
<CompletedLedStrip
|
||||
screenshots={screenshotOfDisplays}
|
||||
onDisplayConfigChange={onDisplayConfigChange}
|
||||
/>
|
||||
<StyledDisplayContainer tw="overflow-y-auto">{displays}</StyledDisplayContainer>
|
||||
<Fab
|
||||
aria-label="reset"
|
||||
size="small"
|
||||
tw="top-2 right-2 absolute"
|
||||
onClick={resetBackToDefaultConfig}
|
||||
>
|
||||
<FontAwesomeIcon icon={faRotateBack} />
|
||||
</Fab>
|
||||
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
|
||||
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
||||
This is a success message!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</StyledConfiguratorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const Configurator = () => {
|
||||
return (
|
||||
<LedCountProvider>
|
||||
<ConfiguratorInner />
|
||||
</LedCountProvider>
|
||||
);
|
||||
};
|
@ -1,25 +0,0 @@
|
||||
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);
|
@ -1,21 +0,0 @@
|
||||
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,
|
||||
) {}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
export class LedStripConfig {
|
||||
constructor(
|
||||
public index: number,
|
||||
public global_start_position: number,
|
||||
public global_end_position: number,
|
||||
) {}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import { DisplayConfig } from './display-config';
|
||||
|
||||
export class PickerConfiguration {
|
||||
constructor(
|
||||
public display_configs: DisplayConfig[] = [],
|
||||
public config_version: number = 1,
|
||||
) {}
|
||||
}
|
@ -1 +0,0 @@
|
||||
export type PixelRgb = [number, number, number];
|
@ -1,12 +0,0 @@
|
||||
import { DisplayConfig } from './display-config';
|
||||
|
||||
export class ScreenshotDto {
|
||||
encode_image!: string;
|
||||
config!: DisplayConfig;
|
||||
colors!: {
|
||||
top: Uint8Array;
|
||||
bottom: Uint8Array;
|
||||
left: Uint8Array;
|
||||
right: Uint8Array;
|
||||
};
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export const borders = ['top', 'right', 'bottom', 'left'] as const;
|
||||
export type Borders = typeof borders[number];
|
7
src/index.tsx
Normal file
7
src/index.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
/* @refresh reload */
|
||||
import { render } from "solid-js/web";
|
||||
|
||||
import "./styles.css";
|
||||
import App from "./App";
|
||||
|
||||
render(() => <App />, document.getElementById("root") as HTMLElement);
|
14
src/main.tsx
14
src/main.tsx
@ -1,14 +0,0 @@
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import GlobalStyles from './styles/global-styles';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<GlobalStyles />
|
||||
<SnackbarProvider maxSnack={3}>
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</React.StrictMode>,
|
||||
);
|
11
src/models/display-info.model.ts
Normal file
11
src/models/display-info.model.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export class DisplayInfo {
|
||||
constructor(
|
||||
public id: number,
|
||||
public x: number,
|
||||
public y: number,
|
||||
public width: number,
|
||||
public height: number,
|
||||
public scale_factor: number,
|
||||
public is_primary: boolean,
|
||||
) {}
|
||||
}
|
106
src/style.css
106
src/style.css
@ -1,106 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
font-weight: 400;
|
||||
|
||||
color: #0f0f0f;
|
||||
background-color: #f6f6f6;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0;
|
||||
padding-top: 10vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: 0.75s;
|
||||
}
|
||||
|
||||
.logo.tauri:hover {
|
||||
filter: drop-shadow(0 0 2em #24c8db);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: #0f0f0f;
|
||||
background-color: #ffffff;
|
||||
transition: border-color 0.25s;
|
||||
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #396cd8;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#greet-input {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
color: #f6f6f6;
|
||||
background-color: #2f2f2f;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #24c8db;
|
||||
}
|
||||
|
||||
input,
|
||||
button {
|
||||
color: #ffffff;
|
||||
background-color: #0f0f0f98;
|
||||
}
|
||||
}
|
3
src/styles.css
Normal file
3
src/styles.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@ -1,3 +0,0 @@
|
||||
import tw from 'twin.macro';
|
||||
|
||||
export const fillParentCss = tw`w-full h-full overflow-hidden`;
|
@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Global, css } from '@emotion/react';
|
||||
import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro';
|
||||
import { fillParentCss } from './fill-parent';
|
||||
|
||||
const customStyles = css({
|
||||
body: {
|
||||
WebkitTapHighlightColor: theme`colors.purple.500`,
|
||||
...tw`antialiased`,
|
||||
...tw`dark:bg-dark-800 bg-dark-100 dark:text-gray-100 text-gray-800`,
|
||||
...fillParentCss,
|
||||
},
|
||||
html: {
|
||||
...fillParentCss,
|
||||
},
|
||||
'#root': {
|
||||
...fillParentCss,
|
||||
},
|
||||
'::-webkit-scrollbar': tw`w-1.5 h-1.5`,
|
||||
'::-webkit-scrollbar-button': tw`hidden`,
|
||||
'::-webkit-scrollbar-track': tw`bg-transparent`,
|
||||
'::-webkit-scrollbar-track-piece': tw`bg-transparent`,
|
||||
'::-webkit-scrollbar-thumb': tw`bg-gray-300 dark:(bg-gray-700/50 hover:bg-gray-700/90 active:bg-gray-700/100) transition-colors rounded-full`,
|
||||
'::-webkit-scrollbar-corner': tw`bg-gray-600`,
|
||||
'::-webkit-resizer': tw`hidden`,
|
||||
});
|
||||
|
||||
const GlobalStyles = () => (
|
||||
<>
|
||||
<BaseStyles />
|
||||
<Global styles={customStyles} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default GlobalStyles;
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
@ -1,15 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
dark: {
|
||||
800: '#0f0f0f',
|
||||
100: '#f6f6f6',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
10
tailwind.config.js
Normal file
10
tailwind.config.js
Normal file
@ -0,0 +1,10 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./src/**/*.{js,jsx,ts,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
@ -1,25 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "preserve",
|
||||
"jsxImportSource": "solid-js",
|
||||
"types": ["vite/client"],
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "@emotion/react"
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"types"
|
||||
],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
18
types/twin.d.ts
vendored
18
types/twin.d.ts
vendored
@ -1,18 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,31 +1,13 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import solidPlugin from "vite-plugin-solid";
|
||||
|
||||
const mobile =
|
||||
process.env.TAURI_PLATFORM === "android" ||
|
||||
process.env.TAURI_PLATFORM === "ios";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
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'],
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
export default defineConfig(async () => ({
|
||||
plugins: [solidPlugin()],
|
||||
|
||||
// Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
|
||||
// prevent vite from obscuring rust errors
|
||||
@ -37,13 +19,13 @@ 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: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "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,
|
||||
},
|
||||
});
|
||||
}));
|
||||
|
Loading…
Reference in New Issue
Block a user