Compare commits
No commits in common. "AVFoundation" and "master" have entirely different histories.
AVFoundati
...
master
7
.eslintignore
Normal file
7
.eslintignore
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
node_modules/*
|
||||||
|
src-tauri
|
52
.eslintrc.cjs
Normal file
52
.eslintrc.cjs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
21
.gitignore
vendored
21
.gitignore
vendored
@ -1,3 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
35
.vscode/launch.json
vendored
35
.vscode/launch.json
vendored
@ -1,35 +0,0 @@
|
|||||||
{
|
|
||||||
// Use IntelliSense to learn about possible attributes.
|
|
||||||
// Hover to view descriptions of existing attributes.
|
|
||||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Tauri Development Debug",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--manifest-path=./src-tauri/Cargo.toml",
|
|
||||||
"--no-default-features"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// task for the `beforeDevCommand` if used, must be configured in `.vscode/tasks.json`
|
|
||||||
"preLaunchTask": "ui:dev"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "lldb",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Tauri Production Debug",
|
|
||||||
"cargo": {
|
|
||||||
"args": [
|
|
||||||
"build",
|
|
||||||
"--release",
|
|
||||||
"--manifest-path=./src-tauri/Cargo.toml"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
// task for the `beforeBuildCommand` if used, must be configured in `.vscode/tasks.json`
|
|
||||||
"preLaunchTask": "ui:build"
|
|
||||||
}
|
|
||||||
]
|
|
11
.vscode/settings.json
vendored
11
.vscode/settings.json
vendored
@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"files.autoSave": "onWindowChange",
|
|
||||||
"cSpell.words": [
|
|
||||||
"Itertools",
|
|
||||||
"Leds",
|
|
||||||
"unlisten"
|
|
||||||
],
|
|
||||||
"idf.customExtraVars": {
|
|
||||||
"OPENOCD_SCRIPTS": "/Users/ivan/.espressif/tools/openocd-esp32/v0.11.0-esp32-20211220/openocd-esp32/share/openocd/scripts"
|
|
||||||
}
|
|
||||||
}
|
|
42
.vscode/tasks.json
vendored
42
.vscode/tasks.json
vendored
@ -1,42 +0,0 @@
|
|||||||
{
|
|
||||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
|
||||||
// for the documentation about the tasks.json format
|
|
||||||
"version": "2.0.0",
|
|
||||||
"tasks": [
|
|
||||||
{
|
|
||||||
"label": "dev",
|
|
||||||
"type": "shell",
|
|
||||||
"isBackground": true,
|
|
||||||
"command": "pnpm",
|
|
||||||
"args": [
|
|
||||||
"tauri",
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"problemMatcher": [
|
|
||||||
"$eslint-stylish"
|
|
||||||
],
|
|
||||||
"options": {
|
|
||||||
"env": {
|
|
||||||
"RUST_LOG": "info"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "ui:dev",
|
|
||||||
"type": "shell",
|
|
||||||
"isBackground": true,
|
|
||||||
"command": "pnpm",
|
|
||||||
"args": [
|
|
||||||
"dev"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "ui:build",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "pnpm",
|
|
||||||
"args": [
|
|
||||||
"build"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@ -1,6 +1,6 @@
|
|||||||
# Tauri + Solid + Typescript
|
# Tauri + React + Typescript
|
||||||
|
|
||||||
This template should help get you started developing with Tauri, Solid and Typescript in Vite.
|
This template should help get you started developing with Tauri, React and Typescript in Vite.
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
5
babel-plugin-macros.config.cjs
Normal file
5
babel-plugin-macros.config.cjs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module.exports = {
|
||||||
|
twin: {
|
||||||
|
preset: 'emotion',
|
||||||
|
},
|
||||||
|
};
|
13
index.html
13
index.html
@ -1,17 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/src/assets/logo.svg" />
|
<title>Tauri + React + TS</title>
|
||||||
<title>Tauri + Solid + Typescript App</title>
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
<script src="/src/index.tsx" type="module"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
63
package.json
63
package.json
@ -1,33 +1,54 @@
|
|||||||
{
|
{
|
||||||
"name": "test-demo",
|
"name": "display-ambient-light-desktop",
|
||||||
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"description": "",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "tsc && vite build",
|
||||||
"serve": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solidjs/router": "^0.8.2",
|
"@emotion/react": "^11.10.5",
|
||||||
"@tauri-apps/api": "^1.3.0",
|
"@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",
|
"debug": "^4.3.4",
|
||||||
"solid-icons": "^1.0.8",
|
"notistack": "^2.0.8",
|
||||||
"solid-js": "^1.7.6",
|
"ramda": "^0.28.0",
|
||||||
"solid-tippy": "^0.2.1",
|
"react": "^18.2.0",
|
||||||
"tippy.js": "^6.3.7"
|
"react-async-hook": "^4.0.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^1.3.1",
|
"@babel/plugin-transform-react-jsx": "^7.20.7",
|
||||||
"@types/debug": "^4.1.8",
|
"@emotion/babel-plugin-jsx-pragmatic": "^0.2.0",
|
||||||
"@types/node": "^18.16.17",
|
"@emotion/serialize": "^1.1.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"@tauri-apps/cli": "^1.2.2",
|
||||||
"postcss": "^8.4.24",
|
"@types/debug": "^4.1.7",
|
||||||
"tailwindcss": "^3.3.2",
|
"@types/node": "^18.11.18",
|
||||||
"typescript": "^4.9.5",
|
"@types/ramda": "^0.28.20",
|
||||||
"vite": "^4.3.9",
|
"@types/react": "^18.0.26",
|
||||||
"vite-plugin-solid": "^2.7.0"
|
"@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",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2973
pnpm-lock.yaml
2973
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
@ -1,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
2733
src-tauri/Cargo.lock
generated
2733
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,45 +1,49 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "test-demo"
|
name = "display-ambient-light-desktop"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = ""
|
repository = ""
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
rust-version = "1.57"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
tauri-build = { version = "1.2", features = [] }
|
tauri-build = { version = "1.1", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "1.2", features = ["shell-open"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
core-graphics = "0.22.3"
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
display-info = "0.4.1"
|
tauri = { version = "1.1", features = ["api-all"] }
|
||||||
anyhow = "1.0.69"
|
scrap = "0.5"
|
||||||
tokio = {version = "1.26.0", features = ["full"] }
|
bmp = "0.5.0"
|
||||||
|
webp = "0.2.2"
|
||||||
|
base64 = "0.13.1"
|
||||||
|
anyhow = "1.0.66"
|
||||||
|
once_cell = "1.16.0"
|
||||||
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
paris = { version = "1.5", features = ["timestamps", "macros"] }
|
||||||
log = "0.4.17"
|
tokio = { version = "1.22.0", features = ["full"] }
|
||||||
env_logger = "0.10.0"
|
tracing = "0.1.37"
|
||||||
percent-encoding = "2.2.0"
|
tracing-subscriber = "0.3.16"
|
||||||
url-build-parse = "9.0.0"
|
|
||||||
color_space = "0.5.3"
|
|
||||||
hex = "0.4.3"
|
hex = "0.4.3"
|
||||||
toml = "0.7.3"
|
rumqttc = "0.17.0"
|
||||||
paho-mqtt = "0.12.1"
|
time = { version = "0.3.17", features = ["formatting"] }
|
||||||
time = {version="0.3.20", features= ["formatting"] }
|
color_space = "0.5.3"
|
||||||
itertools = "0.10.5"
|
futures = "0.3.25"
|
||||||
core-foundation = "0.9.3"
|
either = "1.8.0"
|
||||||
tokio-stream = "0.1.14"
|
image = "0.24.5"
|
||||||
mdns-sd = "0.7.2"
|
mdns = "3.0.0"
|
||||||
futures = "0.3.28"
|
macos-app-nap = "0.0.1"
|
||||||
ddc-hi = "0.4.1"
|
|
||||||
coreaudio-rs = "0.11.2"
|
|
||||||
rust_swift_screencapture = { version = "0.1.1", path = "../../../../demo/rust-swift-screencapture" }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
# by default Tauri runs in production mode
|
||||||
# DO NOT REMOVE!!
|
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
default = [ "custom-protocol" ]
|
||||||
|
# this feature is used used for production builds where `devPath` points to the filesystem
|
||||||
|
# DO NOT remove this
|
||||||
|
custom-protocol = [ "tauri/custom-protocol" ]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
test_dir = "0.2.0"
|
||||||
|
@ -1,161 +0,0 @@
|
|||||||
use std::env::current_dir;
|
|
||||||
|
|
||||||
use display_info::DisplayInfo;
|
|
||||||
use paris::{error, info};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tauri::api::path::config_dir;
|
|
||||||
|
|
||||||
use crate::screenshot::LedSamplePoints;
|
|
||||||
|
|
||||||
const CONFIG_FILE_NAME: &str = "cc.ivanli.ambient_light/led_strip_config.toml";
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, PartialEq)]
|
|
||||||
pub enum Border {
|
|
||||||
Top,
|
|
||||||
Bottom,
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct LedStripConfig {
|
|
||||||
pub index: usize,
|
|
||||||
pub border: Border,
|
|
||||||
pub display_id: u32,
|
|
||||||
pub start_pos: usize,
|
|
||||||
pub len: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ColorCalibration {
|
|
||||||
r: f32,
|
|
||||||
g: f32,
|
|
||||||
b: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ColorCalibration {
|
|
||||||
pub fn to_bytes(&self) -> [u8; 3] {
|
|
||||||
[
|
|
||||||
(self.r * 255.0) as u8,
|
|
||||||
(self.g * 255.0) as u8,
|
|
||||||
(self.b * 255.0) as u8,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct LedStripConfigGroup {
|
|
||||||
pub strips: Vec<LedStripConfig>,
|
|
||||||
pub mappers: Vec<SamplePointMapper>,
|
|
||||||
pub color_calibration: ColorCalibration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LedStripConfigGroup {
|
|
||||||
pub async fn read_config() -> anyhow::Result<Self> {
|
|
||||||
let displays = DisplayInfo::all()?;
|
|
||||||
|
|
||||||
// config path
|
|
||||||
let path = config_dir()
|
|
||||||
.unwrap_or(current_dir().unwrap())
|
|
||||||
.join(CONFIG_FILE_NAME);
|
|
||||||
|
|
||||||
let exists = tokio::fs::try_exists(path.clone())
|
|
||||||
.await
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to check config file exists: {}", e))?;
|
|
||||||
|
|
||||||
if exists {
|
|
||||||
let config = tokio::fs::read_to_string(path).await?;
|
|
||||||
|
|
||||||
let mut config: LedStripConfigGroup = toml::from_str(&config)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to parse config file: {}", e))?;
|
|
||||||
|
|
||||||
for strip in config.strips.iter_mut() {
|
|
||||||
strip.display_id = displays[strip.index / 4].id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// log::info!("config loaded: {:?}", config);
|
|
||||||
|
|
||||||
Ok(config)
|
|
||||||
} else {
|
|
||||||
info!("config file not exist, fallback to default config");
|
|
||||||
Ok(Self::get_default_config().await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn write_config(configs: &Self) -> anyhow::Result<()> {
|
|
||||||
let path = config_dir()
|
|
||||||
.unwrap_or(current_dir().unwrap())
|
|
||||||
.join(CONFIG_FILE_NAME);
|
|
||||||
|
|
||||||
tokio::fs::create_dir_all(path.parent().unwrap()).await?;
|
|
||||||
|
|
||||||
let config_text = toml::to_string(&configs).map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to parse config file: {}. configs: {:?}", e, configs)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
tokio::fs::write(&path, config_text).await.map_err(|e| {
|
|
||||||
anyhow::anyhow!("Failed to write config file: {}. path: {:?}", e, &path)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_default_config() -> anyhow::Result<Self> {
|
|
||||||
let displays = display_info::DisplayInfo::all().map_err(|e| {
|
|
||||||
error!("can not list display info: {}", e);
|
|
||||||
anyhow::anyhow!("can not list display info: {}", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut strips = Vec::new();
|
|
||||||
let mut mappers = Vec::new();
|
|
||||||
for (i, display) in displays.iter().enumerate() {
|
|
||||||
let mut configs = Vec::new();
|
|
||||||
for j in 0..4 {
|
|
||||||
let item = LedStripConfig {
|
|
||||||
index: j + i * 4,
|
|
||||||
display_id: display.id,
|
|
||||||
border: match j {
|
|
||||||
0 => Border::Top,
|
|
||||||
1 => Border::Bottom,
|
|
||||||
2 => Border::Left,
|
|
||||||
3 => Border::Right,
|
|
||||||
_ => unreachable!(),
|
|
||||||
},
|
|
||||||
start_pos: j + i * 4 * 30,
|
|
||||||
len: 30,
|
|
||||||
};
|
|
||||||
configs.push(item);
|
|
||||||
strips.push(item);
|
|
||||||
mappers.push(SamplePointMapper {
|
|
||||||
start: (j + i * 4) * 30,
|
|
||||||
end: (j + i * 4 + 1) * 30,
|
|
||||||
pos: (j + i * 4) * 30,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let color_calibration = ColorCalibration {
|
|
||||||
r: 1.0,
|
|
||||||
g: 1.0,
|
|
||||||
b: 1.0,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
strips,
|
|
||||||
mappers,
|
|
||||||
color_calibration,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct SamplePointMapper {
|
|
||||||
pub start: usize,
|
|
||||||
pub end: usize,
|
|
||||||
pub pos: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct SamplePointConfig {
|
|
||||||
pub display_id: u32,
|
|
||||||
pub points: Vec<LedSamplePoints>,
|
|
||||||
}
|
|
@ -1,239 +0,0 @@
|
|||||||
use std::{borrow::BorrowMut, sync::Arc};
|
|
||||||
|
|
||||||
use tauri::async_runtime::RwLock;
|
|
||||||
use tokio::{sync::OnceCell, task::yield_now};
|
|
||||||
|
|
||||||
use crate::ambient_light::{config, LedStripConfigGroup};
|
|
||||||
|
|
||||||
use super::{Border, SamplePointMapper, ColorCalibration};
|
|
||||||
|
|
||||||
pub struct ConfigManager {
|
|
||||||
config: Arc<RwLock<LedStripConfigGroup>>,
|
|
||||||
config_update_sender: tokio::sync::watch::Sender<LedStripConfigGroup>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ConfigManager {
|
|
||||||
pub async fn global() -> &'static Self {
|
|
||||||
static CONFIG_MANAGER_GLOBAL: OnceCell<ConfigManager> = OnceCell::const_new();
|
|
||||||
CONFIG_MANAGER_GLOBAL
|
|
||||||
.get_or_init(|| async {
|
|
||||||
let configs = LedStripConfigGroup::read_config().await.unwrap();
|
|
||||||
let (config_update_sender, config_update_receiver) =
|
|
||||||
tokio::sync::watch::channel(configs.clone());
|
|
||||||
|
|
||||||
if let Err(err) = config_update_sender.send(configs.clone()) {
|
|
||||||
log::error!("Failed to send config update when read config first time: {}", err);
|
|
||||||
}
|
|
||||||
drop(config_update_receiver);
|
|
||||||
ConfigManager {
|
|
||||||
config: Arc::new(RwLock::new(configs)),
|
|
||||||
config_update_sender,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reload(&self) -> anyhow::Result<()> {
|
|
||||||
let mut configs = self.config.write().await;
|
|
||||||
*configs = LedStripConfigGroup::read_config().await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn update(&self, configs: &LedStripConfigGroup) -> anyhow::Result<()> {
|
|
||||||
LedStripConfigGroup::write_config(configs).await?;
|
|
||||||
self.reload().await?;
|
|
||||||
|
|
||||||
self.config_update_sender
|
|
||||||
.send(configs.clone())
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
|
|
||||||
yield_now().await;
|
|
||||||
|
|
||||||
log::debug!("config updated: {:?}", configs);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn configs(&self) -> LedStripConfigGroup {
|
|
||||||
self.config.read().await.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn patch_led_strip_len(
|
|
||||||
&self,
|
|
||||||
display_id: u32,
|
|
||||||
border: Border,
|
|
||||||
delta_len: i8,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut config = self.config.write().await;
|
|
||||||
|
|
||||||
for strip in config.strips.iter_mut() {
|
|
||||||
if strip.display_id == display_id && strip.border == border {
|
|
||||||
let target = strip.len as i64 + delta_len as i64;
|
|
||||||
if target < 0 || target > 1000 {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Overflow. range: 0-1000, current: {}",
|
|
||||||
target
|
|
||||||
));
|
|
||||||
}
|
|
||||||
strip.len = target as usize;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Self::rebuild_mappers(&mut config);
|
|
||||||
|
|
||||||
let cloned_config = config.clone();
|
|
||||||
|
|
||||||
drop(config);
|
|
||||||
|
|
||||||
self.update(&cloned_config).await?;
|
|
||||||
|
|
||||||
self.config_update_sender
|
|
||||||
.send(cloned_config)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn move_strip_part(
|
|
||||||
&self,
|
|
||||||
display_id: u32,
|
|
||||||
border: Border,
|
|
||||||
target_start: usize,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut config = self.config.write().await;
|
|
||||||
|
|
||||||
for (index, strip) in config.clone().strips.iter().enumerate() {
|
|
||||||
if strip.display_id == display_id && strip.border == border {
|
|
||||||
let mut mapper = config.mappers[index].borrow_mut();
|
|
||||||
|
|
||||||
if target_start == mapper.start {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let target_end = mapper.end + target_start - mapper.start;
|
|
||||||
|
|
||||||
if target_start > 1000 || target_end > 1000 {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Overflow. range: 0-1000, current: {}-{}",
|
|
||||||
target_start,
|
|
||||||
target_end
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
mapper.start = target_start as usize;
|
|
||||||
mapper.end = target_end as usize;
|
|
||||||
|
|
||||||
log::info!("mapper: {:?}", mapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cloned_config = config.clone();
|
|
||||||
|
|
||||||
drop(config);
|
|
||||||
|
|
||||||
self.update(&cloned_config).await?;
|
|
||||||
|
|
||||||
self.config_update_sender
|
|
||||||
.send(cloned_config)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn reverse_led_strip_part(
|
|
||||||
&self,
|
|
||||||
display_id: u32,
|
|
||||||
border: Border,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut config = self.config.write().await;
|
|
||||||
|
|
||||||
for (index, strip) in config.clone().strips.iter().enumerate() {
|
|
||||||
if strip.display_id == display_id && strip.border == border {
|
|
||||||
let mut mapper = config.mappers[index].borrow_mut();
|
|
||||||
|
|
||||||
let start = mapper.start;
|
|
||||||
mapper.start = mapper.end;
|
|
||||||
mapper.end = start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let cloned_config = config.clone();
|
|
||||||
|
|
||||||
drop(config);
|
|
||||||
|
|
||||||
self.update(&cloned_config).await?;
|
|
||||||
|
|
||||||
self.config_update_sender
|
|
||||||
.send(cloned_config)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rebuild_mappers(config: &mut LedStripConfigGroup) {
|
|
||||||
let mut prev_pos_end = 0;
|
|
||||||
let mappers: Vec<SamplePointMapper> = config
|
|
||||||
.strips
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(index, strip)| {
|
|
||||||
let mapper = &config.mappers[index];
|
|
||||||
|
|
||||||
if mapper.start < mapper.end {
|
|
||||||
let mapper = SamplePointMapper {
|
|
||||||
start: mapper.start,
|
|
||||||
end: mapper.start + strip.len,
|
|
||||||
pos: prev_pos_end,
|
|
||||||
};
|
|
||||||
prev_pos_end = prev_pos_end + strip.len;
|
|
||||||
mapper
|
|
||||||
} else {
|
|
||||||
let mapper = SamplePointMapper {
|
|
||||||
end: mapper.end,
|
|
||||||
start: mapper.end + strip.len,
|
|
||||||
pos: prev_pos_end,
|
|
||||||
};
|
|
||||||
prev_pos_end = prev_pos_end + strip.len;
|
|
||||||
mapper
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
config.mappers = mappers;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_items(&self, items: Vec<config::LedStripConfig>) -> anyhow::Result<()> {
|
|
||||||
let mut config = self.config.write().await;
|
|
||||||
|
|
||||||
config.strips = items;
|
|
||||||
|
|
||||||
let cloned_config = config.clone();
|
|
||||||
|
|
||||||
drop(config);
|
|
||||||
|
|
||||||
self.update(&cloned_config).await?;
|
|
||||||
|
|
||||||
self.config_update_sender
|
|
||||||
.send(cloned_config)
|
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to send config update: {}", e))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clone_config_update_receiver(
|
|
||||||
&self,
|
|
||||||
) -> tokio::sync::watch::Receiver<LedStripConfigGroup> {
|
|
||||||
self.config_update_sender.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_color_calibration(&self, color_calibration: ColorCalibration) -> anyhow::Result<()> {
|
|
||||||
let config = self.config.write().await;
|
|
||||||
|
|
||||||
let mut cloned_config = config.clone();
|
|
||||||
cloned_config.color_calibration = color_calibration;
|
|
||||||
|
|
||||||
drop(config);
|
|
||||||
|
|
||||||
self.update(&cloned_config).await
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
mod config;
|
|
||||||
mod config_manager;
|
|
||||||
mod publisher;
|
|
||||||
|
|
||||||
pub use config::*;
|
|
||||||
pub use config_manager::*;
|
|
||||||
pub use publisher::*;
|
|
@ -1,443 +0,0 @@
|
|||||||
use std::{borrow::Borrow, collections::HashMap, sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use paris::warn;
|
|
||||||
use tauri::async_runtime::RwLock;
|
|
||||||
use tokio::{
|
|
||||||
net::UdpSocket,
|
|
||||||
sync::{broadcast, watch},
|
|
||||||
time::sleep,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ambient_light::{config, ConfigManager},
|
|
||||||
led_color::LedColor,
|
|
||||||
rpc::UdpRpc,
|
|
||||||
screenshot::{self, LedSamplePoints},
|
|
||||||
screenshot_manager::{self, ScreenshotManager},
|
|
||||||
};
|
|
||||||
|
|
||||||
use itertools::Itertools;
|
|
||||||
|
|
||||||
use super::{LedStripConfigGroup, SamplePointMapper};
|
|
||||||
|
|
||||||
pub struct LedColorsPublisher {
|
|
||||||
sorted_colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
|
||||||
sorted_colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
|
|
||||||
colors_rx: Arc<RwLock<watch::Receiver<Vec<u8>>>>,
|
|
||||||
colors_tx: Arc<RwLock<watch::Sender<Vec<u8>>>>,
|
|
||||||
inner_tasks_version: Arc<RwLock<usize>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl LedColorsPublisher {
|
|
||||||
pub async fn global() -> &'static Self {
|
|
||||||
static LED_COLORS_PUBLISHER_GLOBAL: tokio::sync::OnceCell<LedColorsPublisher> =
|
|
||||||
tokio::sync::OnceCell::const_new();
|
|
||||||
|
|
||||||
let (sorted_tx, sorted_rx) = watch::channel(Vec::new());
|
|
||||||
let (tx, rx) = watch::channel(Vec::new());
|
|
||||||
|
|
||||||
LED_COLORS_PUBLISHER_GLOBAL
|
|
||||||
.get_or_init(|| async {
|
|
||||||
LedColorsPublisher {
|
|
||||||
sorted_colors_rx: Arc::new(RwLock::new(sorted_rx)),
|
|
||||||
sorted_colors_tx: Arc::new(RwLock::new(sorted_tx)),
|
|
||||||
colors_rx: Arc::new(RwLock::new(rx)),
|
|
||||||
colors_tx: Arc::new(RwLock::new(tx)),
|
|
||||||
inner_tasks_version: Arc::new(RwLock::new(0)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_one_display_colors_fetcher(
|
|
||||||
&self,
|
|
||||||
display_id: u32,
|
|
||||||
sample_points: Vec<LedSamplePoints>,
|
|
||||||
bound_scale_factor: f32,
|
|
||||||
mappers: Vec<SamplePointMapper>,
|
|
||||||
display_colors_tx: broadcast::Sender<(u32, Vec<u8>)>,
|
|
||||||
) {
|
|
||||||
let internal_tasks_version = self.inner_tasks_version.clone();
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
|
||||||
|
|
||||||
let screenshot_rx = screenshot_manager.subscribe_by_display_id(display_id).await;
|
|
||||||
|
|
||||||
if let Err(err) = screenshot_rx {
|
|
||||||
log::error!("{}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let mut screenshot_rx = screenshot_rx.unwrap();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let init_version = internal_tasks_version.read().await.clone();
|
|
||||||
|
|
||||||
while screenshot_rx.changed().await.is_ok() {
|
|
||||||
let screenshot = screenshot_rx.borrow().clone();
|
|
||||||
let colors = screenshot.get_colors_by_sample_points(&sample_points).await;
|
|
||||||
|
|
||||||
let colors_copy = colors.clone();
|
|
||||||
|
|
||||||
let mappers = mappers.clone();
|
|
||||||
|
|
||||||
match Self::send_colors_by_display(colors, mappers).await {
|
|
||||||
Ok(_) => {
|
|
||||||
// log::info!("sent colors: #{: >15}", display_id);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to send colors: #{: >15}\t{}", display_id, err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// match display_colors_tx.send((
|
|
||||||
// display_id,
|
|
||||||
// colors_copy
|
|
||||||
// .into_iter()
|
|
||||||
// .map(|color| color.get_rgb())
|
|
||||||
// .flatten()
|
|
||||||
// .collect::<Vec<_>>(),
|
|
||||||
// )) {
|
|
||||||
// Ok(_) => {
|
|
||||||
// // log::info!("sent colors: {:?}", color_len);
|
|
||||||
// }
|
|
||||||
// Err(err) => {
|
|
||||||
// warn!("Failed to send display_colors: {}", err);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
// Check if the inner task version changed
|
|
||||||
let version = internal_tasks_version.read().await.clone();
|
|
||||||
if version != init_version {
|
|
||||||
log::info!(
|
|
||||||
"inner task version changed, stop. {} != {}",
|
|
||||||
internal_tasks_version.read().await.clone(),
|
|
||||||
init_version
|
|
||||||
);
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_all_colors_worker(
|
|
||||||
&self,
|
|
||||||
display_ids: Vec<u32>,
|
|
||||||
mappers: Vec<SamplePointMapper>,
|
|
||||||
mut display_colors_rx: broadcast::Receiver<(u32, Vec<u8>)>,
|
|
||||||
) {
|
|
||||||
let sorted_colors_tx = self.sorted_colors_tx.clone();
|
|
||||||
let colors_tx = self.colors_tx.clone();
|
|
||||||
log::debug!("start all_colors_worker");
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
for _ in 0..10 {
|
|
||||||
let sorted_colors_tx = sorted_colors_tx.write().await;
|
|
||||||
let colors_tx = colors_tx.write().await;
|
|
||||||
|
|
||||||
let mut all_colors: Vec<Option<Vec<u8>>> = vec![None; display_ids.len()];
|
|
||||||
let mut start: tokio::time::Instant = tokio::time::Instant::now();
|
|
||||||
|
|
||||||
log::debug!("start all_colors_worker task");
|
|
||||||
loop {
|
|
||||||
let color_info = display_colors_rx.recv().await;
|
|
||||||
|
|
||||||
if let Err(err) = color_info {
|
|
||||||
match err {
|
|
||||||
broadcast::error::RecvError::Closed => {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
broadcast::error::RecvError::Lagged(_) => {
|
|
||||||
warn!("display_colors_rx lagged");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (display_id, colors) = color_info.unwrap();
|
|
||||||
|
|
||||||
let index = display_ids.iter().position(|id| *id == display_id);
|
|
||||||
|
|
||||||
if index.is_none() {
|
|
||||||
warn!("display id not found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
all_colors[index.unwrap()] = Some(colors);
|
|
||||||
|
|
||||||
if all_colors.iter().all(|color| color.is_some()) {
|
|
||||||
let flatten_colors = all_colors
|
|
||||||
.clone()
|
|
||||||
.into_iter()
|
|
||||||
.flat_map(|c| c.unwrap())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
match colors_tx.send(flatten_colors.clone()) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to send colors: {}", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let sorted_colors =
|
|
||||||
ScreenshotManager::get_sorted_colors(&flatten_colors, &mappers);
|
|
||||||
|
|
||||||
match sorted_colors_tx.send(sorted_colors) {
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Failed to send sorted colors: {}", err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
log::debug!("tick: {}ms", start.elapsed().as_millis());
|
|
||||||
start = tokio::time::Instant::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start(&self) {
|
|
||||||
log::info!("start colors worker");
|
|
||||||
|
|
||||||
let config_manager = ConfigManager::global().await;
|
|
||||||
let mut config_receiver = config_manager.clone_config_update_receiver();
|
|
||||||
let configs = config_receiver.borrow().clone();
|
|
||||||
|
|
||||||
self.handle_config_change(configs).await;
|
|
||||||
|
|
||||||
log::info!("waiting for config update...");
|
|
||||||
while config_receiver.changed().await.is_ok() {
|
|
||||||
log::info!("config updated, restart inner tasks...");
|
|
||||||
let configs = config_receiver.borrow().clone();
|
|
||||||
self.handle_config_change(configs).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_config_change(&self, configs: LedStripConfigGroup) {
|
|
||||||
let inner_tasks_version = self.inner_tasks_version.clone();
|
|
||||||
let configs = Self::get_colors_configs(&configs).await;
|
|
||||||
|
|
||||||
if let Err(err) = configs {
|
|
||||||
warn!("Failed to get configs: {}", err);
|
|
||||||
sleep(Duration::from_millis(100)).await;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let configs = configs.unwrap();
|
|
||||||
|
|
||||||
let mut inner_tasks_version = inner_tasks_version.write().await;
|
|
||||||
*inner_tasks_version = inner_tasks_version.overflowing_add(1).0;
|
|
||||||
drop(inner_tasks_version);
|
|
||||||
|
|
||||||
let (display_colors_tx, display_colors_rx) = broadcast::channel::<(u32, Vec<u8>)>(8);
|
|
||||||
|
|
||||||
for sample_point_group in configs.sample_point_groups.clone() {
|
|
||||||
let display_id = sample_point_group.display_id;
|
|
||||||
let sample_points = sample_point_group.points;
|
|
||||||
let bound_scale_factor = sample_point_group.bound_scale_factor;
|
|
||||||
self.start_one_display_colors_fetcher(
|
|
||||||
display_id,
|
|
||||||
sample_points,
|
|
||||||
bound_scale_factor,
|
|
||||||
sample_point_group.mappers,
|
|
||||||
display_colors_tx.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let display_ids = configs.sample_point_groups;
|
|
||||||
self.start_all_colors_worker(
|
|
||||||
display_ids.iter().map(|c| c.display_id).collect(),
|
|
||||||
configs.mappers,
|
|
||||||
display_colors_rx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_colors(offset: u16, mut payload: Vec<u8>) -> anyhow::Result<()> {
|
|
||||||
// let mqtt = MqttRpc::global().await;
|
|
||||||
|
|
||||||
// mqtt.publish_led_sub_pixels(payload).await;
|
|
||||||
|
|
||||||
let socket = UdpSocket::bind("0.0.0.0:8000").await?;
|
|
||||||
let mut buffer = vec![2];
|
|
||||||
buffer.push((offset >> 8) as u8);
|
|
||||||
buffer.push((offset & 0xff) as u8);
|
|
||||||
buffer.append(&mut payload);
|
|
||||||
socket.send_to(&buffer, "192.168.31.206:23042").await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_colors_by_display(
|
|
||||||
colors: Vec<LedColor>,
|
|
||||||
mappers: Vec<SamplePointMapper>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
// let color_len = colors.len();
|
|
||||||
let display_led_offset = mappers
|
|
||||||
.clone()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|mapper| [mapper.start, mapper.end])
|
|
||||||
.min()
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let udp_rpc = UdpRpc::global().await;
|
|
||||||
if let Err(err) = udp_rpc {
|
|
||||||
warn!("udp_rpc can not be initialized: {}", err);
|
|
||||||
}
|
|
||||||
let udp_rpc = udp_rpc.as_ref().unwrap();
|
|
||||||
|
|
||||||
// let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
|
||||||
for group in mappers.clone() {
|
|
||||||
if (group.start.abs_diff(group.end)) > colors.len() {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"get_sorted_colors: color_index out of range. color_index: {}, strip len: {}, colors.len(): {}",
|
|
||||||
group.pos,
|
|
||||||
group.start.abs_diff(group.end),
|
|
||||||
colors.len()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let group_size = group.start.abs_diff(group.end);
|
|
||||||
let mut buffer = Vec::<u8>::with_capacity(group_size * 3);
|
|
||||||
|
|
||||||
if group.end > group.start {
|
|
||||||
for i in group.pos - display_led_offset..group_size + group.pos - display_led_offset
|
|
||||||
{
|
|
||||||
let bytes = colors[i].as_bytes();
|
|
||||||
buffer.append(&mut bytes.to_vec());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i in (group.pos - display_led_offset
|
|
||||||
..group_size + group.pos - display_led_offset)
|
|
||||||
.rev()
|
|
||||||
{
|
|
||||||
let bytes = colors[i].as_bytes();
|
|
||||||
buffer.append(&mut bytes.to_vec());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = group.start.min(group.end);
|
|
||||||
let mut tx_buffer = vec![2];
|
|
||||||
tx_buffer.push((offset >> 8) as u8);
|
|
||||||
tx_buffer.push((offset & 0xff) as u8);
|
|
||||||
tx_buffer.append(&mut buffer);
|
|
||||||
|
|
||||||
udp_rpc.send_to_all(&tx_buffer).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clone_sorted_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
|
|
||||||
self.sorted_colors_rx.read().await.clone()
|
|
||||||
}
|
|
||||||
pub async fn get_colors_configs(
|
|
||||||
configs: &LedStripConfigGroup,
|
|
||||||
) -> anyhow::Result<AllColorConfig> {
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
|
||||||
|
|
||||||
let display_ids = configs
|
|
||||||
.strips
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.display_id)
|
|
||||||
.unique()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mappers = configs.mappers.clone();
|
|
||||||
|
|
||||||
let mut colors_configs = Vec::new();
|
|
||||||
|
|
||||||
let mut merged_screenshot_receiver = screenshot_manager.clone_merged_screenshot_rx().await;
|
|
||||||
merged_screenshot_receiver.resubscribe();
|
|
||||||
|
|
||||||
let mut screenshots = HashMap::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
log::info!("waiting merged screenshot...");
|
|
||||||
let screenshot = merged_screenshot_receiver.recv().await;
|
|
||||||
|
|
||||||
if let Err(err) = screenshot {
|
|
||||||
match err {
|
|
||||||
tokio::sync::broadcast::error::RecvError::Closed => {
|
|
||||||
warn!("closed");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
tokio::sync::broadcast::error::RecvError::Lagged(_) => {
|
|
||||||
warn!("lagged");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let screenshot = screenshot.unwrap();
|
|
||||||
// log::info!("got screenshot: {:?}", screenshot.display_id);
|
|
||||||
|
|
||||||
screenshots.insert(screenshot.display_id, screenshot);
|
|
||||||
|
|
||||||
if screenshots.len() == display_ids.len() {
|
|
||||||
let mut led_start = 0;
|
|
||||||
|
|
||||||
for display_id in display_ids {
|
|
||||||
let led_strip_configs = configs
|
|
||||||
.strips
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(_, c)| c.display_id == display_id);
|
|
||||||
|
|
||||||
let screenshot = screenshots.get(&display_id).unwrap();
|
|
||||||
log::debug!("screenshot updated: {:?}", display_id);
|
|
||||||
|
|
||||||
let points: Vec<_> = led_strip_configs
|
|
||||||
.clone()
|
|
||||||
.map(|(_, config)| screenshot.get_sample_points(&config))
|
|
||||||
.flatten()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if points.len() == 0 {
|
|
||||||
warn!("no led strip config for display_id: {}", display_id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bound_scale_factor = screenshot.bound_scale_factor;
|
|
||||||
|
|
||||||
let led_end = led_start + points.iter().map(|p| p.len()).sum::<usize>();
|
|
||||||
|
|
||||||
let mappers = led_strip_configs.map(|(i, _)| mappers[i].clone()).collect();
|
|
||||||
|
|
||||||
let colors_config = DisplaySamplePointGroup {
|
|
||||||
display_id,
|
|
||||||
points,
|
|
||||||
bound_scale_factor,
|
|
||||||
mappers,
|
|
||||||
};
|
|
||||||
|
|
||||||
colors_configs.push(colors_config);
|
|
||||||
led_start = led_end;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("got all colors configs: {:?}", colors_configs.len());
|
|
||||||
|
|
||||||
return Ok(AllColorConfig {
|
|
||||||
sample_point_groups: colors_configs,
|
|
||||||
mappers,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clone_colors_receiver(&self) -> watch::Receiver<Vec<u8>> {
|
|
||||||
self.colors_rx.read().await.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AllColorConfig {
|
|
||||||
pub sample_point_groups: Vec<DisplaySamplePointGroup>,
|
|
||||||
pub mappers: Vec<config::SamplePointMapper>,
|
|
||||||
// pub screenshot_receivers: Vec<watch::Receiver<Screenshot>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct DisplaySamplePointGroup {
|
|
||||||
pub display_id: u32,
|
|
||||||
pub points: Vec<LedSamplePoints>,
|
|
||||||
pub bound_scale_factor: f32,
|
|
||||||
pub mappers: Vec<config::SamplePointMapper>,
|
|
||||||
}
|
|
238
src-tauri/src/core/core.rs
Normal file
238
src-tauri/src/core/core.rs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
3
src-tauri/src/core/mod.rs
Normal file
3
src-tauri/src/core/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod core;
|
||||||
|
|
||||||
|
pub use self::core::*;
|
@ -1,13 +0,0 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub enum Brightness {
|
|
||||||
Relative(i16),
|
|
||||||
Absolute(u16),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct DisplayBrightness {
|
|
||||||
pub brightness: Brightness,
|
|
||||||
pub display_index: usize,
|
|
||||||
}
|
|
@ -1,96 +0,0 @@
|
|||||||
use std::{sync::Arc, time::SystemTime};
|
|
||||||
|
|
||||||
use ddc_hi::{Ddc, Display};
|
|
||||||
use tokio::sync::RwLock;
|
|
||||||
|
|
||||||
use super::DisplayState;
|
|
||||||
|
|
||||||
pub struct DisplayHandler {
|
|
||||||
pub state: Arc<RwLock<DisplayState>>,
|
|
||||||
pub controller: Arc<RwLock<Display>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayHandler {
|
|
||||||
pub async fn fetch_state(&self) {
|
|
||||||
let mut controller = self.controller.write().await;
|
|
||||||
|
|
||||||
let mut temp_state = self.state.read().await.clone();
|
|
||||||
|
|
||||||
match controller.handle.get_vcp_feature(0x10) {
|
|
||||||
Ok(value) => {
|
|
||||||
temp_state.max_brightness = value.maximum();
|
|
||||||
temp_state.min_brightness = 0;
|
|
||||||
temp_state.brightness = value.value();
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
};
|
|
||||||
match controller.handle.get_vcp_feature(0x12) {
|
|
||||||
Ok(value) => {
|
|
||||||
temp_state.max_contrast = value.maximum();
|
|
||||||
temp_state.min_contrast = 0;
|
|
||||||
temp_state.contrast = value.value();
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
};
|
|
||||||
match controller.handle.get_vcp_feature(0xdc) {
|
|
||||||
Ok(value) => {
|
|
||||||
temp_state.max_mode = value.maximum();
|
|
||||||
temp_state.min_mode = 0;
|
|
||||||
temp_state.mode = value.value();
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
};
|
|
||||||
|
|
||||||
temp_state.last_fetched_at = SystemTime::now();
|
|
||||||
|
|
||||||
let mut state = self.state.write().await;
|
|
||||||
*state = temp_state;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_brightness(&self, brightness: u16) -> anyhow::Result<()> {
|
|
||||||
let mut controller = self.controller.write().await;
|
|
||||||
let mut state = self.state.write().await;
|
|
||||||
|
|
||||||
controller
|
|
||||||
.handle
|
|
||||||
.set_vcp_feature(0x10, brightness)
|
|
||||||
.map_err(|err| anyhow::anyhow!("can not set brightness. {:?}", err))?;
|
|
||||||
|
|
||||||
|
|
||||||
state.brightness = brightness;
|
|
||||||
|
|
||||||
state.last_modified_at = SystemTime::now();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_contrast(&self, contrast: u16) -> anyhow::Result<()> {
|
|
||||||
let mut controller = self.controller.write().await;
|
|
||||||
let mut state = self.state.write().await;
|
|
||||||
|
|
||||||
controller
|
|
||||||
.handle
|
|
||||||
.set_vcp_feature(0x12, contrast)
|
|
||||||
.map_err(|err| anyhow::anyhow!("can not set contrast. {:?}", err))?;
|
|
||||||
|
|
||||||
state.contrast = contrast;
|
|
||||||
state.last_modified_at = SystemTime::now();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set_mode(&self, mode: u16) -> anyhow::Result<()> {
|
|
||||||
let mut controller = self.controller.write().await;
|
|
||||||
let mut state = self.state.write().await;
|
|
||||||
|
|
||||||
controller
|
|
||||||
.handle
|
|
||||||
.set_vcp_feature(0xdc, mode)
|
|
||||||
.map_err(|err| anyhow::anyhow!("can not set mode. {:?}", err))?;
|
|
||||||
|
|
||||||
state.mode = mode;
|
|
||||||
state.last_modified_at = SystemTime::now();
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
use std::time::SystemTime;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct DisplayState {
|
|
||||||
pub brightness: u16,
|
|
||||||
pub max_brightness: u16,
|
|
||||||
pub min_brightness: u16,
|
|
||||||
pub contrast: u16,
|
|
||||||
pub max_contrast: u16,
|
|
||||||
pub min_contrast: u16,
|
|
||||||
pub mode: u16,
|
|
||||||
pub max_mode: u16,
|
|
||||||
pub min_mode: u16,
|
|
||||||
pub last_modified_at: SystemTime,
|
|
||||||
pub last_fetched_at: SystemTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayState {
|
|
||||||
pub fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
brightness: 30,
|
|
||||||
contrast: 50,
|
|
||||||
mode: 0,
|
|
||||||
last_modified_at: SystemTime::UNIX_EPOCH,
|
|
||||||
max_brightness: 100,
|
|
||||||
min_brightness: 0,
|
|
||||||
max_contrast: 100,
|
|
||||||
min_contrast: 0,
|
|
||||||
max_mode: 15,
|
|
||||||
min_mode: 0,
|
|
||||||
last_fetched_at: SystemTime::UNIX_EPOCH,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct DisplayStateWrapper {
|
|
||||||
pub version: u8,
|
|
||||||
pub states: Vec<DisplayState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayStateWrapper {
|
|
||||||
pub fn new(states: Vec<DisplayState>) -> Self {
|
|
||||||
Self { version: 1, states }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,282 +0,0 @@
|
|||||||
use std::{env::current_dir, sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use ddc_hi::Display;
|
|
||||||
use paris::{error, info, warn};
|
|
||||||
use tauri::api::path::config_dir;
|
|
||||||
use tokio::{
|
|
||||||
sync::{broadcast, watch, OnceCell, RwLock},
|
|
||||||
task::yield_now,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
display::DisplayStateWrapper,
|
|
||||||
rpc::{BoardMessageChannels, DisplaySetting},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{display_handler::DisplayHandler, display_state::DisplayState};
|
|
||||||
|
|
||||||
const CONFIG_FILE_NAME: &str = "cc.ivanli.ambient_light/displays.toml";
|
|
||||||
|
|
||||||
pub struct DisplayManager {
|
|
||||||
displays: Arc<RwLock<Vec<Arc<RwLock<DisplayHandler>>>>>,
|
|
||||||
setting_request_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
displays_changed_sender: Arc<watch::Sender<Vec<DisplayState>>>,
|
|
||||||
auto_save_state_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisplayManager {
|
|
||||||
pub async fn global() -> &'static Self {
|
|
||||||
static DISPLAY_MANAGER: OnceCell<DisplayManager> = OnceCell::const_new();
|
|
||||||
|
|
||||||
DISPLAY_MANAGER.get_or_init(|| Self::create()).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn create() -> Self {
|
|
||||||
let (displays_changed_sender, _) = watch::channel(Vec::new());
|
|
||||||
let displays_changed_sender = Arc::new(displays_changed_sender);
|
|
||||||
|
|
||||||
let mut instance = Self {
|
|
||||||
displays: Arc::new(RwLock::new(Vec::new())),
|
|
||||||
setting_request_handler: None,
|
|
||||||
displays_changed_sender,
|
|
||||||
auto_save_state_handler: None,
|
|
||||||
};
|
|
||||||
instance.fetch_displays().await;
|
|
||||||
instance.restore_states().await;
|
|
||||||
instance.fetch_state_of_displays().await;
|
|
||||||
instance.subscribe_setting_request();
|
|
||||||
instance.auto_save_state_of_displays();
|
|
||||||
instance
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auto_save_state_of_displays(&mut self) {
|
|
||||||
let displays = self.displays.clone();
|
|
||||||
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
|
||||||
Self::save_states(displays.clone()).await;
|
|
||||||
Self::send_displays_changed(displays.clone()).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.auto_save_state_handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn send_displays_changed(displays: Arc<RwLock<Vec<Arc<RwLock<DisplayHandler>>>>>) {
|
|
||||||
let mut states = Vec::new();
|
|
||||||
|
|
||||||
for display in displays.read().await.iter() {
|
|
||||||
let state = display.read().await.state.read().await.clone();
|
|
||||||
states.push(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
let channel = BoardMessageChannels::global().await;
|
|
||||||
let tx = channel.displays_changed_sender.clone();
|
|
||||||
if let Err(err) = tx.send(states) {
|
|
||||||
error!("Failed to send displays changed: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_displays(&self) {
|
|
||||||
let mut displays = self.displays.write().await;
|
|
||||||
displays.clear();
|
|
||||||
|
|
||||||
let controllers = Display::enumerate();
|
|
||||||
|
|
||||||
for display in controllers {
|
|
||||||
let controller = Arc::new(RwLock::new(display));
|
|
||||||
let state = Arc::new(RwLock::new(DisplayState::default()));
|
|
||||||
let handler = DisplayHandler {
|
|
||||||
state: state.clone(),
|
|
||||||
controller: controller.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
displays.push(Arc::new(RwLock::new(handler)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn fetch_state_of_displays(&self) {
|
|
||||||
let displays = self.displays.read().await;
|
|
||||||
|
|
||||||
for display in displays.iter() {
|
|
||||||
let display = display.read().await;
|
|
||||||
display.fetch_state().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_displays(&self) -> Vec<DisplayState> {
|
|
||||||
let displays = self.displays.read().await;
|
|
||||||
let mut states = Vec::new();
|
|
||||||
for display in displays.iter() {
|
|
||||||
let state = display.read().await.state.read().await.clone();
|
|
||||||
states.push(state);
|
|
||||||
}
|
|
||||||
states
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe_setting_request(&mut self) {
|
|
||||||
let displays = self.displays.clone();
|
|
||||||
let displays_changed_sender = self.displays_changed_sender.clone();
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
let channels = BoardMessageChannels::global().await;
|
|
||||||
let mut request_rx = channels.display_setting_request_sender.subscribe();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if let Err(err) = request_rx.recv().await {
|
|
||||||
match err {
|
|
||||||
broadcast::error::RecvError::Closed => {
|
|
||||||
info!("display setting request channel closed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
broadcast::error::RecvError::Lagged(_) => {
|
|
||||||
warn!("display setting request channel lagged");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = request_rx.recv().await.unwrap();
|
|
||||||
|
|
||||||
let displays = displays.write().await;
|
|
||||||
|
|
||||||
let display = displays.get(message.display_index);
|
|
||||||
if display.is_none() {
|
|
||||||
warn!("display#{} not found", message.display_index);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let display = display.unwrap().write().await;
|
|
||||||
let result = match message.setting {
|
|
||||||
DisplaySetting::Brightness(value) => display.set_brightness(value as u16).await,
|
|
||||||
DisplaySetting::Contrast(value) => display.set_contrast(value as u16).await,
|
|
||||||
DisplaySetting::Mode(value) => display.set_mode(value as u16).await,
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
error!("failed to set display setting: {}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(display);
|
|
||||||
|
|
||||||
let mut states = Vec::new();
|
|
||||||
for display in displays.iter() {
|
|
||||||
let state = display.read().await.state.read().await.clone();
|
|
||||||
states.push(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = displays_changed_sender.send(states) {
|
|
||||||
error!("failed to send displays changed event: {}", err);
|
|
||||||
}
|
|
||||||
yield_now().await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.setting_request_handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn restore_states(&self) {
|
|
||||||
let path = config_dir()
|
|
||||||
.unwrap_or(current_dir().unwrap())
|
|
||||||
.join(CONFIG_FILE_NAME);
|
|
||||||
|
|
||||||
if !path.exists() {
|
|
||||||
log::info!("config file not found: {}. skip read.", path.display());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = std::fs::read_to_string(path);
|
|
||||||
if let Err(err) = text {
|
|
||||||
log::error!("failed to read config file: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = text.unwrap();
|
|
||||||
let wrapper = toml::from_str::<DisplayStateWrapper>(&text);
|
|
||||||
|
|
||||||
if let Err(err) = wrapper {
|
|
||||||
log::error!("failed to parse display states file: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let states = wrapper.unwrap().states;
|
|
||||||
|
|
||||||
let displays = self.displays.read().await;
|
|
||||||
for (index, display) in displays.iter().enumerate() {
|
|
||||||
let display = display.read().await;
|
|
||||||
let mut state = display.state.write().await;
|
|
||||||
let saved = states.get(index);
|
|
||||||
if let Some(saved) = saved {
|
|
||||||
state.brightness = saved.brightness;
|
|
||||||
state.contrast = saved.contrast;
|
|
||||||
state.mode = saved.mode;
|
|
||||||
log::info!("restore display config. display#{}: {:?}", index, state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!(
|
|
||||||
"restore display config. store displays: {}, online displays: {}",
|
|
||||||
states.len(),
|
|
||||||
displays.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_states(displays: Arc<RwLock<Vec<Arc<RwLock<DisplayHandler>>>>>) {
|
|
||||||
let path = config_dir()
|
|
||||||
.unwrap_or(current_dir().unwrap())
|
|
||||||
.join(CONFIG_FILE_NAME);
|
|
||||||
|
|
||||||
let displays = displays.read().await;
|
|
||||||
let mut states = Vec::new();
|
|
||||||
for display in displays.iter() {
|
|
||||||
let state = display.read().await.state.read().await.clone();
|
|
||||||
states.push(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
let wrapper = DisplayStateWrapper::new(states);
|
|
||||||
|
|
||||||
let text = toml::to_string(&wrapper);
|
|
||||||
if let Err(err) = text {
|
|
||||||
log::error!("failed to serialize display states: {}", err);
|
|
||||||
log::error!("display states: {:?}", &wrapper);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = text.unwrap();
|
|
||||||
if path.exists() {
|
|
||||||
if let Err(err) = std::fs::remove_file(&path) {
|
|
||||||
log::error!("failed to remove old config file: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(err) = std::fs::write(&path, text) {
|
|
||||||
log::error!("failed to write config file: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!(
|
|
||||||
"save display config. store displays: {}, online displays: {}",
|
|
||||||
wrapper.states.len(),
|
|
||||||
displays.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe_displays_changed(&self) -> watch::Receiver<Vec<DisplayState>> {
|
|
||||||
self.displays_changed_sender.subscribe()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for DisplayManager {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
log::info!("dropping display manager=============");
|
|
||||||
if let Some(handler) = self.setting_request_handler.take() {
|
|
||||||
handler.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handler) = self.auto_save_state_handler.take() {
|
|
||||||
handler.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
// mod brightness;
|
|
||||||
// mod manager;
|
|
||||||
mod display_state;
|
|
||||||
mod manager;
|
|
||||||
mod display_handler;
|
|
||||||
|
|
||||||
pub use display_state::*;
|
|
||||||
|
|
||||||
// pub use brightness::*;
|
|
||||||
pub use manager::*;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,488 +1,103 @@
|
|||||||
// Prevents additional console window on WiOk(ndows in release, DO NOT REMOVE!!
|
#![cfg_attr(
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
#![feature(bool_to_option)]
|
||||||
|
|
||||||
mod ambient_light;
|
mod core;
|
||||||
mod display;
|
mod picker;
|
||||||
mod led_color;
|
|
||||||
mod rpc;
|
mod rpc;
|
||||||
mod screenshot;
|
|
||||||
mod screenshot_manager;
|
|
||||||
mod volume;
|
|
||||||
|
|
||||||
use ambient_light::{Border, ColorCalibration, LedStripConfig, LedStripConfigGroup};
|
use crate::core::AmbientLightMode;
|
||||||
use display::{DisplayManager, DisplayState};
|
use crate::core::CoreManager;
|
||||||
use display_info::DisplayInfo;
|
use paris::*;
|
||||||
use paris::{error, info, warn};
|
use picker::config::DisplayConfig;
|
||||||
use rpc::{BoardInfo, UdpRpc};
|
use picker::manager::Picker;
|
||||||
use screenshot::Screenshot;
|
use picker::screenshot::ScreenshotDto;
|
||||||
use screenshot_manager::ScreenshotManager;
|
use tauri::async_runtime::Mutex;
|
||||||
use serde::{Deserialize, Serialize};
|
use once_cell::sync::OnceCell;
|
||||||
use serde_json::to_string;
|
|
||||||
use tauri::{http::ResponseBuilder, regex, Manager};
|
|
||||||
use volume::VolumeManager;
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
static GET_SCREENSHOT_LOCK: OnceCell<Mutex<bool>> = OnceCell::new();
|
||||||
#[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]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
async fn take_snapshot() -> Vec<ScreenshotDto> {
|
||||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
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![]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
|
||||||
|
base64_bitmap_list
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn list_display_info() -> Result<String, String> {
|
async fn get_screenshot_by_config(config: DisplayConfig) -> Result<ScreenshotDto, String> {
|
||||||
let displays = display_info::DisplayInfo::all().map_err(|e| {
|
info!("Hi?");
|
||||||
error!("can not list display info: {}", e);
|
// let _lock = GET_SCREENSHOT_LOCK.get_or_init(|| Mutex::new(false)).lock().await;
|
||||||
e.to_string()
|
// info!("Hi!");
|
||||||
})?;
|
let start = time::Instant::now();
|
||||||
let displays: Vec<DisplayInfoWrapper> =
|
let screenshot_dto = Picker::preview_display_by_config(config).await;
|
||||||
displays.iter().map(|v| DisplayInfoWrapper(v)).collect();
|
info!("截图耗时 {} s", start.elapsed().as_seconds_f32());
|
||||||
let json_str = to_string(&displays).map_err(|e| {
|
match screenshot_dto {
|
||||||
error!("can not list display info: {}", e);
|
Ok(screenshot_dto) => Ok(screenshot_dto),
|
||||||
e.to_string()
|
Err(error) => {
|
||||||
})?;
|
error!("preview_display_by_config failed. {}", error);
|
||||||
Ok(json_str)
|
Err(format!("preview_display_by_config failed. {}", error))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn read_led_strip_configs() -> Result<LedStripConfigGroup, String> {
|
|
||||||
let config = ambient_light::LedStripConfigGroup::read_config()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("can not read led strip configs: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})?;
|
|
||||||
Ok(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn write_led_strip_configs(
|
|
||||||
configs: Vec<ambient_light::LedStripConfig>,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
|
|
||||||
config_manager.set_items(configs).await.map_err(|e| {
|
|
||||||
error!("can not write led strip configs: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_led_strips_sample_points(
|
|
||||||
config: LedStripConfig,
|
|
||||||
) -> Result<Vec<screenshot::LedSamplePoints>, String> {
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
|
||||||
let channels = screenshot_manager.channels.read().await;
|
|
||||||
if let Some(rx) = channels.get(&config.display_id) {
|
|
||||||
let rx = rx.read().await;
|
|
||||||
let screenshot = rx.borrow().clone();
|
|
||||||
let sample_points = screenshot.get_sample_points(&config);
|
|
||||||
Ok(sample_points)
|
|
||||||
} else {
|
|
||||||
return Err(format!("display not found: {}", config.display_id));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_one_edge_colors(
|
async fn get_picker_config() -> picker::config::Configuration {
|
||||||
display_id: u32,
|
let configuration = picker::config::Manager::global().get_config().await;
|
||||||
sample_points: Vec<screenshot::LedSamplePoints>,
|
info!("configuration: {:?}", configuration);
|
||||||
) -> Result<Vec<led_color::LedColor>, String> {
|
configuration
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
}
|
||||||
let channels = screenshot_manager.channels.read().await;
|
#[tauri::command]
|
||||||
if let Some(rx) = channels.get(&display_id) {
|
async fn write_picker_config(config: picker::config::Configuration) -> Result<(), String> {
|
||||||
let rx = rx.read().await;
|
let manager = picker::config::Manager::global();
|
||||||
let screenshot = rx.borrow().clone();
|
let path = picker::config::Manager::get_config_file_path();
|
||||||
let bytes = screenshot.bytes.read().await.to_owned();
|
info!("log save in {:?}", path.to_str());
|
||||||
let colors =
|
manager.set_config(&config).await;
|
||||||
Screenshot::get_one_edge_colors(&sample_points, &bytes, screenshot.bytes_per_row);
|
match picker::config::Manager::write_config_to_disk(path, &config) {
|
||||||
Ok(colors)
|
Ok(_) => Ok(()),
|
||||||
} else {
|
Err(err) => {
|
||||||
Err(format!("display not found: {}", display_id))
|
error!("can not write picker config. {:?}", err);
|
||||||
|
Err(format!("can not write picker config. {:?}", err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn patch_led_strip_len(display_id: u32, border: Border, delta_len: i8) -> Result<(), String> {
|
async fn play_mode(target_mode: AmbientLightMode) {
|
||||||
info!(
|
info!("target mode: {:?}", target_mode);
|
||||||
"patch_led_strip_len: {} {:?} {}",
|
|
||||||
display_id, border, delta_len
|
|
||||||
);
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
config_manager
|
|
||||||
.patch_led_strip_len(display_id, border, delta_len)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("can not patch led strip len: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
info!("patch_led_strip_len: ok");
|
tokio::spawn(async move { CoreManager::global().set_ambient_light(target_mode).await });
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn send_colors(offset: u16, buffer: Vec<u8>) -> Result<(), String> {
|
|
||||||
ambient_light::LedColorsPublisher::send_colors(offset, buffer)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("can not send colors: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn move_strip_part(
|
|
||||||
display_id: u32,
|
|
||||||
border: Border,
|
|
||||||
target_start: usize,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
config_manager
|
|
||||||
.move_strip_part(display_id, border, target_start)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("can not move strip part: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn reverse_led_strip_part(display_id: u32, border: Border) -> Result<(), String> {
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
config_manager
|
|
||||||
.reverse_led_strip_part(display_id, border)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("can not reverse led strip part: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn set_color_calibration(calibration: ColorCalibration) -> Result<(), String> {
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
config_manager
|
|
||||||
.set_color_calibration(calibration)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
error!("can not set color calibration: {}", e);
|
|
||||||
e.to_string()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn read_config() -> ambient_light::LedStripConfigGroup {
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
config_manager.configs().await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_boards() -> Result<Vec<BoardInfo>, String> {
|
|
||||||
let udp_rpc = UdpRpc::global().await;
|
|
||||||
|
|
||||||
if let Err(e) = udp_rpc {
|
|
||||||
return Err(format!("can not ping: {}", e));
|
|
||||||
}
|
|
||||||
|
|
||||||
let udp_rpc = udp_rpc.as_ref().unwrap();
|
|
||||||
|
|
||||||
let boards = udp_rpc.get_boards().await;
|
|
||||||
let boards = boards.into_iter().collect::<Vec<_>>();
|
|
||||||
Ok(boards)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_displays() -> Vec<DisplayState> {
|
|
||||||
let display_manager = DisplayManager::global().await;
|
|
||||||
|
|
||||||
display_manager.get_displays().await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
rpc::manager::Manager::global();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
|
||||||
screenshot_manager.start().await.unwrap_or_else(|e| {
|
|
||||||
error!("can not start screenshot manager: {}", e);
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let led_color_publisher = ambient_light::LedColorsPublisher::global().await;
|
|
||||||
led_color_publisher.start().await;
|
|
||||||
});
|
|
||||||
|
|
||||||
let _volume = VolumeManager::global().await;
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
greet,
|
take_snapshot,
|
||||||
list_display_info,
|
play_mode,
|
||||||
read_led_strip_configs,
|
get_picker_config,
|
||||||
write_led_strip_configs,
|
get_screenshot_by_config,
|
||||||
get_led_strips_sample_points,
|
write_picker_config,
|
||||||
get_one_edge_colors,
|
|
||||||
patch_led_strip_len,
|
|
||||||
send_colors,
|
|
||||||
move_strip_part,
|
|
||||||
reverse_led_strip_part,
|
|
||||||
set_color_calibration,
|
|
||||||
read_config,
|
|
||||||
get_boards,
|
|
||||||
get_displays
|
|
||||||
])
|
])
|
||||||
.register_uri_scheme_protocol("ambient-light", move |_app, request| {
|
|
||||||
let response = ResponseBuilder::new().header("Access-Control-Allow-Origin", "*");
|
|
||||||
|
|
||||||
let uri = request.uri();
|
|
||||||
let uri = percent_encoding::percent_decode_str(uri)
|
|
||||||
.decode_utf8()
|
|
||||||
.unwrap()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let url = url_build_parse::parse_url(uri.as_str());
|
|
||||||
|
|
||||||
if let Err(err) = url {
|
|
||||||
error!("url parse error: {}", err);
|
|
||||||
return response
|
|
||||||
.status(500)
|
|
||||||
.mimetype("text/plain")
|
|
||||||
.body("Parse uri failed.".as_bytes().to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = url.unwrap();
|
|
||||||
|
|
||||||
let re = regex::Regex::new(r"^/displays/(\d+)$").unwrap();
|
|
||||||
let path = url.path;
|
|
||||||
let captures = re.captures(path.as_str());
|
|
||||||
|
|
||||||
if let None = captures {
|
|
||||||
error!("path not matched: {:?}", path);
|
|
||||||
return response
|
|
||||||
.status(404)
|
|
||||||
.mimetype("text/plain")
|
|
||||||
.body("Path Not Found.".as_bytes().to_vec());
|
|
||||||
}
|
|
||||||
|
|
||||||
let captures = captures.unwrap();
|
|
||||||
|
|
||||||
let display_id = captures[1].parse::<u32>().unwrap();
|
|
||||||
|
|
||||||
let bytes = tokio::task::block_in_place(move || {
|
|
||||||
tauri::async_runtime::block_on(async move {
|
|
||||||
let screenshot_manager = ScreenshotManager::global().await;
|
|
||||||
let rx: Result<tokio::sync::watch::Receiver<Screenshot>, anyhow::Error> =
|
|
||||||
screenshot_manager.subscribe_by_display_id(display_id).await;
|
|
||||||
|
|
||||||
if let Err(err) = rx {
|
|
||||||
anyhow::bail!("Display#{}: not found. {}", display_id, err);
|
|
||||||
}
|
|
||||||
let mut rx = rx.unwrap();
|
|
||||||
|
|
||||||
if rx.changed().await.is_err() {
|
|
||||||
anyhow::bail!("Display#{}: no more screenshot.", display_id);
|
|
||||||
}
|
|
||||||
let screenshot = rx.borrow().clone();
|
|
||||||
let bytes = screenshot.bytes.read().await;
|
|
||||||
if bytes.len() == 0 {
|
|
||||||
anyhow::bail!("Display#{}: no screenshot.", display_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("Display#{}: screenshot size: {}", display_id, bytes.len());
|
|
||||||
|
|
||||||
let (scale_factor_x, scale_factor_y, width, height) = if url.query.is_some()
|
|
||||||
&& url.query.as_ref().unwrap().contains_key("height")
|
|
||||||
&& url.query.as_ref().unwrap().contains_key("width")
|
|
||||||
{
|
|
||||||
let width = url.query.as_ref().unwrap()["width"]
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!("width parse error: {}", err);
|
|
||||||
err
|
|
||||||
})?;
|
|
||||||
let height = url.query.as_ref().unwrap()["height"]
|
|
||||||
.parse::<u32>()
|
|
||||||
.map_err(|err| {
|
|
||||||
warn!("height parse error: {}", err);
|
|
||||||
err
|
|
||||||
})?;
|
|
||||||
(
|
|
||||||
screenshot.width as f32 / width as f32,
|
|
||||||
screenshot.height as f32 / height as f32,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
log::debug!("scale by scale_factor");
|
|
||||||
let scale_factor = screenshot.scale_factor;
|
|
||||||
(
|
|
||||||
scale_factor,
|
|
||||||
scale_factor,
|
|
||||||
(screenshot.width as f32 / scale_factor) as u32,
|
|
||||||
(screenshot.height as f32 / scale_factor) as u32,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
log::debug!(
|
|
||||||
"scale by query. width: {}, height: {}, scale_factor: {}, len: {}",
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
screenshot.width as f32 / width as f32,
|
|
||||||
width * height * 4,
|
|
||||||
);
|
|
||||||
|
|
||||||
let bytes_per_row = screenshot.bytes_per_row as f32;
|
|
||||||
|
|
||||||
let mut rgba_buffer = vec![0u8; (width * height * 4) as usize];
|
|
||||||
|
|
||||||
for y in 0..height {
|
|
||||||
for x in 0..width {
|
|
||||||
let offset = ((y as f32) * scale_factor_y).floor() as usize
|
|
||||||
* bytes_per_row as usize
|
|
||||||
+ ((x as f32) * scale_factor_x).floor() as usize * 4;
|
|
||||||
let b = bytes[offset];
|
|
||||||
let g = bytes[offset + 1];
|
|
||||||
let r = bytes[offset + 2];
|
|
||||||
let a = bytes[offset + 3];
|
|
||||||
let offset_2 = (y * width + x) as usize * 4;
|
|
||||||
rgba_buffer[offset_2] = r;
|
|
||||||
rgba_buffer[offset_2 + 1] = g;
|
|
||||||
rgba_buffer[offset_2 + 2] = b;
|
|
||||||
rgba_buffer[offset_2 + 3] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(rgba_buffer.clone())
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Ok(bytes) = bytes {
|
|
||||||
return response
|
|
||||||
.mimetype("octet/stream")
|
|
||||||
.status(200)
|
|
||||||
.body(bytes.to_vec());
|
|
||||||
}
|
|
||||||
let err = bytes.unwrap_err();
|
|
||||||
error!("request screenshot bin data failed: {}", err);
|
|
||||||
return response
|
|
||||||
.mimetype("text/plain")
|
|
||||||
.status(500)
|
|
||||||
.body(err.to_string().into_bytes());
|
|
||||||
})
|
|
||||||
.setup(move |app| {
|
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let config_manager = ambient_light::ConfigManager::global().await;
|
|
||||||
let mut config_update_receiver = config_manager.clone_config_update_receiver();
|
|
||||||
loop {
|
|
||||||
if let Err(err) = config_update_receiver.changed().await {
|
|
||||||
error!("config update receiver changed error: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("config changed. emit config_changed event.");
|
|
||||||
|
|
||||||
let config = config_update_receiver.borrow().clone();
|
|
||||||
|
|
||||||
app_handle.emit_all("config_changed", config).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
||||||
let mut publisher_update_receiver = publisher.clone_sorted_colors_receiver().await;
|
|
||||||
loop {
|
|
||||||
if let Err(err) = publisher_update_receiver.changed().await {
|
|
||||||
error!("publisher update receiver changed error: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let publisher = publisher_update_receiver.borrow().clone();
|
|
||||||
|
|
||||||
app_handle
|
|
||||||
.emit_all("led_sorted_colors_changed", publisher)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let publisher = ambient_light::LedColorsPublisher::global().await;
|
|
||||||
let mut publisher_update_receiver = publisher.clone_colors_receiver().await;
|
|
||||||
loop {
|
|
||||||
if let Err(err) = publisher_update_receiver.changed().await {
|
|
||||||
error!("publisher update receiver changed error: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let publisher = publisher_update_receiver.borrow().clone();
|
|
||||||
|
|
||||||
app_handle
|
|
||||||
.emit_all("led_colors_changed", publisher)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
match UdpRpc::global().await {
|
|
||||||
Ok(udp_rpc) => {
|
|
||||||
let mut receiver = udp_rpc.subscribe_boards_change();
|
|
||||||
loop {
|
|
||||||
if let Err(err) = receiver.changed().await {
|
|
||||||
error!("boards change receiver changed error: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let boards = receiver.borrow().clone();
|
|
||||||
|
|
||||||
let boards = boards.into_iter().collect::<Vec<_>>();
|
|
||||||
|
|
||||||
app_handle.emit_all("boards_changed", boards).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("udp rpc error: {}", err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let app_handle = app.handle().clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let display_manager = DisplayManager::global().await;
|
|
||||||
let mut rx = display_manager.subscribe_displays_changed();
|
|
||||||
|
|
||||||
while rx.changed().await.is_ok() {
|
|
||||||
let displays = rx.borrow().clone();
|
|
||||||
|
|
||||||
log::info!("displays changed. emit displays_changed event.");
|
|
||||||
|
|
||||||
app_handle.emit_all("displays_changed", displays).unwrap();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
53
src-tauri/src/picker/config/display_config.rs
Normal file
53
src-tauri/src/picker/config/display_config.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LedStripConfigOfBorders {
|
||||||
|
pub top: Option<LedStripConfig>,
|
||||||
|
pub bottom: Option<LedStripConfig>,
|
||||||
|
pub left: Option<LedStripConfig>,
|
||||||
|
pub right: Option<LedStripConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct LedStripConfig {
|
||||||
|
pub index: usize,
|
||||||
|
pub global_start_position: usize,
|
||||||
|
pub global_end_position: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct DisplayConfig {
|
||||||
|
pub id: usize,
|
||||||
|
pub index_of_display: usize,
|
||||||
|
pub display_width: usize,
|
||||||
|
pub display_height: usize,
|
||||||
|
pub led_strip_of_borders: LedStripConfigOfBorders,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LedStripConfigOfBorders {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
top: None,
|
||||||
|
bottom: None,
|
||||||
|
left: None,
|
||||||
|
right: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DisplayConfig {
|
||||||
|
pub fn default(
|
||||||
|
id: usize,
|
||||||
|
index_of_display: usize,
|
||||||
|
display_width: usize,
|
||||||
|
display_height: usize,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
index_of_display,
|
||||||
|
display_width,
|
||||||
|
display_height,
|
||||||
|
led_strip_of_borders: LedStripConfigOfBorders::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
147
src-tauri/src/picker/config/manger.rs
Normal file
147
src-tauri/src/picker/config/manger.rs
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
use std::{
|
||||||
|
env::current_dir,
|
||||||
|
fs::{self, File},
|
||||||
|
io::Read,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use paris::info;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{api::path::config_dir, async_runtime::Mutex};
|
||||||
|
|
||||||
|
use super::DisplayConfig;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
|
pub struct Configuration {
|
||||||
|
pub config_version: u8,
|
||||||
|
pub display_configs: Vec<DisplayConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
config_version: 1,
|
||||||
|
display_configs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Manager {
|
||||||
|
config: Arc<Mutex<Configuration>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn global() -> &'static Manager {
|
||||||
|
static DISPLAY_CONFIG_MANAGE: OnceCell<Manager> = OnceCell::new();
|
||||||
|
|
||||||
|
DISPLAY_CONFIG_MANAGE.get_or_init(|| Self::init_from_disk())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default() -> Self {
|
||||||
|
Self::new(Configuration::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(config: Configuration) -> Self {
|
||||||
|
Self {
|
||||||
|
config: Arc::new(Mutex::new(config)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_config_file_path() -> PathBuf {
|
||||||
|
config_dir()
|
||||||
|
.unwrap_or(current_dir().unwrap())
|
||||||
|
.join("display_config.json")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_from_disk() -> Self {
|
||||||
|
let config_file_path = Self::get_config_file_path();
|
||||||
|
match Self::read_config_from_disk(config_file_path) {
|
||||||
|
Ok(config) => Self::new(config),
|
||||||
|
Err(_) => Self::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_config_from_disk(config_file_path: PathBuf) -> anyhow::Result<Configuration> {
|
||||||
|
let mut file = File::open(config_file_path)
|
||||||
|
.map_err(|error| anyhow::anyhow!("config file is not existed. {}", error))?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)
|
||||||
|
.map_err(|error| anyhow::anyhow!("can not read config file. {}", error))?;
|
||||||
|
serde_json::from_str(&contents)
|
||||||
|
.map_err(|error| anyhow::anyhow!("can not parse config file contents. {}", error))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write_config_to_disk(
|
||||||
|
config_file_path: PathBuf,
|
||||||
|
config: &Configuration,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let contents = serde_json::to_string(config)
|
||||||
|
.map_err(|error| anyhow::anyhow!("can not serialize config. {}", error))?;
|
||||||
|
info!("contents: {}", contents);
|
||||||
|
fs::write(config_file_path, contents.as_bytes())
|
||||||
|
.map_err(|error| anyhow::anyhow!("can not write config file. {}", error))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_config(&self) -> Configuration {
|
||||||
|
self.config.lock().await.clone()
|
||||||
|
}
|
||||||
|
pub async fn set_config(&self, new_config: &Configuration) {
|
||||||
|
let mut config = self.config.lock().await;
|
||||||
|
*config = new_config.clone();
|
||||||
|
}
|
||||||
|
pub async fn reload_config(&self) -> anyhow::Result<Configuration> {
|
||||||
|
let mut config = self.config.lock().await;
|
||||||
|
let new_config = Self::read_config_from_disk(Self::get_config_file_path())
|
||||||
|
.map_err(|err| anyhow::anyhow!("can not reload config. {:?}", err))?;
|
||||||
|
*config = new_config.clone();
|
||||||
|
return anyhow::Ok(new_config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use test_dir::{DirBuilder, TestDir};
|
||||||
|
|
||||||
|
use crate::picker::config::Configuration;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn write_config_to_disk_should_be_successful() {
|
||||||
|
let temp = TestDir::temp().create("config_dir", test_dir::FileType::Dir);
|
||||||
|
let config_file_path = temp.path("config_dir").join("picker.config.json");
|
||||||
|
let manager = crate::picker::config::manger::Manager::default();
|
||||||
|
crate::picker::config::manger::Manager::write_config_to_disk(
|
||||||
|
config_file_path.clone(),
|
||||||
|
&Configuration::default(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let contents = fs::read_to_string(config_file_path.clone()).unwrap();
|
||||||
|
let _config: Configuration = serde_json::from_str(contents.as_str()).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn read_config_to_disk_should_be_successful() {
|
||||||
|
let temp = TestDir::temp().create("config_dir", test_dir::FileType::Dir);
|
||||||
|
let config_file_path = temp.path("config_dir").join("picker.config.json");
|
||||||
|
fs::write(
|
||||||
|
config_file_path.clone(),
|
||||||
|
json!({
|
||||||
|
"config_version": 1,
|
||||||
|
"display_configs": []
|
||||||
|
})
|
||||||
|
.to_string()
|
||||||
|
.as_bytes(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let _manager =
|
||||||
|
crate::picker::config::manger::Manager::read_config_from_disk(config_file_path.clone())
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
5
src-tauri/src/picker/config/mod.rs
Normal file
5
src-tauri/src/picker/config/mod.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
mod display_config;
|
||||||
|
mod manger;
|
||||||
|
|
||||||
|
pub use display_config::*;
|
||||||
|
pub use manger::*;
|
51
src-tauri/src/picker/display_picker.rs
Normal file
51
src-tauri/src/picker/display_picker.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -2,47 +2,45 @@ use color_space::{Hsv, Rgb};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub struct LedColor([u8; 3]);
|
pub struct LedColor {
|
||||||
|
bits: [u8; 3],
|
||||||
|
}
|
||||||
|
|
||||||
impl LedColor {
|
impl LedColor {
|
||||||
pub fn default() -> Self {
|
pub fn default() -> Self {
|
||||||
Self ([0, 0, 0] )
|
Self { bits: [0, 0, 0] }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
pub fn new(r: u8, g: u8, b: u8) -> Self {
|
||||||
Self ([r, g, b])
|
Self { bits: [r, g, b] }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
pub fn from_hsv(h: f64, s: f64, v: f64) -> Self {
|
||||||
let rgb = Rgb::from(Hsv::new(h, s, v));
|
let rgb = Rgb::from(Hsv::new(h, s, v));
|
||||||
Self ([rgb.r as u8, rgb.g as u8, rgb.b as u8])
|
Self { bits: [rgb.r as u8, rgb.g as u8, rgb.b as u8] }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rgb(&self) -> [u8; 3] {
|
pub fn get_rgb(&self) -> [u8; 3] {
|
||||||
self.0
|
self.bits
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.0.iter().any(|bit| *bit == 0)
|
self.bits.iter().any(|bit| *bit == 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
pub fn set_rgb(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
self.0 = [r, g, b];
|
self.bits = [r, g, b];
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
pub fn merge(&mut self, r: u8, g: u8, b: u8) -> &Self {
|
||||||
self.0 = [
|
self.bits = [
|
||||||
(self.0[0] / 2 + r / 2),
|
(self.bits[0] / 2 + r / 2),
|
||||||
(self.0[1] / 2 + g / 2),
|
(self.bits[1] / 2 + g / 2),
|
||||||
(self.0[2] / 2 + b / 2),
|
(self.bits[2] / 2 + b / 2),
|
||||||
];
|
];
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_bytes (&self) -> [u8; 3] {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Serialize for LedColor {
|
impl Serialize for LedColor {
|
||||||
@ -50,7 +48,7 @@ impl Serialize for LedColor {
|
|||||||
where
|
where
|
||||||
S: serde::Serializer,
|
S: serde::Serializer,
|
||||||
{
|
{
|
||||||
let hex = format!("#{}", hex::encode(self.0));
|
let hex = format!("#{}", hex::encode(self.bits));
|
||||||
serializer.serialize_str(hex.as_str())
|
serializer.serialize_str(hex.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
87
src-tauri/src/picker/manager.rs
Normal file
87
src-tauri/src/picker/manager.rs
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
6
src-tauri/src/picker/mod.rs
Normal file
6
src-tauri/src/picker/mod.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
pub mod led_color;
|
||||||
|
pub mod screen;
|
||||||
|
pub mod manager;
|
||||||
|
pub mod screenshot;
|
||||||
|
pub mod display_picker;
|
||||||
|
pub mod config;
|
83
src-tauri/src/picker/preview_manager.rs
Normal file
83
src-tauri/src/picker/preview_manager.rs
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use paris::{info, warn};
|
||||||
|
use scrap::Display;
|
||||||
|
use std::{borrow::Borrow, sync::Arc};
|
||||||
|
use tokio::{sync::Mutex, task};
|
||||||
|
|
||||||
|
use crate::picker::{config, screen::Screen};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
config::DisplayConfig,
|
||||||
|
display_picker::DisplayPicker,
|
||||||
|
manager::Picker,
|
||||||
|
screenshot::{Screenshot, ScreenshotDto},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PreviewPicker {
|
||||||
|
pub pickers: Arc<Mutex<Vec<Arc<Mutex<DisplayPicker>>>>>,
|
||||||
|
pub screenshots: Arc<Mutex<Vec<Screenshot>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreviewPicker {
|
||||||
|
pub fn global() -> &'static PreviewPicker {
|
||||||
|
static SCREEN_COLOR_PREVIEW_PICKER: OnceCell<PreviewPicker> = OnceCell::new();
|
||||||
|
|
||||||
|
SCREEN_COLOR_PREVIEW_PICKER.get_or_init(|| PreviewPicker {
|
||||||
|
pickers: Arc::new(Mutex::new(vec![])),
|
||||||
|
screenshots: Arc::new(Mutex::new(vec![])),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list_displays(&self) {
|
||||||
|
let mut pickers = self.pickers.lock().await;
|
||||||
|
let displays = Display::all()
|
||||||
|
.map_err(|error| anyhow::anyhow!("Can not get all of displays. {}", error))?;
|
||||||
|
|
||||||
|
let mut configs = vec![];
|
||||||
|
let mut futs = FuturesUnordered::new();
|
||||||
|
|
||||||
|
for (index, display) in displays.iter().enumerate() {
|
||||||
|
let height = display.height();
|
||||||
|
let width = display.width();
|
||||||
|
let config = DisplayConfig::default(index, width, height);
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
for config in configs.iter() {
|
||||||
|
let picker = DisplayPicker::from_config(*config);
|
||||||
|
match picker {
|
||||||
|
Ok(picker) => {
|
||||||
|
pickers.push(Arc::new(Mutex::new(picker)));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
warn!(
|
||||||
|
"can not create DisplayPicker from config. config: {:?}",
|
||||||
|
config
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_screenshot_by_config(
|
||||||
|
&self,
|
||||||
|
config: DisplayConfig,
|
||||||
|
) -> anyhow::Result<ScreenshotDto> {
|
||||||
|
let start = time::Instant::now();
|
||||||
|
let mut picker = DisplayPicker::from_config(config)?;
|
||||||
|
let screenshot = picker.take_screenshot()?;
|
||||||
|
info!("Take Screenshot Spend: {}", start.elapsed());
|
||||||
|
|
||||||
|
anyhow::Ok(screenshot.to_dto().await)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn preview_display_by_config(config: DisplayConfig) -> anyhow::Result<ScreenshotDto> {
|
||||||
|
let start = time::Instant::now();
|
||||||
|
let mut picker = DisplayPicker::from_config(config)?;
|
||||||
|
let screenshot = picker.take_screenshot()?;
|
||||||
|
info!("Take Screenshot Spend: {}", start.elapsed());
|
||||||
|
|
||||||
|
anyhow::Ok(screenshot.to_dto().await)
|
||||||
|
}
|
||||||
|
}
|
52
src-tauri/src/picker/screen.rs
Normal file
52
src-tauri/src/picker/screen.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
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 {}
|
286
src-tauri/src/picker/screenshot.rs
Normal file
286
src-tauri/src/picker/screenshot.rs
Normal file
@ -0,0 +1,286 @@
|
|||||||
|
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,328 +0,0 @@
|
|||||||
use std::{sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use paris::{error, info, warn};
|
|
||||||
use tokio::{io, net::UdpSocket, sync::RwLock, task::yield_now, time::timeout};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
ambient_light::{ConfigManager, LedStripConfig},
|
|
||||||
rpc::DisplaySettingRequest,
|
|
||||||
volume::{self, VolumeManager},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{BoardConnectStatus, BoardInfo, BoardMessageChannels};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Board {
|
|
||||||
pub info: Arc<RwLock<BoardInfo>>,
|
|
||||||
socket: Option<Arc<UdpSocket>>,
|
|
||||||
listen_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
volume_changed_subscriber_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
state_of_displays_changed_subscriber_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
led_strip_config_changed_subscriber_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Board {
|
|
||||||
pub fn new(info: BoardInfo) -> Self {
|
|
||||||
Self {
|
|
||||||
info: Arc::new(RwLock::new(info)),
|
|
||||||
socket: None,
|
|
||||||
listen_handler: None,
|
|
||||||
volume_changed_subscriber_handler: None,
|
|
||||||
state_of_displays_changed_subscriber_handler: None,
|
|
||||||
led_strip_config_changed_subscriber_handler: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_socket(&mut self) -> anyhow::Result<()> {
|
|
||||||
let info = self.info.clone();
|
|
||||||
let info = info.read().await;
|
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
|
||||||
|
|
||||||
socket.connect((info.address, info.port)).await?;
|
|
||||||
let socket = Arc::new(socket);
|
|
||||||
self.socket = Some(socket.clone());
|
|
||||||
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
let mut buf = [0u8; 128];
|
|
||||||
|
|
||||||
let board_message_channels = crate::rpc::channels::BoardMessageChannels::global().await;
|
|
||||||
|
|
||||||
let display_setting_request_sender = board_message_channels
|
|
||||||
.display_setting_request_sender
|
|
||||||
.clone();
|
|
||||||
let volume_setting_request_sender =
|
|
||||||
board_message_channels.volume_setting_request_sender.clone();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match socket.recv(&mut buf).await {
|
|
||||||
Ok(len) => {
|
|
||||||
log::info!("recv: {:?}", &buf[..len]);
|
|
||||||
if buf[0] == 3 {
|
|
||||||
let result =
|
|
||||||
display_setting_request_sender.send(DisplaySettingRequest {
|
|
||||||
display_index: buf[1] as usize,
|
|
||||||
setting: crate::rpc::DisplaySetting::Brightness(buf[2]),
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Err(err) = result {
|
|
||||||
error!("send display setting request to channel failed: {:?}", err);
|
|
||||||
}
|
|
||||||
} else if buf[0] == 4 {
|
|
||||||
let result = volume_setting_request_sender.send(buf[1] as f32 / 100.0);
|
|
||||||
if let Err(err) = result {
|
|
||||||
error!("send volume setting request to channel failed: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
|
||||||
yield_now().await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
error!("socket recv error: {:?}", e);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.listen_handler = Some(handler);
|
|
||||||
|
|
||||||
self.subscribe_volume_changed().await;
|
|
||||||
self.subscribe_state_of_displays_changed().await;
|
|
||||||
self.subscribe_led_strip_config_changed().await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn subscribe_volume_changed(&mut self) {
|
|
||||||
let channel = BoardMessageChannels::global().await;
|
|
||||||
let mut volume_changed_rx = channel.volume_changed_sender.subscribe();
|
|
||||||
let info = self.info.clone();
|
|
||||||
let socket = self.socket.clone();
|
|
||||||
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let volume: Result<f32, tokio::sync::broadcast::error::RecvError> =
|
|
||||||
volume_changed_rx.recv().await;
|
|
||||||
if let Err(err) = volume {
|
|
||||||
match err {
|
|
||||||
tokio::sync::broadcast::error::RecvError::Closed => {
|
|
||||||
log::error!("volume changed channel closed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::sync::broadcast::error::RecvError::Lagged(_) => {
|
|
||||||
log::info!("volume changed channel lagged");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let volume = volume.unwrap();
|
|
||||||
|
|
||||||
let info = info.read().await;
|
|
||||||
if socket.is_none() || info.connect_status != BoardConnectStatus::Connected {
|
|
||||||
log::info!("board is not connected, skip send volume changed");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket = socket.as_ref().unwrap();
|
|
||||||
|
|
||||||
let mut buf = [0u8; 2];
|
|
||||||
buf[0] = 4;
|
|
||||||
buf[1] = (volume * 100.0) as u8;
|
|
||||||
|
|
||||||
if let Err(err) = socket.send(&buf).await {
|
|
||||||
log::warn!("send volume changed failed: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let volume_manager = VolumeManager::global().await;
|
|
||||||
let volume = volume_manager.get_volume().await;
|
|
||||||
|
|
||||||
if let Some(socket) = self.socket.as_ref() {
|
|
||||||
let buf = [4, (volume * 100.0) as u8];
|
|
||||||
if let Err(err) = socket.send(&buf).await {
|
|
||||||
log::warn!("send volume failed: {:?}", err);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log::warn!("socket is none, skip send volume");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.volume_changed_subscriber_handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn subscribe_state_of_displays_changed(&mut self) {
|
|
||||||
let channel: &BoardMessageChannels = BoardMessageChannels::global().await;
|
|
||||||
let mut state_of_displays_changed_rx = channel.displays_changed_sender.subscribe();
|
|
||||||
let info = self.info.clone();
|
|
||||||
let socket = self.socket.clone();
|
|
||||||
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
let states: Result<
|
|
||||||
Vec<crate::display::DisplayState>,
|
|
||||||
tokio::sync::broadcast::error::RecvError,
|
|
||||||
> = state_of_displays_changed_rx.recv().await;
|
|
||||||
if let Err(err) = states {
|
|
||||||
match err {
|
|
||||||
tokio::sync::broadcast::error::RecvError::Closed => {
|
|
||||||
log::error!("state of displays changed channel closed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
tokio::sync::broadcast::error::RecvError::Lagged(_) => {
|
|
||||||
log::info!("state of displays changed channel lagged");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let info = info.read().await;
|
|
||||||
if socket.is_none() || info.connect_status != BoardConnectStatus::Connected {
|
|
||||||
log::info!("board is not connected, skip send state of displays changed");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket = socket.as_ref().unwrap();
|
|
||||||
|
|
||||||
let mut buf = [0u8; 3];
|
|
||||||
let states = states.unwrap();
|
|
||||||
for (index, state) in states.iter().enumerate() {
|
|
||||||
buf[0] = 3;
|
|
||||||
buf[1] = index as u8;
|
|
||||||
buf[2] = state.brightness as u8;
|
|
||||||
|
|
||||||
log::info!("send state of displays changed: {:?}", &buf[..]);
|
|
||||||
|
|
||||||
if let Err(err) = socket.send(&buf).await {
|
|
||||||
log::warn!("send state of displays changed failed: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.state_of_displays_changed_subscriber_handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn subscribe_led_strip_config_changed(&mut self) {
|
|
||||||
let config_manager = ConfigManager::global().await;
|
|
||||||
let mut led_strip_config_changed_rx = config_manager.clone_config_update_receiver();
|
|
||||||
let info = self.info.clone();
|
|
||||||
let socket = self.socket.clone();
|
|
||||||
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
while led_strip_config_changed_rx.changed().await.is_ok() {
|
|
||||||
let config = led_strip_config_changed_rx.borrow().clone();
|
|
||||||
|
|
||||||
let info = info.read().await;
|
|
||||||
if socket.is_none() || info.connect_status != BoardConnectStatus::Connected {
|
|
||||||
log::info!("board is not connected, skip send led strip config changed");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket = socket.as_ref().unwrap();
|
|
||||||
|
|
||||||
let mut buf = [0u8; 4];
|
|
||||||
buf[0] = 5;
|
|
||||||
buf[1..].copy_from_slice(&config.color_calibration.to_bytes());
|
|
||||||
|
|
||||||
log::info!("send led strip config changed: {:?}", &buf[..]);
|
|
||||||
|
|
||||||
if let Err(err) = socket.send(&buf).await {
|
|
||||||
log::warn!("send led strip config changed failed: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.led_strip_config_changed_subscriber_handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_colors(&self, buf: &[u8]) {
|
|
||||||
let info = self.info.read().await;
|
|
||||||
if self.socket.is_none() || info.connect_status != BoardConnectStatus::Connected {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let socket = self.socket.as_ref().unwrap();
|
|
||||||
|
|
||||||
socket.send(buf).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check(&self) -> anyhow::Result<()> {
|
|
||||||
let info = self.info.read().await;
|
|
||||||
let socket = UdpSocket::bind("0.0.0.0:0").await?;
|
|
||||||
socket.connect((info.address, info.port)).await?;
|
|
||||||
drop(info);
|
|
||||||
|
|
||||||
let instant = std::time::Instant::now();
|
|
||||||
|
|
||||||
socket.send(&[1]).await?;
|
|
||||||
let mut buf = [0u8; 1];
|
|
||||||
let recv_future = socket.recv(&mut buf);
|
|
||||||
|
|
||||||
let check_result = timeout(Duration::from_secs(1), recv_future).await;
|
|
||||||
let mut info = self.info.write().await;
|
|
||||||
match check_result {
|
|
||||||
Ok(_) => {
|
|
||||||
let ttl = instant.elapsed();
|
|
||||||
if buf == [1] {
|
|
||||||
info.connect_status = BoardConnectStatus::Connected;
|
|
||||||
} else {
|
|
||||||
if let BoardConnectStatus::Connecting(retry) = info.connect_status {
|
|
||||||
if retry < 10 {
|
|
||||||
info.connect_status = BoardConnectStatus::Connecting(retry + 1);
|
|
||||||
info!("reconnect: {}", retry + 1);
|
|
||||||
} else {
|
|
||||||
info.connect_status = BoardConnectStatus::Disconnected;
|
|
||||||
warn!("board Disconnected: bad pong.");
|
|
||||||
}
|
|
||||||
} else if info.connect_status != BoardConnectStatus::Disconnected {
|
|
||||||
info.connect_status = BoardConnectStatus::Connecting(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info.ttl = Some(ttl.as_millis());
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
if let BoardConnectStatus::Connecting(retry) = info.connect_status {
|
|
||||||
if retry < 10 {
|
|
||||||
info.connect_status = BoardConnectStatus::Connecting(retry + 1);
|
|
||||||
info!("reconnect: {}", retry + 1);
|
|
||||||
} else {
|
|
||||||
info.connect_status = BoardConnectStatus::Disconnected;
|
|
||||||
warn!("board Disconnected: timeout");
|
|
||||||
}
|
|
||||||
} else if info.connect_status != BoardConnectStatus::Disconnected {
|
|
||||||
info.connect_status = BoardConnectStatus::Connecting(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
info.ttl = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info.checked_at = Some(std::time::SystemTime::now());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Board {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
info!("board drop");
|
|
||||||
|
|
||||||
if let Some(handler) = self.listen_handler.take() {
|
|
||||||
handler.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handler) = self.volume_changed_subscriber_handler.take() {
|
|
||||||
handler.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handler) = self.state_of_displays_changed_subscriber_handler.take() {
|
|
||||||
handler.abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handler) = self.led_strip_config_changed_subscriber_handler.take() {
|
|
||||||
handler.abort();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
use std::{net::Ipv4Addr, time::Duration};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum BoardConnectStatus {
|
|
||||||
Connected,
|
|
||||||
Connecting(u8),
|
|
||||||
Disconnected,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct BoardInfo {
|
|
||||||
pub fullname: String,
|
|
||||||
pub host: String,
|
|
||||||
pub address: Ipv4Addr,
|
|
||||||
pub port: u16,
|
|
||||||
pub connect_status: BoardConnectStatus,
|
|
||||||
pub checked_at: Option<std::time::SystemTime>,
|
|
||||||
pub ttl: Option<u128>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoardInfo {
|
|
||||||
pub fn new(fullname: String, host: String, address: Ipv4Addr, port: u16) -> Self {
|
|
||||||
Self {
|
|
||||||
fullname,
|
|
||||||
host,
|
|
||||||
address,
|
|
||||||
port,
|
|
||||||
connect_status: BoardConnectStatus::Unknown,
|
|
||||||
checked_at: None,
|
|
||||||
ttl: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use tokio::sync::{broadcast, OnceCell};
|
|
||||||
|
|
||||||
use crate::display::DisplayState;
|
|
||||||
|
|
||||||
use super::DisplaySettingRequest;
|
|
||||||
|
|
||||||
pub struct BoardMessageChannels {
|
|
||||||
pub display_setting_request_sender: Arc<broadcast::Sender<DisplaySettingRequest>>,
|
|
||||||
pub volume_setting_request_sender: Arc<broadcast::Sender<f32>>,
|
|
||||||
pub volume_changed_sender: Arc<broadcast::Sender<f32>>,
|
|
||||||
pub displays_changed_sender: Arc<broadcast::Sender<Vec<DisplayState>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoardMessageChannels {
|
|
||||||
pub async fn global() -> &'static Self {
|
|
||||||
static BOARD_MESSAGE_CHANNELS: OnceCell<BoardMessageChannels> = OnceCell::const_new();
|
|
||||||
|
|
||||||
BOARD_MESSAGE_CHANNELS.get_or_init(|| async {Self::new()}).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let (display_setting_request_sender, _) = broadcast::channel(16);
|
|
||||||
let display_setting_request_sender = Arc::new(display_setting_request_sender);
|
|
||||||
|
|
||||||
let (volume_setting_request_sender, _) = broadcast::channel(16);
|
|
||||||
let volume_setting_request_sender = Arc::new(volume_setting_request_sender);
|
|
||||||
|
|
||||||
let (volume_changed_sender, _) = broadcast::channel(2);
|
|
||||||
let volume_changed_sender = Arc::new(volume_changed_sender);
|
|
||||||
|
|
||||||
let (displays_changed_sender, _) = broadcast::channel(2);
|
|
||||||
let displays_changed_sender = Arc::new(displays_changed_sender);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
display_setting_request_sender,
|
|
||||||
volume_setting_request_sender,
|
|
||||||
volume_changed_sender,
|
|
||||||
displays_changed_sender,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum DisplaySetting {
|
|
||||||
Brightness(u8),
|
|
||||||
Contrast(u8),
|
|
||||||
Mode(u8),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct DisplaySettingRequest {
|
|
||||||
pub display_index: usize,
|
|
||||||
pub setting: DisplaySetting,
|
|
||||||
}
|
|
55
src-tauri/src/rpc/manager.rs
Normal file
55
src-tauri/src/rpc/manager.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
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,11 +1,2 @@
|
|||||||
mod board_info;
|
pub mod manager;
|
||||||
mod udp;
|
pub mod mqtt;
|
||||||
mod board;
|
|
||||||
mod display_setting_request;
|
|
||||||
mod channels;
|
|
||||||
|
|
||||||
pub use board_info::*;
|
|
||||||
pub use udp::*;
|
|
||||||
pub use board::*;
|
|
||||||
pub use display_setting_request::*;
|
|
||||||
pub use channels::*;
|
|
72
src-tauri/src/rpc/mqtt.rs
Normal file
72
src-tauri/src/rpc/mqtt.rs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,218 +0,0 @@
|
|||||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
|
||||||
|
|
||||||
use futures::future::join_all;
|
|
||||||
use mdns_sd::{ServiceDaemon, ServiceEvent};
|
|
||||||
use paris::{error, info, warn};
|
|
||||||
use tokio::sync::{watch, OnceCell, RwLock};
|
|
||||||
|
|
||||||
use super::{Board, BoardInfo};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct UdpRpc {
|
|
||||||
boards: Arc<RwLock<HashMap<String, Board>>>,
|
|
||||||
boards_change_sender: Arc<watch::Sender<Vec<BoardInfo>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UdpRpc {
|
|
||||||
pub async fn global() -> &'static anyhow::Result<Self> {
|
|
||||||
static UDP_RPC: OnceCell<anyhow::Result<UdpRpc>> = OnceCell::const_new();
|
|
||||||
|
|
||||||
UDP_RPC
|
|
||||||
.get_or_init(|| async {
|
|
||||||
let udp_rpc = UdpRpc::new().await?;
|
|
||||||
udp_rpc.initialize().await;
|
|
||||||
Ok(udp_rpc)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn new() -> anyhow::Result<Self> {
|
|
||||||
let boards = Arc::new(RwLock::new(HashMap::new()));
|
|
||||||
let (boards_change_sender, _) = watch::channel(Vec::new());
|
|
||||||
let boards_change_sender = Arc::new(boards_change_sender);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
boards,
|
|
||||||
boards_change_sender,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initialize(&self) {
|
|
||||||
let shared_self = Arc::new(self.clone());
|
|
||||||
|
|
||||||
let shared_self_for_search = shared_self.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
loop {
|
|
||||||
match shared_self_for_search.search_boards().await {
|
|
||||||
Ok(_) => {
|
|
||||||
info!("search_boards finished");
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("search_boards failed: {:?}", err);
|
|
||||||
tokio::time::sleep(Duration::from_secs(5)).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let shared_self_for_check = shared_self.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
shared_self_for_check.check_boards().await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn search_boards(&self) -> anyhow::Result<()> {
|
|
||||||
let service_type = "_ambient_light._udp.local.";
|
|
||||||
let mdns = ServiceDaemon::new()?;
|
|
||||||
let receiver = mdns.browse(&service_type).map_err(|e| {
|
|
||||||
warn!("Failed to browse for {:?}: {:?}", service_type, e);
|
|
||||||
e
|
|
||||||
})?;
|
|
||||||
let sender = self.boards_change_sender.clone();
|
|
||||||
|
|
||||||
while let Ok(event) = receiver.recv() {
|
|
||||||
match event {
|
|
||||||
ServiceEvent::ServiceResolved(info) => {
|
|
||||||
info!(
|
|
||||||
"Resolved a new service: {} host: {} port: {} IP: {:?} TXT properties: {:?}",
|
|
||||||
info.get_fullname(),
|
|
||||||
info.get_hostname(),
|
|
||||||
info.get_port(),
|
|
||||||
info.get_addresses(),
|
|
||||||
info.get_properties(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut boards = self.boards.write().await;
|
|
||||||
|
|
||||||
let board_info = BoardInfo::new(
|
|
||||||
info.get_fullname().to_string(),
|
|
||||||
info.get_hostname().to_string(),
|
|
||||||
info.get_addresses().iter().next().unwrap().clone(),
|
|
||||||
info.get_port(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut board = Board::new(board_info.clone());
|
|
||||||
|
|
||||||
if let Err(err) = board.init_socket().await {
|
|
||||||
error!("failed to init socket: {:?}", err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if boards.insert(board_info.fullname.clone(), board).is_some() {
|
|
||||||
info!("replace board {:?}", board_info);
|
|
||||||
} else {
|
|
||||||
info!("add board {:?}", board_info);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_boards = boards
|
|
||||||
.values()
|
|
||||||
.map(|it| async move { it.info.read().await.clone() });
|
|
||||||
let tx_boards = join_all(tx_boards).await;
|
|
||||||
|
|
||||||
drop(boards);
|
|
||||||
|
|
||||||
sender.send(tx_boards)?;
|
|
||||||
}
|
|
||||||
ServiceEvent::ServiceRemoved(_, fullname) => {
|
|
||||||
info!("removed board {:?}", fullname);
|
|
||||||
let mut boards = self.boards.write().await;
|
|
||||||
if boards.remove(&fullname).is_some() {
|
|
||||||
info!("removed board {:?} successful", fullname);
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_boards = boards
|
|
||||||
.values()
|
|
||||||
.map(|it| async move { it.info.read().await.clone() });
|
|
||||||
let tx_boards = join_all(tx_boards).await;
|
|
||||||
|
|
||||||
drop(boards);
|
|
||||||
|
|
||||||
sender.send(tx_boards)?;
|
|
||||||
}
|
|
||||||
other_event => {
|
|
||||||
// log::info!("{:?}", &other_event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subscribe_boards_change(&self) -> watch::Receiver<Vec<BoardInfo>> {
|
|
||||||
self.boards_change_sender.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_boards(&self) -> Vec<BoardInfo> {
|
|
||||||
self.boards_change_sender.borrow().clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn send_to_all(&self, buff: &Vec<u8>) -> anyhow::Result<()> {
|
|
||||||
let boards = self.boards.read().await;
|
|
||||||
|
|
||||||
for board in boards.values() {
|
|
||||||
board.send_colors(buff).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// let socket = self.socket.clone();
|
|
||||||
|
|
||||||
// let handlers = boards.into_iter().map(|board| {
|
|
||||||
// if board.connect_status == BoardConnectStatus::Disconnected {
|
|
||||||
// return tokio::spawn(async move {
|
|
||||||
// log::debug!("board {} is disconnected, skip.", board.host);
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// let socket = socket.clone();
|
|
||||||
// let buff = buff.clone();
|
|
||||||
// tokio::spawn(async move {
|
|
||||||
// match socket.send_to(&buff, (board.address, board.port)).await {
|
|
||||||
// Ok(_) => {}
|
|
||||||
// Err(err) => {
|
|
||||||
// error!("failed to send to {}: {:?}", board.host, err);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// });
|
|
||||||
|
|
||||||
// join_all(handlers).await;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn check_boards(&self) {
|
|
||||||
let mut interval = tokio::time::interval(Duration::from_secs(1));
|
|
||||||
loop {
|
|
||||||
tokio::task::yield_now().await;
|
|
||||||
interval.tick().await;
|
|
||||||
|
|
||||||
let boards = self.boards.read().await;
|
|
||||||
|
|
||||||
if boards.is_empty() {
|
|
||||||
info!("no boards found");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for board in boards.values() {
|
|
||||||
if let Err(err) = board.check().await {
|
|
||||||
error!("failed to check board: {:?}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let tx_boards = boards
|
|
||||||
.values()
|
|
||||||
.map(|it| async move { it.info.read().await.clone() });
|
|
||||||
let tx_boards = join_all(tx_boards).await;
|
|
||||||
|
|
||||||
drop(boards);
|
|
||||||
|
|
||||||
let board_change_sender = self.boards_change_sender.clone();
|
|
||||||
if let Err(err) = board_change_sender.send(tx_boards) {
|
|
||||||
error!("failed to send board change: {:?}", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
drop(board_change_sender);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,214 +0,0 @@
|
|||||||
|
|
||||||
use std::fmt::Formatter;
|
|
||||||
use std::{iter, fmt::Debug};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tauri::async_runtime::RwLock;
|
|
||||||
|
|
||||||
use crate::{ambient_light::LedStripConfig, led_color::LedColor};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Screenshot {
|
|
||||||
pub display_id: u32,
|
|
||||||
pub height: u32,
|
|
||||||
pub width: u32,
|
|
||||||
pub bytes_per_row: usize,
|
|
||||||
pub bytes: Arc<RwLock<Arc<Vec<u8>>>>,
|
|
||||||
pub scale_factor: f32,
|
|
||||||
pub bound_scale_factor: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for Screenshot {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.debug_struct("Screenshot")
|
|
||||||
.field("display_id", &self.display_id)
|
|
||||||
.field("height", &self.height)
|
|
||||||
.field("width", &self.width)
|
|
||||||
.field("bytes_per_row", &self.bytes_per_row)
|
|
||||||
.field("scale_factor", &self.scale_factor)
|
|
||||||
.field("bound_scale_factor", &self.bound_scale_factor)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static SINGLE_AXIS_POINTS: usize = 5;
|
|
||||||
|
|
||||||
impl Screenshot {
|
|
||||||
pub fn new(
|
|
||||||
display_id: u32,
|
|
||||||
height: u32,
|
|
||||||
width: u32,
|
|
||||||
bytes_per_row: usize,
|
|
||||||
bytes: Arc<Vec<u8>>,
|
|
||||||
scale_factor: f32,
|
|
||||||
bound_scale_factor: f32,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
display_id,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
bytes_per_row,
|
|
||||||
bytes: Arc::new(RwLock::new(bytes)),
|
|
||||||
scale_factor,
|
|
||||||
bound_scale_factor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sample_points(&self, config: &LedStripConfig) -> Vec<LedSamplePoints> {
|
|
||||||
let height = self.height as usize;
|
|
||||||
let width = self.width as usize;
|
|
||||||
// let height = CGDisplay::new(self.display_id).bounds().size.height as usize;
|
|
||||||
// let width = CGDisplay::new(self.display_id).bounds().size.width as usize;
|
|
||||||
|
|
||||||
match config.border {
|
|
||||||
crate::ambient_light::Border::Top => {
|
|
||||||
Self::get_one_edge_sample_points(height / 18, width, config.len, SINGLE_AXIS_POINTS)
|
|
||||||
}
|
|
||||||
crate::ambient_light::Border::Bottom => {
|
|
||||||
let points = Self::get_one_edge_sample_points(height / 18, width, config.len, SINGLE_AXIS_POINTS);
|
|
||||||
points
|
|
||||||
.into_iter()
|
|
||||||
.map(|groups| -> Vec<Point> {
|
|
||||||
groups.into_iter().map(|(x, y)| (x, height - y)).collect()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
crate::ambient_light::Border::Left => {
|
|
||||||
let points = Self::get_one_edge_sample_points(width / 32, height, config.len, SINGLE_AXIS_POINTS);
|
|
||||||
points
|
|
||||||
.into_iter()
|
|
||||||
.map(|groups| -> Vec<Point> {
|
|
||||||
groups.into_iter().map(|(x, y)| (y, x)).collect()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
crate::ambient_light::Border::Right => {
|
|
||||||
let points = Self::get_one_edge_sample_points(width / 32, height, config.len, SINGLE_AXIS_POINTS);
|
|
||||||
points
|
|
||||||
.into_iter()
|
|
||||||
.map(|groups| -> Vec<Point> {
|
|
||||||
groups.into_iter().map(|(x, y)| (width - y, x)).collect()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_one_edge_sample_points(
|
|
||||||
width: usize,
|
|
||||||
length: usize,
|
|
||||||
leds: usize,
|
|
||||||
single_axis_points: usize,
|
|
||||||
) -> Vec<LedSamplePoints> {
|
|
||||||
if leds == 0 {
|
|
||||||
return vec![];
|
|
||||||
}
|
|
||||||
|
|
||||||
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_one_edge_colors(
|
|
||||||
sample_points_of_leds: &Vec<LedSamplePoints>,
|
|
||||||
bitmap: &Vec<u8>,
|
|
||||||
bytes_per_row: usize,
|
|
||||||
) -> 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 {
|
|
||||||
// log::info!("x: {}, y: {}, bytes_per_row: {}", x, y, bytes_per_row);
|
|
||||||
let position = x * 4 + y * bytes_per_row;
|
|
||||||
b += bitmap[position] as f64;
|
|
||||||
g += bitmap[position + 1] as f64;
|
|
||||||
r += bitmap[position + 2] as f64;
|
|
||||||
}
|
|
||||||
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
|
||||||
colors.push(color);
|
|
||||||
}
|
|
||||||
colors
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_one_edge_colors_by_cg_image(
|
|
||||||
sample_points_of_leds: &Vec<LedSamplePoints>,
|
|
||||||
bitmap: core_foundation::data::CFData,
|
|
||||||
bytes_per_row: usize,
|
|
||||||
) -> 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 {
|
|
||||||
// log::info!("x: {}, y: {}, bytes_per_row: {}", x, y, bytes_per_row);
|
|
||||||
let position = x * 4 + y * bytes_per_row;
|
|
||||||
b += bitmap[position] as f64;
|
|
||||||
g += bitmap[position + 1] as f64;
|
|
||||||
r += bitmap[position + 2] as f64;
|
|
||||||
// log::info!("position: {}, total: {}", position, bitmap.len());
|
|
||||||
}
|
|
||||||
let color = LedColor::new((r / len) as u8, (g / len) as u8, (b / len) as u8);
|
|
||||||
colors.push(color);
|
|
||||||
}
|
|
||||||
colors
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_colors_by_sample_points(
|
|
||||||
&self,
|
|
||||||
points: &Vec<LedSamplePoints>,
|
|
||||||
) -> Vec<LedColor> {
|
|
||||||
let bytes = self.bytes.read().await;
|
|
||||||
|
|
||||||
Self::get_one_edge_colors(points, &bytes, self.bytes_per_row)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
type Point = (usize, usize);
|
|
||||||
pub type LedSamplePoints = Vec<Point>;
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ScreenSamplePoints {
|
|
||||||
pub top: Vec<LedSamplePoints>,
|
|
||||||
pub bottom: Vec<LedSamplePoints>,
|
|
||||||
pub left: Vec<LedSamplePoints>,
|
|
||||||
pub right: Vec<LedSamplePoints>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct DisplayColorsOfLedStrips {
|
|
||||||
pub top: Vec<u8>,
|
|
||||||
pub bottom: Vec<u8>,
|
|
||||||
pub left: Vec<u8>,
|
|
||||||
pub right: Vec<u8>,
|
|
||||||
}
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
|
||||||
pub struct ScreenshotPayload {
|
|
||||||
pub display_id: u32,
|
|
||||||
pub height: u32,
|
|
||||||
pub width: u32,
|
|
||||||
}
|
|
@ -1,244 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
|
|
||||||
use core_graphics::display::{
|
|
||||||
kCGNullWindowID, kCGWindowImageDefault, kCGWindowListOptionOnScreenOnly, CGDisplay,
|
|
||||||
};
|
|
||||||
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
|
|
||||||
use paris::{info, warn};
|
|
||||||
use rust_swift_screencapture::display::CGDisplayId;
|
|
||||||
use tauri::async_runtime::RwLock;
|
|
||||||
use tokio::sync::{broadcast, watch, Mutex, OnceCell};
|
|
||||||
use tokio::task::yield_now;
|
|
||||||
use tokio::time::sleep;
|
|
||||||
|
|
||||||
use crate::screenshot::LedSamplePoints;
|
|
||||||
use crate::{ambient_light::SamplePointMapper, led_color::LedColor, screenshot::Screenshot};
|
|
||||||
|
|
||||||
pub fn get_display_colors(
|
|
||||||
display_id: u32,
|
|
||||||
sample_points: &Vec<Vec<LedSamplePoints>>,
|
|
||||||
bound_scale_factor: f32,
|
|
||||||
) -> anyhow::Result<Vec<LedColor>> {
|
|
||||||
log::debug!("take_screenshot");
|
|
||||||
let cg_display = CGDisplay::new(display_id);
|
|
||||||
|
|
||||||
let mut colors = vec![];
|
|
||||||
for points in sample_points {
|
|
||||||
if points.len() == 0 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let start_x = points[0][0].0;
|
|
||||||
let start_y = points[0][0].1;
|
|
||||||
let end_x = points.last().unwrap().last().unwrap().0;
|
|
||||||
let end_y = points.last().unwrap().last().unwrap().1;
|
|
||||||
|
|
||||||
let (start_x, end_x) = (usize::min(start_x, end_x), usize::max(start_x, end_x));
|
|
||||||
let (start_y, end_y) = (usize::min(start_y, end_y), usize::max(start_y, end_y));
|
|
||||||
|
|
||||||
let origin = CGPoint {
|
|
||||||
x: start_x as f64 * bound_scale_factor as f64 + cg_display.bounds().origin.x,
|
|
||||||
y: start_y as f64 * bound_scale_factor as f64 + cg_display.bounds().origin.y,
|
|
||||||
};
|
|
||||||
let size = CGSize {
|
|
||||||
width: (end_x - start_x + 1) as f64,
|
|
||||||
height: (end_y - start_y + 1) as f64,
|
|
||||||
};
|
|
||||||
|
|
||||||
// log::info!(
|
|
||||||
// "origin: {:?}, size: {:?}, start_x: {}, start_y: {}, bounds: {:?}",
|
|
||||||
// origin,
|
|
||||||
// size,
|
|
||||||
// start_x,
|
|
||||||
// start_y,
|
|
||||||
// cg_display.bounds().size
|
|
||||||
// );
|
|
||||||
|
|
||||||
let cg_image = CGDisplay::screenshot(
|
|
||||||
CGRect::new(&origin, &size),
|
|
||||||
kCGWindowListOptionOnScreenOnly,
|
|
||||||
kCGNullWindowID,
|
|
||||||
kCGWindowImageDefault,
|
|
||||||
)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Display#{}: take screenshot failed", display_id))?;
|
|
||||||
|
|
||||||
let bitmap = cg_image.data();
|
|
||||||
|
|
||||||
let points = points
|
|
||||||
.iter()
|
|
||||||
.map(|points| {
|
|
||||||
points
|
|
||||||
.iter()
|
|
||||||
.map(|(x, y)| (*x - start_x, *y - start_y))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
let mut part_colors =
|
|
||||||
Screenshot::get_one_edge_colors_by_cg_image(&points, bitmap, cg_image.bytes_per_row());
|
|
||||||
colors.append(&mut part_colors);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(colors)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ScreenshotManager {
|
|
||||||
pub channels: Arc<RwLock<HashMap<u32, Arc<RwLock<watch::Sender<Screenshot>>>>>>,
|
|
||||||
merged_screenshot_tx: Arc<RwLock<broadcast::Sender<Screenshot>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (merged_screenshot_tx, _) = broadcast::channel::<Screenshot>(2);
|
|
||||||
Self {
|
|
||||||
channels,
|
|
||||||
merged_screenshot_tx: Arc::new(RwLock::new(merged_screenshot_tx)),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn start(&self) -> anyhow::Result<()> {
|
|
||||||
let displays = display_info::DisplayInfo::all()?;
|
|
||||||
|
|
||||||
let futures = displays.iter().map(|display| async {
|
|
||||||
self.start_one(display.id, display.scale_factor)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|err| {
|
|
||||||
warn!("start_one failed: display_id: {}, err: {}", display.id, err);
|
|
||||||
});
|
|
||||||
info!("start_one finished: display_id: {}", display.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
futures::future::join_all(futures).await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn start_one(&self, display_id: u32, scale_factor: f32) -> anyhow::Result<()> {
|
|
||||||
let merged_screenshot_tx = self.merged_screenshot_tx.clone();
|
|
||||||
|
|
||||||
let (tx, _) = watch::channel(Screenshot::new(
|
|
||||||
display_id,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
Arc::new(vec![]),
|
|
||||||
scale_factor,
|
|
||||||
scale_factor,
|
|
||||||
));
|
|
||||||
let tx = Arc::new(RwLock::new(tx));
|
|
||||||
|
|
||||||
let mut channels = self.channels.write().await;
|
|
||||||
channels.insert(display_id, tx.clone());
|
|
||||||
|
|
||||||
drop(channels);
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let display = rust_swift_screencapture::display::Display::new(display_id);
|
|
||||||
let mut frame_rx = display.subscribe_frame().await;
|
|
||||||
|
|
||||||
display.start_capture(30).await;
|
|
||||||
|
|
||||||
let tx_for_send = tx.read().await;
|
|
||||||
|
|
||||||
while frame_rx.changed().await.is_ok() {
|
|
||||||
let frame = frame_rx.borrow().clone();
|
|
||||||
let screenshot = Screenshot::new(
|
|
||||||
display_id,
|
|
||||||
frame.height as u32,
|
|
||||||
frame.width as u32,
|
|
||||||
frame.bytes_per_row as usize,
|
|
||||||
frame.bytes,
|
|
||||||
scale_factor,
|
|
||||||
scale_factor,
|
|
||||||
);
|
|
||||||
let merged_screenshot_tx = merged_screenshot_tx.write().await;
|
|
||||||
if let Err(err) = merged_screenshot_tx.send(screenshot.clone()) {
|
|
||||||
// log::warn!("merged_screenshot_tx.send failed: {}", err);
|
|
||||||
}
|
|
||||||
if let Err(err) = tx_for_send.send(screenshot.clone()) {
|
|
||||||
log::warn!("display {} screenshot_tx.send failed: {}", display_id, err);
|
|
||||||
} else {
|
|
||||||
log::debug!("screenshot: {:?}", screenshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
yield_now().await;
|
|
||||||
}
|
|
||||||
sleep(Duration::from_secs(5)).await;
|
|
||||||
info!(
|
|
||||||
"display {} frame_rx.changed() failed, try to restart",
|
|
||||||
display_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_sorted_colors(colors: &Vec<u8>, mappers: &Vec<SamplePointMapper>) -> Vec<u8> {
|
|
||||||
let total_leds = mappers
|
|
||||||
.iter()
|
|
||||||
.map(|mapper| usize::max(mapper.start, mapper.end))
|
|
||||||
.max()
|
|
||||||
.unwrap_or(0) as usize;
|
|
||||||
let mut global_colors = vec![0u8; total_leds * 3];
|
|
||||||
|
|
||||||
let mut color_index = 0;
|
|
||||||
mappers.iter().for_each(|group| {
|
|
||||||
if group.end > global_colors.len() || group.start > global_colors.len() {
|
|
||||||
warn!(
|
|
||||||
"get_sorted_colors: group out of range. start: {}, end: {}, global_colors.len(): {}",
|
|
||||||
group.start,
|
|
||||||
group.end,
|
|
||||||
global_colors.len()
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if color_index + group.start.abs_diff(group.end) * 3 > colors.len(){
|
|
||||||
warn!(
|
|
||||||
"get_sorted_colors: color_index out of range. color_index: {}, strip len: {}, colors.len(): {}",
|
|
||||||
color_index / 3,
|
|
||||||
group.start.abs_diff(group.end),
|
|
||||||
colors.len() / 3
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if group.end > group.start {
|
|
||||||
for i in group.start..group.end {
|
|
||||||
global_colors[i * 3] = colors[color_index +0];
|
|
||||||
global_colors[i * 3 + 1] = colors[color_index +1];
|
|
||||||
global_colors[i * 3 + 2] = colors[color_index +2];
|
|
||||||
color_index += 3;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i in (group.end..group.start).rev() {
|
|
||||||
global_colors[i * 3] = colors[color_index +0];
|
|
||||||
global_colors[i * 3 + 1] = colors[color_index +1];
|
|
||||||
global_colors[i * 3 + 2] = colors[color_index +2];
|
|
||||||
color_index += 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
global_colors
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn clone_merged_screenshot_rx(&self) -> broadcast::Receiver<Screenshot> {
|
|
||||||
self.merged_screenshot_tx.read().await.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn subscribe_by_display_id(
|
|
||||||
&self,
|
|
||||||
display_id: CGDisplayId,
|
|
||||||
) -> anyhow::Result<watch::Receiver<Screenshot>> {
|
|
||||||
let channels = self.channels.read().await;
|
|
||||||
if let Some(tx) = channels.get(&display_id) {
|
|
||||||
Ok(tx.read().await.subscribe())
|
|
||||||
} else {
|
|
||||||
Err(anyhow::anyhow!("display_id: {} not found", display_id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,203 +0,0 @@
|
|||||||
use std::{mem, sync::Arc};
|
|
||||||
|
|
||||||
use coreaudio::{
|
|
||||||
audio_unit::macos_helpers::get_default_device_id,
|
|
||||||
sys::{
|
|
||||||
kAudioHardwareServiceDeviceProperty_VirtualMasterVolume, kAudioObjectPropertyScopeOutput,
|
|
||||||
AudioObjectGetPropertyData, AudioObjectHasProperty, AudioObjectPropertyAddress,
|
|
||||||
AudioObjectSetPropertyData,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use paris::error;
|
|
||||||
use tokio::sync::{OnceCell, RwLock};
|
|
||||||
|
|
||||||
use crate::rpc::BoardMessageChannels;
|
|
||||||
|
|
||||||
pub struct VolumeManager {
|
|
||||||
current_volume: Arc<RwLock<f32>>,
|
|
||||||
handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
read_handler: Option<tokio::task::JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VolumeManager {
|
|
||||||
pub async fn global() -> &'static Self {
|
|
||||||
static VOLUME_MANAGER: OnceCell<VolumeManager> = OnceCell::const_new();
|
|
||||||
|
|
||||||
VOLUME_MANAGER
|
|
||||||
.get_or_init(|| async { Self::create() })
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create() -> Self {
|
|
||||||
let mut instance = Self {
|
|
||||||
current_volume: Arc::new(RwLock::new(0.0)),
|
|
||||||
handler: None,
|
|
||||||
read_handler: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
instance.subscribe_volume_setting_request();
|
|
||||||
instance.auto_read_volume();
|
|
||||||
|
|
||||||
instance
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subscribe_volume_setting_request(&mut self) {
|
|
||||||
let handler = tokio::spawn(async {
|
|
||||||
let channels = BoardMessageChannels::global().await;
|
|
||||||
let mut request_rx = channels.volume_setting_request_sender.subscribe();
|
|
||||||
|
|
||||||
while let Ok(volume) = request_rx.recv().await {
|
|
||||||
if let Err(err) = Self::set_volume(volume) {
|
|
||||||
error!("failed to set volume: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn auto_read_volume(&mut self) {
|
|
||||||
let current_volume = self.current_volume.clone();
|
|
||||||
|
|
||||||
let handler = tokio::spawn(async move {
|
|
||||||
let channel = BoardMessageChannels::global().await;
|
|
||||||
let volume_changed_tx = channel.volume_changed_sender.clone();
|
|
||||||
loop {
|
|
||||||
match Self::read_volume() {
|
|
||||||
Ok(value) => {
|
|
||||||
let mut volume = current_volume.write().await;
|
|
||||||
if *volume != value {
|
|
||||||
if let Err(err) = volume_changed_tx.send(value) {
|
|
||||||
error!("failed to send volume changed event: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*volume = value;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
error!("failed to read volume: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
self.read_handler = Some(handler);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_volume(volume: f32) -> anyhow::Result<()> {
|
|
||||||
log::debug!("set volume: {}", volume);
|
|
||||||
|
|
||||||
let device_id = get_default_device_id(false);
|
|
||||||
|
|
||||||
if device_id.is_none() {
|
|
||||||
anyhow::bail!("default audio output device is not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let device_id = device_id.unwrap();
|
|
||||||
|
|
||||||
let address = AudioObjectPropertyAddress {
|
|
||||||
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
|
||||||
mScope: kAudioObjectPropertyScopeOutput,
|
|
||||||
mElement: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!("device id: {}", device_id);
|
|
||||||
log::debug!("address: {:?}", address);
|
|
||||||
|
|
||||||
if 0 == unsafe { AudioObjectHasProperty(device_id, &address) } {
|
|
||||||
anyhow::bail!("Can not get audio property");
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = mem::size_of::<f32>() as u32;
|
|
||||||
|
|
||||||
let result = unsafe {
|
|
||||||
AudioObjectSetPropertyData(
|
|
||||||
device_id,
|
|
||||||
&address,
|
|
||||||
0,
|
|
||||||
std::ptr::null(),
|
|
||||||
size,
|
|
||||||
&volume as *const f32 as *const std::ffi::c_void,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if result != 0 {
|
|
||||||
anyhow::bail!("Can not set audio property");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_volume() -> anyhow::Result<f32> {
|
|
||||||
let device_id = get_default_device_id(false);
|
|
||||||
|
|
||||||
if device_id.is_none() {
|
|
||||||
anyhow::bail!("default audio output device is not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
let device_id = device_id.unwrap();
|
|
||||||
|
|
||||||
let address = AudioObjectPropertyAddress {
|
|
||||||
mSelector: kAudioHardwareServiceDeviceProperty_VirtualMasterVolume,
|
|
||||||
mScope: kAudioObjectPropertyScopeOutput,
|
|
||||||
mElement: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
log::debug!("device id: {}", device_id);
|
|
||||||
log::debug!("address: {:?}", address);
|
|
||||||
|
|
||||||
if 0 == unsafe { AudioObjectHasProperty(device_id, &address) } {
|
|
||||||
anyhow::bail!("Can not get audio property");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut size = mem::size_of::<f32>() as u32;
|
|
||||||
|
|
||||||
let mut volume = 0.0f32;
|
|
||||||
|
|
||||||
let result = unsafe {
|
|
||||||
AudioObjectGetPropertyData(
|
|
||||||
device_id,
|
|
||||||
&address,
|
|
||||||
0,
|
|
||||||
std::ptr::null(),
|
|
||||||
&mut size,
|
|
||||||
&mut volume as *mut f32 as *mut std::ffi::c_void,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if result != 0 {
|
|
||||||
anyhow::bail!("Can not get audio property. result: {}", result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if size != mem::size_of::<f32>() as u32 {
|
|
||||||
anyhow::bail!("Can not get audio property. data size is not matched.");
|
|
||||||
}
|
|
||||||
|
|
||||||
log::debug!("current system volume of primary device: {}", volume);
|
|
||||||
|
|
||||||
Ok(volume)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_volume(&self) -> f32 {
|
|
||||||
self.current_volume.read().await.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for VolumeManager {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
log::info!("drop volume manager");
|
|
||||||
if let Some(handler) = self.handler.take() {
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
handler.abort();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(handler) = self.read_handler.take() {
|
|
||||||
tokio::task::block_in_place(move || {
|
|
||||||
handler.abort();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
mod manager;
|
|
||||||
|
|
||||||
pub use manager::*;
|
|
@ -3,23 +3,24 @@
|
|||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
"beforeBuildCommand": "pnpm build",
|
"beforeBuildCommand": "pnpm build",
|
||||||
"devPath": "http://localhost:1420",
|
"devPath": "http://localhost:1420",
|
||||||
"distDir": "../dist",
|
"distDir": "../dist"
|
||||||
"withGlobalTauri": false
|
|
||||||
},
|
},
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "test-demo",
|
"productName": "display-ambient-light-desktop",
|
||||||
"version": "0.0.1"
|
"version": "0.0.0"
|
||||||
},
|
},
|
||||||
"tauri": {
|
"tauri": {
|
||||||
"allowlist": {
|
"allowlist": {
|
||||||
"all": false,
|
"all": true
|
||||||
"shell": {
|
|
||||||
"all": false,
|
|
||||||
"open": true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
|
"category": "DeveloperTool",
|
||||||
|
"copyright": "",
|
||||||
|
"deb": {
|
||||||
|
"depends": []
|
||||||
|
},
|
||||||
|
"externalBin": [],
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
@ -27,10 +28,22 @@
|
|||||||
"icons/icon.icns",
|
"icons/icon.icns",
|
||||||
"icons/icon.ico"
|
"icons/icon.ico"
|
||||||
],
|
],
|
||||||
"identifier": "cc.ivanli.ambient-light.desktop",
|
"identifier": "cc.ivanli.ambient",
|
||||||
"targets": "all",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"minimumSystemVersion": "13"
|
"entitlements": null,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"frameworks": [],
|
||||||
|
"providerShortName": null,
|
||||||
|
"signingIdentity": null
|
||||||
|
},
|
||||||
|
"resources": [],
|
||||||
|
"shortDescription": "",
|
||||||
|
"targets": "all",
|
||||||
|
"windows": {
|
||||||
|
"certificateThumbprint": null,
|
||||||
|
"digestAlgorithm": "sha256",
|
||||||
|
"timestampUrl": ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": {
|
"security": {
|
||||||
@ -42,10 +55,10 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
|
"height": 600,
|
||||||
"resizable": true,
|
"resizable": true,
|
||||||
"title": "test-demo",
|
"title": "display-ambient-light-desktop",
|
||||||
"width": 800,
|
"width": 800
|
||||||
"height": 600
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
11
src/App.css
Normal file
11
src/App.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.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;
|
||||||
|
}
|
96
src/App.tsx
96
src/App.tsx
@ -1,39 +1,73 @@
|
|||||||
import { Routes, Route } from '@solidjs/router';
|
import { useCallback, useState } from 'react';
|
||||||
import { LedStripConfiguration } from './components/led-strip-configuration/led-strip-configuration';
|
import tw from 'twin.macro';
|
||||||
import { WhiteBalance } from './components/white-balance/white-balance';
|
import { invoke } from '@tauri-apps/api/tauri';
|
||||||
import { createEffect } from 'solid-js';
|
import './App.css';
|
||||||
import { invoke } from '@tauri-apps/api';
|
import { Configurator } from './configurator/configurator';
|
||||||
import { setLedStripStore } from './stores/led-strip.store';
|
import { ButtonSwitch } from './commons/components/button';
|
||||||
import { LedStripConfigContainer } from './models/led-strip-config';
|
import { fillParentCss } from './styles/fill-parent';
|
||||||
import { InfoIndex } from './components/info/info-index';
|
|
||||||
import { DisplayStateIndex } from './components/displays/display-state-index';
|
type Mode = 'Flowing' | 'Follow' | null;
|
||||||
|
|
||||||
|
localStorage.setItem('debug', '*');
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
createEffect(() => {
|
const [screenshots, setScreenshots] = useState<string[]>([]);
|
||||||
invoke<LedStripConfigContainer>('read_config').then((config) => {
|
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
||||||
console.log('read config', config);
|
const [currentMode, setCurrentMode] = useState<Mode>(null);
|
||||||
setLedStripStore({
|
|
||||||
strips: config.strips,
|
async function takeSnapshot() {
|
||||||
mappers: config.mappers,
|
const base64TextList: string[] = await invoke('take_snapshot');
|
||||||
colorCalibration: config.color_calibration,
|
|
||||||
});
|
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],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div css={[fillParentCss]} tw="box-border flex flex-col">
|
||||||
<div>
|
<div tw="flex justify-between">
|
||||||
<a href="/info">基本信息</a>
|
{ledStripColors.map((it) => (
|
||||||
<a href="/displays">显示器信息</a>
|
<span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
||||||
<a href="/led-strips-configuration">灯条配置</a>
|
))}
|
||||||
<a href="/white-balance">白平衡</a>
|
</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>
|
||||||
<Routes>
|
|
||||||
<Route path="/info" component={InfoIndex} />
|
|
||||||
<Route path="/displays" component={DisplayStateIndex} />
|
|
||||||
<Route path="/led-strips-configuration" component={LedStripConfiguration} />
|
|
||||||
<Route path="/white-balance" component={WhiteBalance} />
|
|
||||||
</Routes>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<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>
|
After Width: | Height: | Size: 4.0 KiB |
@ -1,16 +0,0 @@
|
|||||||
|
|
||||||
<!-- http://mike.eire.ca/2010/02/25/easy-svg-grid/ -->
|
|
||||||
<!-- "I needed a grid in the background while I was debugging an SVG image I was creating, something
|
|
||||||
like Photoshop’s transparency grid. Here’s what I did." -->
|
|
||||||
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="200" height="400">
|
|
||||||
<defs>
|
|
||||||
<pattern id="grid" width="10" height="10" patternUnits="userSpaceOnUse">
|
|
||||||
<rect fill="black" x="0" y="0" width="5" height="5" opacity="0.3" />
|
|
||||||
<rect fill="white" x="5" y="0" width="5" height="5" />
|
|
||||||
<rect fill="black" x="5" y="5" width="5" height="5" opacity="0.3" />
|
|
||||||
<rect fill="white" x="0" y="5" width="5" height="5" />
|
|
||||||
</pattern>
|
|
||||||
</defs>
|
|
||||||
<rect fill="url(#grid)" x="0" y="0" width="100%" height="100%" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 759 B |
21
src/commons/components/button.tsx
Normal file
21
src/commons/components/button.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import tw, { theme } from 'twin.macro';
|
||||||
|
import { css } from '@emotion/react';
|
||||||
|
|
||||||
|
interface ButtonProps {
|
||||||
|
value?: boolean;
|
||||||
|
isSmall?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonSwitch = styled.button(({ value, isSmall }: ButtonProps) => [
|
||||||
|
// The common button styles
|
||||||
|
tw`px-8 py-2 rounded-xl transform duration-75 dark:bg-black m-2 shadow-lg text-opacity-95 dark:shadow-gray-800`,
|
||||||
|
|
||||||
|
tw`hover:(scale-105)`,
|
||||||
|
tw`focus:(scale-100)`,
|
||||||
|
|
||||||
|
value && 'bg-gradient-to-r from-purple-500 to-blue-500',
|
||||||
|
|
||||||
|
isSmall ? tw`text-sm` : tw`text-lg`,
|
||||||
|
]);
|
@ -1,36 +0,0 @@
|
|||||||
import { Component, ParentComponent } from 'solid-js';
|
|
||||||
import { DisplayState } from '../../models/display-state.model';
|
|
||||||
|
|
||||||
type DisplayStateCardProps = {
|
|
||||||
state: DisplayState;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ItemProps = {
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item: ParentComponent<ItemProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<dl class="flex">
|
|
||||||
<dt class="w-20">{props.label}</dt>
|
|
||||||
<dd class="flex-auto">{props.children}</dd>
|
|
||||||
</dl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DisplayStateCard: Component<DisplayStateCardProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<section class="p-2 rounded shadow">
|
|
||||||
<Item label="Brightness">{props.state.brightness}</Item>
|
|
||||||
<Item label="Max Brightness">{props.state.max_brightness}</Item>
|
|
||||||
<Item label="Min Brightness">{props.state.min_brightness}</Item>
|
|
||||||
<Item label="Contrast">{props.state.contrast}</Item>
|
|
||||||
<Item label="Max Contrast">{props.state.max_contrast}</Item>
|
|
||||||
<Item label="Min Contrast">{props.state.min_contrast}</Item>
|
|
||||||
<Item label="Max Mode">{props.state.max_mode}</Item>
|
|
||||||
<Item label="Min Mode">{props.state.min_mode}</Item>
|
|
||||||
<Item label="Mode">{props.state.mode}</Item>
|
|
||||||
<Item label="Last Modified At">{props.state.last_modified_at.toISOString()}</Item>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,52 +0,0 @@
|
|||||||
import { Component, For, createEffect, createSignal } from 'solid-js';
|
|
||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import debug from 'debug';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import { DisplayState, RawDisplayState } from '../../models/display-state.model';
|
|
||||||
import { DisplayStateCard } from './display-state-card';
|
|
||||||
|
|
||||||
const logger = debug('app:components:displays:display-state-index');
|
|
||||||
|
|
||||||
export const DisplayStateIndex: Component = () => {
|
|
||||||
const [states, setStates] = createSignal<DisplayState[]>([]);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const unlisten = listen<RawDisplayState[]>('displays_changed', (ev) => {
|
|
||||||
logger('displays_changed', ev);
|
|
||||||
setStates(
|
|
||||||
ev.payload.map((it) => ({
|
|
||||||
...it,
|
|
||||||
last_modified_at: new Date(it.last_modified_at.secs_since_epoch * 1000),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
invoke<RawDisplayState[]>('get_displays').then((states) => {
|
|
||||||
logger('get_displays', states);
|
|
||||||
setStates(
|
|
||||||
states.map((it) => ({
|
|
||||||
...it,
|
|
||||||
last_modified_at: new Date(it.last_modified_at.secs_since_epoch * 1000),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlisten.then((unlisten) => unlisten());
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<ol class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 p-2 gap-2">
|
|
||||||
<For each={states()}>
|
|
||||||
{(state, index) => (
|
|
||||||
<li class="bg-slate-50 text-gray-800 relative border-2 border-slate-50 hover:border-sky-300 focus:border-sky-300 transition">
|
|
||||||
<DisplayStateCard state={state} />
|
|
||||||
<span class="absolute left-2 -top-3 bg-sky-300 text-white px-1 py-0.5 text-xs rounded-sm font-mono">
|
|
||||||
#{index() + 1}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,42 +0,0 @@
|
|||||||
import { Component, For, createEffect, createSignal } from 'solid-js';
|
|
||||||
import { BoardInfo } from '../../models/board-info.model';
|
|
||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import debug from 'debug';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import { BoardInfoPanel } from './board-info-panel';
|
|
||||||
|
|
||||||
const logger = debug('app:components:info:board-index');
|
|
||||||
|
|
||||||
export const BoardIndex: Component = () => {
|
|
||||||
const [boards, setBoards] = createSignal<BoardInfo[]>([]);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const unlisten = listen<BoardInfo[]>('boards_changed', (ev) => {
|
|
||||||
logger('boards_changed', ev);
|
|
||||||
setBoards(ev.payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
invoke<BoardInfo[]>('get_boards').then((boards) => {
|
|
||||||
logger('get_boards', boards);
|
|
||||||
setBoards(boards);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
unlisten.then((unlisten) => unlisten());
|
|
||||||
};
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<ol class="grid sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 p-2 gap-2">
|
|
||||||
<For each={boards()}>
|
|
||||||
{(board, index) => (
|
|
||||||
<li class="bg-slate-50 text-gray-800 relative border-2 border-slate-50 hover:border-sky-300 focus:border-sky-300 transition">
|
|
||||||
<BoardInfoPanel board={board} />
|
|
||||||
<span class="absolute left-2 -top-3 bg-sky-300 text-white px-1 py-0.5 text-xs rounded-sm font-mono">
|
|
||||||
#{index() + 1}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,60 +0,0 @@
|
|||||||
import { Component, ParentComponent, createMemo } from 'solid-js';
|
|
||||||
import { BoardInfo } from '../../models/board-info.model';
|
|
||||||
|
|
||||||
type ItemProps = {
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Item: ParentComponent<ItemProps> = (props) => {
|
|
||||||
return (
|
|
||||||
<dl class="flex">
|
|
||||||
<dt class="w-20">{props.label}</dt>
|
|
||||||
<dd class="flex-auto">{props.children}</dd>
|
|
||||||
</dl>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BoardInfoPanel: Component<{ board: BoardInfo }> = (props) => {
|
|
||||||
const ttl = createMemo(() => {
|
|
||||||
if (props.board.connect_status !== 'Connected') {
|
|
||||||
return '--';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.board.ttl == null) {
|
|
||||||
return 'timeout';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span class="font-mono">{props.board.ttl.toFixed(0)}</span> ms
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const connectStatus = createMemo(() => {
|
|
||||||
if (typeof props.board.connect_status === 'string') {
|
|
||||||
return props.board.connect_status;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('Connecting' in props.board.connect_status) {
|
|
||||||
return `Connecting (${props.board.connect_status.Connecting.toFixed(0)})`;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section class="p-2 rounded shadow">
|
|
||||||
<Item label="Host">{props.board.fullname}</Item>
|
|
||||||
<Item label="Host">{props.board.host}</Item>
|
|
||||||
<Item label="Ip Addr">
|
|
||||||
<span class="font-mono">{props.board.address}</span>
|
|
||||||
</Item>
|
|
||||||
<Item label="Port">
|
|
||||||
<span class="font-mono">{props.board.port}</span>
|
|
||||||
</Item>
|
|
||||||
<Item label="Status">
|
|
||||||
<span class="font-mono">{connectStatus()}</span>
|
|
||||||
</Item>
|
|
||||||
<Item label="TTL">{ttl()}</Item>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,10 +0,0 @@
|
|||||||
import { Component } from 'solid-js';
|
|
||||||
import { BoardIndex } from './board-index';
|
|
||||||
|
|
||||||
export const InfoIndex: Component = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<BoardIndex />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,42 +0,0 @@
|
|||||||
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-slate-900/50 gap-2 text-white drop-shadow-[0_2px_2px_rgba(0,0,0,0.8)] rounded">
|
|
||||||
<dt class="uppercase w-1/2 select-all whitespace-nowrap">{props.label}</dt>
|
|
||||||
<dd class="select-all w-1/2 whitespace-nowrap">{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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,84 +0,0 @@
|
|||||||
import {
|
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
JSX,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
ParentComponent,
|
|
||||||
} from 'solid-js';
|
|
||||||
import { displayStore, setDisplayStore } from '../../stores/display.store';
|
|
||||||
import background from '../../assets/transparent-grid-background.svg?url';
|
|
||||||
|
|
||||||
export const DisplayListContainer: ParentComponent = (props) => {
|
|
||||||
let root: HTMLElement;
|
|
||||||
const [olStyle, setOlStyle] = createSignal({
|
|
||||||
top: '0px',
|
|
||||||
left: '0px',
|
|
||||||
});
|
|
||||||
const [rootStyle, setRootStyle] = createSignal<JSX.CSSProperties>({
|
|
||||||
height: '100%',
|
|
||||||
});
|
|
||||||
const [bound, setBound] = createSignal({
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: 100,
|
|
||||||
bottom: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
const resetSize = () => {
|
|
||||||
const _bound = bound();
|
|
||||||
|
|
||||||
setDisplayStore({
|
|
||||||
viewScale: root.clientWidth / (_bound.right - _bound.left),
|
|
||||||
});
|
|
||||||
|
|
||||||
setOlStyle({
|
|
||||||
top: `${-_bound.top * displayStore.viewScale}px`,
|
|
||||||
left: `${-_bound.left * displayStore.viewScale}px`,
|
|
||||||
});
|
|
||||||
|
|
||||||
setRootStyle({
|
|
||||||
height: `${(_bound.bottom - _bound.top) * displayStore.viewScale}px`,
|
|
||||||
background: `url(${background})`,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const boundLeft = Math.min(0, ...displayStore.displays.map((display) => display.x));
|
|
||||||
const boundTop = Math.min(0, ...displayStore.displays.map((display) => display.y));
|
|
||||||
const boundRight = Math.max(
|
|
||||||
0,
|
|
||||||
...displayStore.displays.map((display) => display.x + display.width),
|
|
||||||
);
|
|
||||||
const boundBottom = Math.max(
|
|
||||||
0,
|
|
||||||
...displayStore.displays.map((display) => display.y + display.height),
|
|
||||||
);
|
|
||||||
|
|
||||||
setBound({
|
|
||||||
left: boundLeft,
|
|
||||||
top: boundTop,
|
|
||||||
right: boundRight,
|
|
||||||
bottom: boundBottom,
|
|
||||||
});
|
|
||||||
let observer: ResizeObserver;
|
|
||||||
onMount(() => {
|
|
||||||
observer = new ResizeObserver(resetSize);
|
|
||||||
observer.observe(root);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
observer?.unobserve(root);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section ref={root!} class="relative bg-gray-400/30" style={rootStyle()}>
|
|
||||||
<ol class="absolute" style={olStyle()}>
|
|
||||||
{props.children}
|
|
||||||
</ol>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,65 +0,0 @@
|
|||||||
import { Component, createMemo } from 'solid-js';
|
|
||||||
import { DisplayInfo } from '../../models/display-info.model';
|
|
||||||
import { displayStore } from '../../stores/display.store';
|
|
||||||
import { ledStripStore } from '../../stores/led-strip.store';
|
|
||||||
import { DisplayInfoPanel } from './display-info-panel';
|
|
||||||
import { LedStripPart } from './led-strip-part';
|
|
||||||
import { ScreenView } from './screen-view';
|
|
||||||
|
|
||||||
type DisplayViewProps = {
|
|
||||||
display: DisplayInfo;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DisplayView: Component<DisplayViewProps> = (props) => {
|
|
||||||
const size = createMemo(() => ({
|
|
||||||
width: props.display.width * displayStore.viewScale,
|
|
||||||
height: props.display.height * displayStore.viewScale,
|
|
||||||
}));
|
|
||||||
const style = createMemo(() => ({
|
|
||||||
top: `${props.display.y * displayStore.viewScale}px`,
|
|
||||||
left: `${props.display.x * displayStore.viewScale}px`,
|
|
||||||
height: `${size().height}px`,
|
|
||||||
width: `${size().width}px`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const ledStripConfigs = createMemo(() => {
|
|
||||||
console.log('ledStripConfigs', ledStripStore.strips);
|
|
||||||
return ledStripStore.strips.filter((c) => c.display_id === props.display.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
class="absolute grid grid-cols-[16px,auto,16px] grid-rows-[16px,auto,16px] overflow-hidden"
|
|
||||||
style={style()}
|
|
||||||
>
|
|
||||||
<ScreenView
|
|
||||||
class="row-start-2 col-start-2 group"
|
|
||||||
displayId={props.display.id}
|
|
||||||
style={{
|
|
||||||
'object-fit': 'contain',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DisplayInfoPanel
|
|
||||||
display={props.display}
|
|
||||||
class="absolute bg-slate-700/20 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded backdrop-blur w-1/3 min-w-[300px] text-black group-hover:opacity-100 opacity-0 transition-opacity"
|
|
||||||
/>
|
|
||||||
</ScreenView>
|
|
||||||
<LedStripPart
|
|
||||||
class="row-start-1 col-start-2 flex-row overflow-hidden"
|
|
||||||
config={ledStripConfigs().find((c) => c.border === 'Top')}
|
|
||||||
/>
|
|
||||||
<LedStripPart
|
|
||||||
class="row-start-2 col-start-1 flex-col overflow-hidden"
|
|
||||||
config={ledStripConfigs().find((c) => c.border === 'Left')}
|
|
||||||
/>
|
|
||||||
<LedStripPart
|
|
||||||
class="row-start-2 col-start-3 flex-col overflow-hidden"
|
|
||||||
config={ledStripConfigs().find((c) => c.border === 'Right')}
|
|
||||||
/>
|
|
||||||
<LedStripPart
|
|
||||||
class="row-start-3 col-start-2 flex-row overflow-hidden"
|
|
||||||
config={ledStripConfigs().find((c) => c.border === 'Bottom')}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,106 +0,0 @@
|
|||||||
import { createEffect, onCleanup } from 'solid-js';
|
|
||||||
import { invoke } from '@tauri-apps/api/tauri';
|
|
||||||
import { DisplayView } from './display-view';
|
|
||||||
import { DisplayListContainer } from './display-list-container';
|
|
||||||
import { displayStore, setDisplayStore } from '../../stores/display.store';
|
|
||||||
import { LedStripConfigContainer } from '../../models/led-strip-config';
|
|
||||||
import { setLedStripStore } from '../../stores/led-strip.store';
|
|
||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import { LedStripPartsSorter } from './led-strip-parts-sorter';
|
|
||||||
import { createStore } from 'solid-js/store';
|
|
||||||
import {
|
|
||||||
LedStripConfigurationContext,
|
|
||||||
LedStripConfigurationContextType,
|
|
||||||
} from '../../contexts/led-strip-configuration.context';
|
|
||||||
|
|
||||||
export const LedStripConfiguration = () => {
|
|
||||||
createEffect(() => {
|
|
||||||
invoke<string>('list_display_info').then((displays) => {
|
|
||||||
setDisplayStore({
|
|
||||||
displays: JSON.parse(displays),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
invoke<LedStripConfigContainer>('read_led_strip_configs').then((configs) => {
|
|
||||||
console.log(configs);
|
|
||||||
setLedStripStore(configs);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// listen to config_changed event
|
|
||||||
createEffect(() => {
|
|
||||||
const unlisten = listen('config_changed', (event) => {
|
|
||||||
const { strips, mappers } = event.payload as LedStripConfigContainer;
|
|
||||||
console.log(event.payload);
|
|
||||||
setLedStripStore({
|
|
||||||
strips,
|
|
||||||
mappers,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
unlisten.then((unlisten) => unlisten());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// listen to led_colors_changed event
|
|
||||||
createEffect(() => {
|
|
||||||
const unlisten = listen<Uint8ClampedArray>('led_colors_changed', (event) => {
|
|
||||||
if (!window.document.hidden) {
|
|
||||||
const colors = event.payload;
|
|
||||||
setLedStripStore({
|
|
||||||
colors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
unlisten.then((unlisten) => unlisten());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// listen to led_sorted_colors_changed event
|
|
||||||
createEffect(() => {
|
|
||||||
const unlisten = listen<Uint8ClampedArray>('led_sorted_colors_changed', (event) => {
|
|
||||||
if (!window.document.hidden) {
|
|
||||||
const sortedColors = event.payload;
|
|
||||||
setLedStripStore({
|
|
||||||
sortedColors,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
unlisten.then((unlisten) => unlisten());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const [ledStripConfiguration, setLedStripConfiguration] = createStore<
|
|
||||||
LedStripConfigurationContextType[0]
|
|
||||||
>({
|
|
||||||
selectedStripPart: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ledStripConfigurationContextValue: LedStripConfigurationContextType = [
|
|
||||||
ledStripConfiguration,
|
|
||||||
{
|
|
||||||
setSelectedStripPart: (v) => {
|
|
||||||
setLedStripConfiguration({
|
|
||||||
selectedStripPart: v,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<LedStripConfigurationContext.Provider value={ledStripConfigurationContextValue}>
|
|
||||||
<LedStripPartsSorter />
|
|
||||||
<DisplayListContainer>
|
|
||||||
{displayStore.displays.map((display) => {
|
|
||||||
return <DisplayView display={display} />;
|
|
||||||
})}
|
|
||||||
</DisplayListContainer>
|
|
||||||
</LedStripConfigurationContext.Provider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,137 +0,0 @@
|
|||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createRoot,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
JSX,
|
|
||||||
splitProps,
|
|
||||||
useContext,
|
|
||||||
} from 'solid-js';
|
|
||||||
import { useTippy } from 'solid-tippy';
|
|
||||||
import { followCursor } from 'tippy.js';
|
|
||||||
import { LedStripConfig } from '../../models/led-strip-config';
|
|
||||||
import { LedStripConfigurationContext } from '../../contexts/led-strip-configuration.context';
|
|
||||||
import { ledStripStore } from '../../stores/led-strip.store';
|
|
||||||
|
|
||||||
type LedStripPartProps = {
|
|
||||||
config?: LedStripConfig | null;
|
|
||||||
} & JSX.HTMLAttributes<HTMLElement>;
|
|
||||||
|
|
||||||
type PixelProps = {
|
|
||||||
color: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Pixel: Component<PixelProps> = (props) => {
|
|
||||||
const style = createMemo(() => ({
|
|
||||||
background: props.color,
|
|
||||||
}));
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex-auto flex h-full w-full justify-center items-center relative"
|
|
||||||
title={props.color}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute top-1/2 -translate-y-1/2 h-2.5 w-2.5 rounded-full ring-1 ring-stone-300"
|
|
||||||
style={style()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LedStripPart: Component<LedStripPartProps> = (props) => {
|
|
||||||
const [localProps, rootProps] = splitProps(props, ['config']);
|
|
||||||
const [stripConfiguration] = useContext(LedStripConfigurationContext);
|
|
||||||
|
|
||||||
const [colors, setColors] = createSignal<string[]>([]);
|
|
||||||
|
|
||||||
// update led strip colors from global store
|
|
||||||
createEffect(() => {
|
|
||||||
if (!localProps.config) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = ledStripStore.strips.findIndex(
|
|
||||||
(s) =>
|
|
||||||
s.display_id === localProps.config!.display_id &&
|
|
||||||
s.border === localProps.config!.border,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapper = ledStripStore.mappers[index];
|
|
||||||
if (!mapper) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const offset = mapper.pos * 3;
|
|
||||||
|
|
||||||
const colors = new Array(localProps.config.len).fill(null).map((_, i) => {
|
|
||||||
const index = offset + i * 3;
|
|
||||||
return `rgb(${ledStripStore.colors[index]}, ${ledStripStore.colors[index + 1]}, ${
|
|
||||||
ledStripStore.colors[index + 2]
|
|
||||||
})`;
|
|
||||||
});
|
|
||||||
|
|
||||||
setColors(colors);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [anchor, setAnchor] = createSignal<HTMLElement>();
|
|
||||||
|
|
||||||
useTippy(anchor, {
|
|
||||||
hidden: true,
|
|
||||||
props: {
|
|
||||||
trigger: 'mouseenter focus',
|
|
||||||
followCursor: true,
|
|
||||||
|
|
||||||
plugins: [followCursor],
|
|
||||||
|
|
||||||
content: () =>
|
|
||||||
createRoot(() => {
|
|
||||||
return (
|
|
||||||
<span class="rounded-lg bg-slate-400/50 backdrop-blur text-white p-2 drop-shadow">
|
|
||||||
Count: {localProps.config?.len ?? '--'}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}) as Element,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const onWheel = (e: WheelEvent) => {
|
|
||||||
if (localProps.config) {
|
|
||||||
invoke('patch_led_strip_len', {
|
|
||||||
displayId: localProps.config.display_id,
|
|
||||||
border: localProps.config.border,
|
|
||||||
deltaLen: e.deltaY > 0 ? 1 : -1,
|
|
||||||
})
|
|
||||||
.then(() => {})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
{...rootProps}
|
|
||||||
ref={setAnchor}
|
|
||||||
class={
|
|
||||||
'flex rounded-full flex-nowrap justify-around items-center overflow-hidden ' +
|
|
||||||
rootProps.class
|
|
||||||
}
|
|
||||||
classList={{
|
|
||||||
'ring ring-inset bg-yellow-400/50 ring-orange-400 animate-pulse':
|
|
||||||
stripConfiguration.selectedStripPart?.border === localProps.config?.border &&
|
|
||||||
stripConfiguration.selectedStripPart?.displayId ===
|
|
||||||
localProps.config?.display_id,
|
|
||||||
}}
|
|
||||||
onWheel={onWheel}
|
|
||||||
>
|
|
||||||
<For each={colors()}>{(item) => <Pixel color={item} />}</For>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,316 +0,0 @@
|
|||||||
import {
|
|
||||||
batch,
|
|
||||||
Component,
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
For,
|
|
||||||
Index,
|
|
||||||
JSX,
|
|
||||||
Match,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
Switch,
|
|
||||||
untrack,
|
|
||||||
useContext,
|
|
||||||
} from 'solid-js';
|
|
||||||
import { LedStripConfig, LedStripPixelMapper } from '../../models/led-strip-config';
|
|
||||||
import { ledStripStore } from '../../stores/led-strip.store';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import { LedStripConfigurationContext } from '../../contexts/led-strip-configuration.context';
|
|
||||||
import background from '../../assets/transparent-grid-background.svg?url';
|
|
||||||
|
|
||||||
const SorterItem: Component<{ strip: LedStripConfig; mapper: LedStripPixelMapper }> = (
|
|
||||||
props,
|
|
||||||
) => {
|
|
||||||
const [leds, setLeds] = createSignal<Array<string | null>>([]);
|
|
||||||
const [dragging, setDragging] = createSignal<boolean>(false);
|
|
||||||
const [dragStart, setDragStart] = createSignal<{ x: number; y: number } | null>(null);
|
|
||||||
const [dragCurr, setDragCurr] = createSignal<{ x: number; y: number } | null>(null);
|
|
||||||
const [dragStartIndex, setDragStartIndex] = createSignal<number>(0);
|
|
||||||
const [cellWidth, setCellWidth] = createSignal<number>(0);
|
|
||||||
const [, { setSelectedStripPart }] = useContext(LedStripConfigurationContext);
|
|
||||||
const [rootWidth, setRootWidth] = createSignal<number>(0);
|
|
||||||
|
|
||||||
let root: HTMLDivElement;
|
|
||||||
|
|
||||||
const move = (targetStart: number) => {
|
|
||||||
if (targetStart === props.mapper.start) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(
|
|
||||||
`moving strip part ${props.strip.display_id} ${props.strip.border} from ${props.mapper.start} to ${targetStart}`,
|
|
||||||
);
|
|
||||||
invoke('move_strip_part', {
|
|
||||||
displayId: props.strip.display_id,
|
|
||||||
border: props.strip.border,
|
|
||||||
targetStart,
|
|
||||||
}).catch((err) => console.error(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
// reset translateX on config updated
|
|
||||||
createEffect(() => {
|
|
||||||
const indexDiff = props.mapper.start - dragStartIndex();
|
|
||||||
const start = untrack(dragStart);
|
|
||||||
const curr = untrack(dragCurr);
|
|
||||||
const _dragging = untrack(dragging);
|
|
||||||
|
|
||||||
if (start === null || curr === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (_dragging && indexDiff !== 0) {
|
|
||||||
const compensation = indexDiff * cellWidth();
|
|
||||||
batch(() => {
|
|
||||||
setDragStartIndex(props.mapper.start);
|
|
||||||
setDragStart({
|
|
||||||
x: start.x + compensation,
|
|
||||||
y: curr.y,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
batch(() => {
|
|
||||||
setDragStartIndex(props.mapper.start);
|
|
||||||
setDragStart(null);
|
|
||||||
setDragCurr(null);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const onPointerDown = (ev: PointerEvent) => {
|
|
||||||
if (ev.button !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
batch(() => {
|
|
||||||
setDragging(true);
|
|
||||||
if (dragStart() === null) {
|
|
||||||
setDragStart({ x: ev.clientX, y: ev.clientY });
|
|
||||||
}
|
|
||||||
setDragCurr({ x: ev.clientX, y: ev.clientY });
|
|
||||||
setDragStartIndex(props.mapper.start);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerUp = (ev: PointerEvent) => {
|
|
||||||
if (ev.button !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dragging() === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDragging(false);
|
|
||||||
const diff = ev.clientX - dragStart()!.x;
|
|
||||||
const moved = Math.round(diff / cellWidth());
|
|
||||||
if (moved === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
move(props.mapper.start + moved);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerMove = (ev: PointerEvent) => {
|
|
||||||
if (dragging() === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedStripPart({
|
|
||||||
displayId: props.strip.display_id,
|
|
||||||
border: props.strip.border,
|
|
||||||
});
|
|
||||||
if (!(ev.buttons & 1)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const draggingInfo = dragging();
|
|
||||||
if (!draggingInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setDragCurr({ x: ev.clientX, y: ev.clientY });
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPointerLeave = () => {
|
|
||||||
setSelectedStripPart(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
onMount(() => {
|
|
||||||
window.addEventListener('pointermove', onPointerMove);
|
|
||||||
window.addEventListener('pointerleave', onPointerLeave);
|
|
||||||
window.addEventListener('pointerup', onPointerUp);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('pointermove', onPointerMove);
|
|
||||||
window.removeEventListener('pointerleave', onPointerLeave);
|
|
||||||
window.removeEventListener('pointerup', onPointerUp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const reverse = () => {
|
|
||||||
invoke('reverse_led_strip_part', {
|
|
||||||
displayId: props.strip.display_id,
|
|
||||||
border: props.strip.border,
|
|
||||||
}).catch((err) => console.error(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
const setColor = (fullIndex: number, colorsIndex: number, fullLeds: string[]) => {
|
|
||||||
const colors = ledStripStore.colors;
|
|
||||||
let c1 = `rgb(${Math.floor(colors[colorsIndex * 3] * 0.8)}, ${Math.floor(
|
|
||||||
colors[colorsIndex * 3 + 1] * 0.8,
|
|
||||||
)}, ${Math.floor(colors[colorsIndex * 3 + 2] * 0.8)})`;
|
|
||||||
let c2 = `rgb(${Math.min(Math.floor(colors[colorsIndex * 3] * 1.2), 255)}, ${Math.min(
|
|
||||||
Math.floor(colors[colorsIndex * 3 + 1] * 1.2),
|
|
||||||
255,
|
|
||||||
)}, ${Math.min(Math.floor(colors[colorsIndex * 3 + 2] * 1.2), 255)})`;
|
|
||||||
|
|
||||||
if (fullLeds.length <= fullIndex) {
|
|
||||||
console.error('out of range', fullIndex, fullLeds.length);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fullLeds[fullIndex] = `linear-gradient(70deg, ${c1} 10%, ${c2})`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// update fullLeds
|
|
||||||
createEffect(() => {
|
|
||||||
const { start, end, pos } = props.mapper;
|
|
||||||
|
|
||||||
const leds = new Array(Math.abs(start - end)).fill(null);
|
|
||||||
|
|
||||||
if (start < end) {
|
|
||||||
for (let i = 0, j = pos; i < leds.length; i++, j++) {
|
|
||||||
setColor(i, j, leds);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (let i = leds.length - 1, j = pos; i >= 0; i--, j++) {
|
|
||||||
setColor(i, j, leds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLeds(leds);
|
|
||||||
});
|
|
||||||
|
|
||||||
// update rootWidth
|
|
||||||
createEffect(() => {
|
|
||||||
let observer: ResizeObserver;
|
|
||||||
onMount(() => {
|
|
||||||
observer = new ResizeObserver(() => {
|
|
||||||
setRootWidth(root.clientWidth);
|
|
||||||
});
|
|
||||||
observer.observe(root);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
observer?.unobserve(root);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
// update cellWidth
|
|
||||||
createEffect(() => {
|
|
||||||
const cellWidth = rootWidth() / ledStripStore.totalLedCount;
|
|
||||||
setCellWidth(cellWidth);
|
|
||||||
});
|
|
||||||
|
|
||||||
const style = createMemo<JSX.CSSProperties>(() => {
|
|
||||||
return {
|
|
||||||
transform: `translateX(${
|
|
||||||
(dragCurr()?.x ?? 0) -
|
|
||||||
(dragStart()?.x ?? 0) +
|
|
||||||
cellWidth() * Math.min(props.mapper.start, props.mapper.end)
|
|
||||||
}px)`,
|
|
||||||
width: `${cellWidth() * leds().length}px`,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="flex mx-2 select-none cursor-ew-resize focus:cursor-ew-resize"
|
|
||||||
onPointerDown={onPointerDown}
|
|
||||||
ondblclick={reverse}
|
|
||||||
ref={root!}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={style()}
|
|
||||||
class="rounded-full border border-white flex h-3"
|
|
||||||
classList={{
|
|
||||||
'bg-gradient-to-b from-yellow-500/60 to-orange-300/60': dragging(),
|
|
||||||
'bg-gradient-to-b from-white/50 to-stone-500/40': !dragging(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<For each={leds()}>
|
|
||||||
{(it) => (
|
|
||||||
<div
|
|
||||||
class="flex-auto flex h-full w-full justify-center items-center relative"
|
|
||||||
title={it ?? ''}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute top-1/2 -translate-y-1/2 h-2.5 w-2.5 rounded-full ring-1 ring-stone-100"
|
|
||||||
classList={{ 'ring-stone-300/50': !it }}
|
|
||||||
style={{ background: it ?? 'transparent' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const SorterResult: Component = () => {
|
|
||||||
const [fullLeds, setFullLeds] = createSignal<string[]>([]);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
const colors = ledStripStore.sortedColors;
|
|
||||||
const fullLeds = new Array(ledStripStore.totalLedCount)
|
|
||||||
.fill('rgba(255,255,255,0.1)')
|
|
||||||
.map((_, i) => {
|
|
||||||
let c1 = `rgb(${Math.floor(colors[i * 3] * 0.8)}, ${Math.floor(
|
|
||||||
colors[i * 3 + 1] * 0.8,
|
|
||||||
)}, ${Math.floor(colors[i * 3 + 2] * 0.8)})`;
|
|
||||||
let c2 = `rgb(${Math.min(Math.floor(colors[i * 3] * 1.2), 255)}, ${Math.min(
|
|
||||||
Math.floor(colors[i * 3 + 1] * 1.2),
|
|
||||||
255,
|
|
||||||
)}, ${Math.min(Math.floor(colors[i * 3 + 2] * 1.2), 255)})`;
|
|
||||||
|
|
||||||
return `linear-gradient(70deg, ${c1} 10%, ${c2})`;
|
|
||||||
});
|
|
||||||
setFullLeds(fullLeds);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="flex h-2 m-2">
|
|
||||||
<For each={fullLeds()}>
|
|
||||||
{(it) => (
|
|
||||||
<div
|
|
||||||
class="flex-auto flex h-full w-full justify-center items-center relative"
|
|
||||||
title={it}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute top-1/2 -translate-y-1/2 h-2.5 w-2.5 rounded-full ring-1 ring-stone-300"
|
|
||||||
style={{ background: it }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LedStripPartsSorter: Component = () => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class="select-none overflow-hidden"
|
|
||||||
style={{
|
|
||||||
'background-image': `url(${background})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<SorterResult />
|
|
||||||
<Index each={ledStripStore.strips}>
|
|
||||||
{(strip, index) => (
|
|
||||||
<Switch>
|
|
||||||
<Match when={strip().len > 0}>
|
|
||||||
<SorterItem strip={strip()} mapper={ledStripStore.mappers[index]} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
)}
|
|
||||||
</Index>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,165 +0,0 @@
|
|||||||
import { convertFileSrc } from '@tauri-apps/api/tauri';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
JSX,
|
|
||||||
onCleanup,
|
|
||||||
onMount,
|
|
||||||
splitProps,
|
|
||||||
} from 'solid-js';
|
|
||||||
|
|
||||||
type ScreenViewProps = {
|
|
||||||
displayId: number;
|
|
||||||
} & JSX.HTMLAttributes<HTMLDivElement>;
|
|
||||||
|
|
||||||
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
|
||||||
const [localProps, rootProps] = splitProps(props, ['displayId']);
|
|
||||||
let canvas: HTMLCanvasElement;
|
|
||||||
let root: HTMLDivElement;
|
|
||||||
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
|
||||||
const [drawInfo, setDrawInfo] = createSignal({
|
|
||||||
drawX: 0,
|
|
||||||
drawY: 0,
|
|
||||||
drawWidth: 0,
|
|
||||||
drawHeight: 0,
|
|
||||||
});
|
|
||||||
const [imageData, setImageData] = createSignal<{
|
|
||||||
buffer: Uint8ClampedArray;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
} | null>(null);
|
|
||||||
const [hidden, setHidden] = createSignal(false);
|
|
||||||
|
|
||||||
const resetSize = () => {
|
|
||||||
const aspectRatio = canvas.width / canvas.height;
|
|
||||||
|
|
||||||
const drawWidth = Math.round(
|
|
||||||
Math.min(root.clientWidth, root.clientHeight * aspectRatio),
|
|
||||||
);
|
|
||||||
const drawHeight = Math.round(
|
|
||||||
Math.min(root.clientHeight, root.clientWidth / aspectRatio),
|
|
||||||
);
|
|
||||||
|
|
||||||
const drawX = Math.round((root.clientWidth - drawWidth) / 2);
|
|
||||||
const drawY = Math.round((root.clientHeight - drawHeight) / 2);
|
|
||||||
|
|
||||||
setDrawInfo({
|
|
||||||
drawX,
|
|
||||||
drawY,
|
|
||||||
drawWidth,
|
|
||||||
drawHeight,
|
|
||||||
});
|
|
||||||
|
|
||||||
canvas.width = root.clientWidth;
|
|
||||||
canvas.height = root.clientHeight;
|
|
||||||
|
|
||||||
draw(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const draw = (cached: boolean = false) => {
|
|
||||||
const { drawX, drawY } = drawInfo();
|
|
||||||
|
|
||||||
let _ctx = ctx();
|
|
||||||
let raw = imageData();
|
|
||||||
if (_ctx && raw) {
|
|
||||||
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
||||||
if (cached) {
|
|
||||||
for (let i = 3; i < raw.buffer.length; i += 8) {
|
|
||||||
raw.buffer[i] = Math.floor(raw.buffer[i] * 0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const img = new ImageData(raw.buffer, raw.width, raw.height);
|
|
||||||
_ctx.putImageData(img, drawX, drawY);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// get screenshot
|
|
||||||
createEffect(() => {
|
|
||||||
let stopped = false;
|
|
||||||
const frame = async () => {
|
|
||||||
const { drawWidth, drawHeight } = drawInfo();
|
|
||||||
const url = convertFileSrc(
|
|
||||||
`displays/${localProps.displayId}?width=${drawWidth}&height=${drawHeight}`,
|
|
||||||
'ambient-light',
|
|
||||||
);
|
|
||||||
await fetch(url, {
|
|
||||||
mode: 'cors',
|
|
||||||
})
|
|
||||||
.then((res) => res.body?.getReader().read())
|
|
||||||
.then((buffer) => {
|
|
||||||
if (buffer?.value) {
|
|
||||||
setImageData({
|
|
||||||
buffer: new Uint8ClampedArray(buffer?.value),
|
|
||||||
width: drawWidth,
|
|
||||||
height: drawHeight,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setImageData(null);
|
|
||||||
}
|
|
||||||
draw();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
while (!stopped) {
|
|
||||||
if (hidden()) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
await frame();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
stopped = true;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// resize
|
|
||||||
createEffect(() => {
|
|
||||||
let resizeObserver: ResizeObserver;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
setCtx(canvas.getContext('2d'));
|
|
||||||
new ResizeObserver(() => {
|
|
||||||
resetSize();
|
|
||||||
}).observe(root);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
resizeObserver?.unobserve(root);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// update hidden
|
|
||||||
createEffect(() => {
|
|
||||||
const hide = () => {
|
|
||||||
setHidden(true);
|
|
||||||
console.log('hide');
|
|
||||||
};
|
|
||||||
const show = () => {
|
|
||||||
setHidden(false);
|
|
||||||
console.log('show');
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('focus', show);
|
|
||||||
window.addEventListener('blur', hide);
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener('focus', show);
|
|
||||||
window.removeEventListener('blur', hide);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={root!}
|
|
||||||
{...rootProps}
|
|
||||||
class={'overflow-hidden h-full w-full ' + rootProps.class}
|
|
||||||
>
|
|
||||||
<canvas ref={canvas!} />
|
|
||||||
{rootProps.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,22 +0,0 @@
|
|||||||
import { Component, JSX } from 'solid-js';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
value?: number;
|
|
||||||
} & JSX.HTMLAttributes<HTMLInputElement>;
|
|
||||||
|
|
||||||
export const ColorSlider: Component<Props> = (props) => {
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
{...props}
|
|
||||||
max={1}
|
|
||||||
min={0}
|
|
||||||
step={0.01}
|
|
||||||
value={props.value}
|
|
||||||
class={
|
|
||||||
'w-full h-2 bg-gradient-to-r rounded-lg appearance-none cursor-pointer dark:bg-gray-700 drop-shadow ' +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,100 +0,0 @@
|
|||||||
import { Component, createSignal } from 'solid-js';
|
|
||||||
|
|
||||||
const ColorItem: Component<{
|
|
||||||
color: string;
|
|
||||||
position: [number, number];
|
|
||||||
size?: [number, number];
|
|
||||||
onClick?: (color: string) => void;
|
|
||||||
}> = (props) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
background: props.color,
|
|
||||||
'grid-row-start': props.position[0],
|
|
||||||
'grid-column-start': props.position[1],
|
|
||||||
'grid-row-end': props.position[0] + (props.size ? props.size[0] : 1),
|
|
||||||
'grid-column-end': props.position[1] + (props.size ? props.size[1] : 1),
|
|
||||||
cursor: props.onClick ? 'pointer' : 'default',
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
props.onClick?.(props.color);
|
|
||||||
}}
|
|
||||||
title={props.color}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TestColorsBg: Component = () => {
|
|
||||||
const [singleColor, setSingleColor] = createSignal<string | null>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section
|
|
||||||
class="grid grid-cols-[8] grid-rows-[8] h-full w-full"
|
|
||||||
classList={{
|
|
||||||
hidden: singleColor() !== null,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ColorItem color="#ff0000" position={[1, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffff00" position={[1, 2]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ff00" position={[1, 3]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ffff" position={[1, 4]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#0000ff" position={[1, 5]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ff00ff" position={[1, 6]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffffff" position={[1, 7]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#000000" position={[1, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffff00" position={[2, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ff00" position={[3, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ffff" position={[4, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#0000ff" position={[5, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ff00ff" position={[6, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffffff" position={[7, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#000000" position={[8, 1]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffffff" position={[2, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ff00ff" position={[3, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#0000ff" position={[4, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ffff" position={[5, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ff00" position={[6, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffff00" position={[7, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ff0000" position={[8, 8]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffffff" position={[8, 2]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ff00ff" position={[8, 3]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#0000ff" position={[8, 4]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ffff" position={[8, 5]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#00ff00" position={[8, 6]} onClick={setSingleColor} />
|
|
||||||
<ColorItem color="#ffff00" position={[8, 7]} onClick={setSingleColor} />
|
|
||||||
</section>
|
|
||||||
<section
|
|
||||||
class="grid grid-cols-[8] grid-rows-[8] h-full w-full"
|
|
||||||
classList={{
|
|
||||||
hidden: singleColor() === null,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ColorItem
|
|
||||||
color={singleColor()!}
|
|
||||||
position={[1, 1]}
|
|
||||||
size={[1, 7]}
|
|
||||||
onClick={() => setSingleColor(null)}
|
|
||||||
/>
|
|
||||||
<ColorItem
|
|
||||||
color={singleColor()!}
|
|
||||||
position={[8, 2]}
|
|
||||||
size={[1, 7]}
|
|
||||||
onClick={() => setSingleColor(null)}
|
|
||||||
/>
|
|
||||||
<ColorItem
|
|
||||||
color={singleColor()!}
|
|
||||||
position={[2, 1]}
|
|
||||||
size={[7, 1]}
|
|
||||||
onClick={() => setSingleColor(null)}
|
|
||||||
/>
|
|
||||||
<ColorItem
|
|
||||||
color={singleColor()!}
|
|
||||||
position={[1, 8]}
|
|
||||||
size={[7, 1]}
|
|
||||||
onClick={() => setSingleColor(null)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,131 +0,0 @@
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
|
||||||
import { Component, createEffect, onCleanup } from 'solid-js';
|
|
||||||
import { ColorCalibration, LedStripConfigContainer } from '../../models/led-strip-config';
|
|
||||||
import { ledStripStore, setLedStripStore } from '../../stores/led-strip.store';
|
|
||||||
import { ColorSlider } from './color-slider';
|
|
||||||
import { TestColorsBg } from './test-colors-bg';
|
|
||||||
import { invoke } from '@tauri-apps/api';
|
|
||||||
import { VsClose } from 'solid-icons/vs';
|
|
||||||
import { BiRegularReset } from 'solid-icons/bi';
|
|
||||||
import transparentBg from '../../assets/transparent-grid-background.svg?url';
|
|
||||||
|
|
||||||
const Value: Component<{ value: number }> = (props) => {
|
|
||||||
return (
|
|
||||||
<span class="w-10 text-sm block font-mono text-right ">
|
|
||||||
{(props.value * 100).toFixed(0)}
|
|
||||||
<span class="text-xs text-stone-600">%</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const WhiteBalance = () => {
|
|
||||||
// listen to config_changed event
|
|
||||||
createEffect(() => {
|
|
||||||
const unlisten = listen('config_changed', (event) => {
|
|
||||||
const { strips, mappers, color_calibration } =
|
|
||||||
event.payload as LedStripConfigContainer;
|
|
||||||
console.log(event.payload);
|
|
||||||
setLedStripStore({
|
|
||||||
strips,
|
|
||||||
mappers,
|
|
||||||
colorCalibration: color_calibration,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
unlisten.then((unlisten) => unlisten());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateColorCalibration = (field: keyof ColorCalibration, value: number) => {
|
|
||||||
const calibration = { ...ledStripStore.colorCalibration, [field]: value };
|
|
||||||
invoke('set_color_calibration', {
|
|
||||||
calibration,
|
|
||||||
}).catch((error) => console.log(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
const exit = () => {
|
|
||||||
window.history.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
invoke('set_color_calibration', {
|
|
||||||
calibration: new ColorCalibration(),
|
|
||||||
}).catch((error) => console.log(error));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section class="select-none text-stone-800">
|
|
||||||
<div
|
|
||||||
class="absolute top-0 left-0 right-0 bottom-0"
|
|
||||||
style={{
|
|
||||||
'background-image': `url(${transparentBg})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TestColorsBg />
|
|
||||||
</div>
|
|
||||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-10/12 max-w-lg bg-stone-100/20 backdrop-blur p-5 rounded-xl shadow-lg">
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<span class="w-3 block">R:</span>
|
|
||||||
<ColorSlider
|
|
||||||
class="from-cyan-500 to-red-500"
|
|
||||||
value={ledStripStore.colorCalibration.r}
|
|
||||||
onInput={(ev) =>
|
|
||||||
updateColorCalibration(
|
|
||||||
'r',
|
|
||||||
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Value value={ledStripStore.colorCalibration.r} />
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<span class="w-3 block">G:</span>
|
|
||||||
<ColorSlider
|
|
||||||
class="from-pink-500 to-green-500"
|
|
||||||
value={ledStripStore.colorCalibration.g}
|
|
||||||
onInput={(ev) =>
|
|
||||||
updateColorCalibration(
|
|
||||||
'g',
|
|
||||||
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Value value={ledStripStore.colorCalibration.g} />
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<span class="w-3 block">B:</span>
|
|
||||||
<ColorSlider
|
|
||||||
class="from-yellow-500 to-blue-500"
|
|
||||||
value={ledStripStore.colorCalibration.b}
|
|
||||||
onInput={(ev) =>
|
|
||||||
updateColorCalibration(
|
|
||||||
'b',
|
|
||||||
(ev.target as HTMLInputElement).valueAsNumber ?? 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Value value={ledStripStore.colorCalibration.b} />
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<span class="w-3 block">W:</span>
|
|
||||||
<ColorSlider class="from-yellow-50 to-cyan-50" />
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
class="absolute -right-4 -top-4 rounded-full aspect-square bg-stone-100/20 backdrop-blur p-1 shadow hover:bg-stone-200/20 active:bg-stone-300"
|
|
||||||
onClick={exit}
|
|
||||||
title="Go Back"
|
|
||||||
>
|
|
||||||
<VsClose size={24} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="absolute -right-4 -bottom-4 rounded-full aspect-square bg-stone-100/20 backdrop-blur p-1 shadow hover:bg-stone-200/20 active:bg-stone-300"
|
|
||||||
onClick={reset}
|
|
||||||
title="Reset to 100%"
|
|
||||||
>
|
|
||||||
<BiRegularReset size={24} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
62
src/configurator/components/completed-container.tsx
Normal file
62
src/configurator/components/completed-container.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { isNil } from 'ramda';
|
||||||
|
import { FC, Fragment, useMemo } from 'react';
|
||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
import { useLedCount } from '../contents/led-count';
|
||||||
|
import { PixelRgb } from '../models/pixel-rgb';
|
||||||
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
import { BorderLedStrip } from './completed-led-strip';
|
||||||
|
|
||||||
|
const StyledCompletedContainerBorder = styled.section(
|
||||||
|
tw`dark:shadow-xl border-gray-600 bg-black border rounded-full flex flex-nowrap justify-around items-center p-px h-3 mb-1`,
|
||||||
|
css`
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1 / span 1;
|
||||||
|
justify-self: stretch;
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
interface StyledCompletedContainerProps {
|
||||||
|
borderLedStrips: BorderLedStrip[];
|
||||||
|
overrideBorderLedStrips?: BorderLedStrip[];
|
||||||
|
}
|
||||||
|
export const CompletedContainer: FC<StyledCompletedContainerProps> = ({
|
||||||
|
borderLedStrips,
|
||||||
|
overrideBorderLedStrips,
|
||||||
|
}) => {
|
||||||
|
const { ledCount } = useLedCount();
|
||||||
|
const completedPixels = useMemo(() => {
|
||||||
|
const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]);
|
||||||
|
(overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => {
|
||||||
|
if (isNil(config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (config.global_start_position <= config.global_end_position) {
|
||||||
|
pixels.forEach((color, i) => {
|
||||||
|
completed[config.global_start_position + i] = color;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pixels.forEach((color, i) => {
|
||||||
|
completed[config.global_start_position - i] = color;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return completed.map((color, i) => (
|
||||||
|
<StyledPixel
|
||||||
|
rgb={color}
|
||||||
|
key={i}
|
||||||
|
css={css`
|
||||||
|
grid-row-start: 1;
|
||||||
|
grid-column-start: ${i + 1};
|
||||||
|
align-self: flex-start;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [ledCount, borderLedStrips, overrideBorderLedStrips]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<StyledCompletedContainerBorder />
|
||||||
|
{completedPixels}
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
119
src/configurator/components/completed-led-strip.tsx
Normal file
119
src/configurator/components/completed-led-strip.tsx
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
100
src/configurator/components/display-with-led-strips.tsx
Normal file
100
src/configurator/components/display-with-led-strips.tsx
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { HTMLAttributes, useCallback, useMemo } from 'react';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
|
||||||
|
import { LedStrip } from './led-strip';
|
||||||
|
import tw, { css, styled, theme } from 'twin.macro';
|
||||||
|
import { ScreenshotDto } from '../models/screenshot.dto';
|
||||||
|
import { LedStripEditor } from './led-strip-editor';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { lensPath, lensProp, set, view } from 'ramda';
|
||||||
|
|
||||||
|
const logger = debug('app:display-with-led-strips');
|
||||||
|
|
||||||
|
export interface DisplayWithLedStripsProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
|
||||||
|
config: DisplayConfig;
|
||||||
|
screenshot: ScreenshotDto;
|
||||||
|
onChange?: (config: DisplayConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.section(
|
||||||
|
tw`m-4 grid gap-1`,
|
||||||
|
css`
|
||||||
|
grid-template-columns: ${theme`width.5`} ${theme`width.3`} auto ${theme`width.3`} ${theme`width.5`};
|
||||||
|
`,
|
||||||
|
css`
|
||||||
|
grid-template-rows: ${theme`width.5`} ${theme`width.3`} auto ${theme`width.3`} ${theme`width.5`};
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const DisplayWithLedStrips: FC<DisplayWithLedStripsProps> = ({
|
||||||
|
config,
|
||||||
|
screenshot,
|
||||||
|
onChange,
|
||||||
|
...htmlAttrs
|
||||||
|
}) => {
|
||||||
|
const screenshotUrl = useMemo(
|
||||||
|
() => `data:image/ico;base64,${screenshot.encode_image}`,
|
||||||
|
[screenshot.encode_image],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onLedStripConfigChange = useCallback(
|
||||||
|
(position: keyof LedStripConfigOfBorders, value: LedStripConfig | null) => {
|
||||||
|
const xLens = lensPath<
|
||||||
|
DisplayConfig,
|
||||||
|
'led_strip_of_borders',
|
||||||
|
keyof LedStripConfigOfBorders
|
||||||
|
>(['led_strip_of_borders', position]);
|
||||||
|
const c = set(xLens, value, config);
|
||||||
|
logger('on change. prev: %o, curr: %o', view(xLens, config), value);
|
||||||
|
onChange?.(c);
|
||||||
|
},
|
||||||
|
[config],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<StyledContainer {...htmlAttrs}>
|
||||||
|
<img src={screenshotUrl} tw="row-start-3 col-start-3 w-full" />
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.top}
|
||||||
|
colors={screenshot.colors.top}
|
||||||
|
tw="row-start-2 col-start-3"
|
||||||
|
/>
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.left}
|
||||||
|
colors={screenshot.colors.left}
|
||||||
|
tw="row-start-3 col-start-2"
|
||||||
|
/>
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.right}
|
||||||
|
colors={screenshot.colors.right}
|
||||||
|
tw="row-start-3 col-start-4"
|
||||||
|
/>
|
||||||
|
<LedStrip
|
||||||
|
config={config.led_strip_of_borders.bottom}
|
||||||
|
colors={screenshot.colors.bottom}
|
||||||
|
tw="row-start-4 col-start-3"
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.top}
|
||||||
|
tw="row-start-1 col-start-3"
|
||||||
|
onChange={(value) => onLedStripConfigChange('top', value)}
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.left}
|
||||||
|
tw="row-start-3 col-start-1"
|
||||||
|
onChange={(value) => onLedStripConfigChange('left', value)}
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.right}
|
||||||
|
tw="row-start-3 col-start-5"
|
||||||
|
onChange={(value) => onLedStripConfigChange('right', value)}
|
||||||
|
/>
|
||||||
|
<LedStripEditor
|
||||||
|
config={config.led_strip_of_borders.bottom}
|
||||||
|
tw="row-start-5 col-start-3"
|
||||||
|
onChange={(value) => onLedStripConfigChange('bottom', value)}
|
||||||
|
/>
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
199
src/configurator/components/draggable-strip.tsx
Normal file
199
src/configurator/components/draggable-strip.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import {
|
||||||
|
createRef,
|
||||||
|
FC,
|
||||||
|
Fragment,
|
||||||
|
MouseEventHandler,
|
||||||
|
ReactNode,
|
||||||
|
RefObject,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { css } from 'twin.macro';
|
||||||
|
import { useLedCount } from '../contents/led-count';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import { PixelRgb } from '../models/pixel-rgb';
|
||||||
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
import { logger } from './completed-led-strip';
|
||||||
|
|
||||||
|
interface DraggableStripProp {
|
||||||
|
config: LedStripConfig;
|
||||||
|
pixels: PixelRgb[];
|
||||||
|
index: number;
|
||||||
|
onConfigChange?: (config: LedStripConfig) => void;
|
||||||
|
onConfigFinish?: (config: LedStripConfig) => void;
|
||||||
|
}
|
||||||
|
export const DraggableStrip: FC<DraggableStripProp> = ({
|
||||||
|
config,
|
||||||
|
pixels,
|
||||||
|
index,
|
||||||
|
onConfigChange,
|
||||||
|
onConfigFinish,
|
||||||
|
}) => {
|
||||||
|
const { ledCount } = useLedCount();
|
||||||
|
|
||||||
|
const startXRef = useRef(0);
|
||||||
|
const currentXRef = useRef(0);
|
||||||
|
const configRef = useRef<LedStripConfig>();
|
||||||
|
const [availableConfig, setAvailableConfig] = useState<LedStripConfig>(config);
|
||||||
|
// const currentDiffRef = useRef(0);
|
||||||
|
const isDragRef = useRef(false);
|
||||||
|
const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>();
|
||||||
|
const [boxTranslateX, setBoxTranslateX] = useState(0);
|
||||||
|
|
||||||
|
const ledItems = useMemo(() => {
|
||||||
|
const step = config.global_start_position - config.global_end_position < 0 ? 1 : -1;
|
||||||
|
return pixels.map((rgb, i) => (
|
||||||
|
<StyledPixel
|
||||||
|
key={i}
|
||||||
|
rgb={rgb}
|
||||||
|
css={css`
|
||||||
|
grid-column: ${(availableConfig.global_start_position ?? 0) + i * step + 1} /
|
||||||
|
span 1;
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
pointer-events: none;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
}, [pixels, availableConfig]);
|
||||||
|
|
||||||
|
const [placeholders, placeholderRefs]: [ReactNode[], RefObject<HTMLSpanElement>[]] =
|
||||||
|
useMemo(
|
||||||
|
() =>
|
||||||
|
new Array(ledCount)
|
||||||
|
.fill(undefined)
|
||||||
|
.map((_, i) => {
|
||||||
|
const ref = createRef<HTMLSpanElement>();
|
||||||
|
const n = (
|
||||||
|
<span
|
||||||
|
ref={ref}
|
||||||
|
key={i}
|
||||||
|
tw=" h-full w-full"
|
||||||
|
css={css`
|
||||||
|
grid-column-start: ${i + 1};
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return [n, ref] as [ReactNode, RefObject<HTMLSpanElement>];
|
||||||
|
})
|
||||||
|
.reduce(
|
||||||
|
([nList, refList], [n, ref]) => [
|
||||||
|
[...nList, n],
|
||||||
|
[...refList, ref],
|
||||||
|
],
|
||||||
|
[[], []] as [ReactNode[], RefObject<HTMLSpanElement>[]],
|
||||||
|
),
|
||||||
|
[ledCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
// start and moving
|
||||||
|
const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||||
|
(ev) => {
|
||||||
|
startXRef.current = ev.pageX;
|
||||||
|
ev.currentTarget.requestPointerLock();
|
||||||
|
isDragRef.current = true;
|
||||||
|
logger('handleMouseDown, config: %o', config);
|
||||||
|
|
||||||
|
const placeholderPositions = placeholderRefs.map((it) => {
|
||||||
|
if (!it.current) {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
const viewportOffset = it.current.getBoundingClientRect();
|
||||||
|
return [viewportOffset.left, viewportOffset.right] as [number, number];
|
||||||
|
});
|
||||||
|
|
||||||
|
logger('placeholderPositions: %o', placeholderPositions);
|
||||||
|
|
||||||
|
// set init position
|
||||||
|
const initPos = placeholderPositions.findIndex(
|
||||||
|
([l, r]) => l <= ev.pageX && r >= ev.pageX,
|
||||||
|
);
|
||||||
|
let prevMatch = 0;
|
||||||
|
|
||||||
|
if (handleMouseMoveRef.current) {
|
||||||
|
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
|
||||||
|
}
|
||||||
|
handleMouseMoveRef.current = (ev) => {
|
||||||
|
if (!isDragRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
currentXRef.current = ev.pageX;
|
||||||
|
setBoxTranslateX(currentXRef.current - startXRef.current);
|
||||||
|
const match = placeholderPositions.findIndex(
|
||||||
|
([l, r]) => l <= currentXRef.current && r >= currentXRef.current,
|
||||||
|
);
|
||||||
|
if (match === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (match === prevMatch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prevMatch = match;
|
||||||
|
|
||||||
|
const diff = match - initPos;
|
||||||
|
const newValue: LedStripConfig = {
|
||||||
|
...config,
|
||||||
|
global_start_position: config.global_start_position + diff,
|
||||||
|
global_end_position: config.global_end_position + diff,
|
||||||
|
};
|
||||||
|
configRef.current = newValue;
|
||||||
|
setAvailableConfig(newValue);
|
||||||
|
logger('change config. old: %o, new: %o', config, newValue);
|
||||||
|
onConfigChange?.(newValue);
|
||||||
|
};
|
||||||
|
document.body.addEventListener('mousemove', handleMouseMoveRef.current);
|
||||||
|
},
|
||||||
|
[placeholderRefs, availableConfig, setAvailableConfig, config],
|
||||||
|
);
|
||||||
|
|
||||||
|
// move event.
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMouseUp = (ev: MouseEvent) => {
|
||||||
|
if (configRef.current && isDragRef.current) {
|
||||||
|
onConfigFinish?.(configRef.current);
|
||||||
|
}
|
||||||
|
startXRef.current = 0;
|
||||||
|
isDragRef.current = false;
|
||||||
|
document.exitPointerLock();
|
||||||
|
if (handleMouseMoveRef.current) {
|
||||||
|
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.body.addEventListener('mouseup', handleMouseUp);
|
||||||
|
return () => {
|
||||||
|
document.body.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
};
|
||||||
|
}, [onConfigFinish]);
|
||||||
|
// reset translateX when config updated.
|
||||||
|
useEffect(() => {
|
||||||
|
startXRef.current = currentXRef.current;
|
||||||
|
setAvailableConfig(config);
|
||||||
|
setBoxTranslateX(0);
|
||||||
|
logger('useEffect, config: %o', config);
|
||||||
|
}, [config]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
{placeholders}
|
||||||
|
{ledItems}
|
||||||
|
<div
|
||||||
|
tw="border border-gray-700 h-3 w-full rounded-full"
|
||||||
|
css={css`
|
||||||
|
grid-column: ${Math.min(
|
||||||
|
config?.global_start_position ?? 0,
|
||||||
|
config?.global_end_position ?? 0,
|
||||||
|
) + 1} / span
|
||||||
|
${Math.abs(config?.global_start_position - config?.global_end_position) + 1};
|
||||||
|
grid-row-start: ${index + 1};
|
||||||
|
cursor: ew-resize;
|
||||||
|
transform: translateX(${boxTranslateX}px);
|
||||||
|
`}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
></div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
};
|
104
src/configurator/components/led-strip-editor.tsx
Normal file
104
src/configurator/components/led-strip-editor.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
28
src/configurator/components/led-strip.tsx
Normal file
28
src/configurator/components/led-strip.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { HTMLAttributes, useMemo } from 'react';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { LedStripConfig } from '../models/led-strip-config';
|
||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
import { splitEvery } from 'ramda';
|
||||||
|
import { StyledPixel } from './styled-pixel';
|
||||||
|
|
||||||
|
export interface LedStripProps extends HTMLAttributes<HTMLElement> {
|
||||||
|
config: LedStripConfig | null;
|
||||||
|
colors: Uint8Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledContainer = styled.section(
|
||||||
|
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center -mx-px -mt-px`,
|
||||||
|
css``,
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LedStrip: FC<LedStripProps> = ({ config, colors, ...htmlAttrs }) => {
|
||||||
|
const pixels = useMemo(() => {
|
||||||
|
const pixels = splitEvery(3, Array.from(colors)) as Array<[number, number, number]>;
|
||||||
|
return pixels.map((rgb, index) => <StyledPixel key={index} rgb={rgb}></StyledPixel>);
|
||||||
|
}, [colors]);
|
||||||
|
return (
|
||||||
|
<StyledContainer {...htmlAttrs} css={[!config && tw`bg-gray-200`]}>
|
||||||
|
{pixels}
|
||||||
|
</StyledContainer>
|
||||||
|
);
|
||||||
|
};
|
10
src/configurator/components/styled-pixel.tsx
Normal file
10
src/configurator/components/styled-pixel.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import tw, { css, styled } from 'twin.macro';
|
||||||
|
|
||||||
|
export const StyledPixel = styled.span(
|
||||||
|
({ rgb: [r, g, b] }: { rgb: [number, number, number] }) => [
|
||||||
|
tw`rounded-full h-3 w-3 bg-current block border border-gray-700`,
|
||||||
|
css`
|
||||||
|
color: rgb(${r}, ${g}, ${b});
|
||||||
|
`,
|
||||||
|
],
|
||||||
|
);
|
146
src/configurator/configurator.tsx
Normal file
146
src/configurator/configurator.tsx
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
25
src/configurator/contents/led-count.tsx
Normal file
25
src/configurator/contents/led-count.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
Dispatch,
|
||||||
|
FC,
|
||||||
|
ReactNode,
|
||||||
|
SetStateAction,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
interface LedCountContext {
|
||||||
|
ledCount: number;
|
||||||
|
setLedCount: Dispatch<SetStateAction<number>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Context = createContext<LedCountContext>(undefined as any);
|
||||||
|
|
||||||
|
export const LedCountProvider: FC<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [ledCount, setLedCount] = useState(0);
|
||||||
|
return (
|
||||||
|
<Context.Provider value={{ ledCount, setLedCount }}>{children}</Context.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useLedCount = () => useContext(Context);
|
21
src/configurator/models/display-config.ts
Normal file
21
src/configurator/models/display-config.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Borders } from '../../constants/border';
|
||||||
|
import { LedStripConfig } from "./led-strip-config";
|
||||||
|
|
||||||
|
export class LedStripConfigOfBorders implements Record<Borders, LedStripConfig | null> {
|
||||||
|
constructor(
|
||||||
|
public top: LedStripConfig | null = null,
|
||||||
|
public bottom: LedStripConfig | null = null,
|
||||||
|
public left: LedStripConfig | null = null,
|
||||||
|
public right: LedStripConfig | null = null,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
export class DisplayConfig {
|
||||||
|
led_strip_of_borders = new LedStripConfigOfBorders();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public id: number,
|
||||||
|
public index_of_display: number,
|
||||||
|
public display_width: number,
|
||||||
|
public display_height: number,
|
||||||
|
) {}
|
||||||
|
}
|
7
src/configurator/models/led-strip-config.ts
Normal file
7
src/configurator/models/led-strip-config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export class LedStripConfig {
|
||||||
|
constructor(
|
||||||
|
public index: number,
|
||||||
|
public global_start_position: number,
|
||||||
|
public global_end_position: number,
|
||||||
|
) {}
|
||||||
|
}
|
8
src/configurator/models/picker-configuration.ts
Normal file
8
src/configurator/models/picker-configuration.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { DisplayConfig } from './display-config';
|
||||||
|
|
||||||
|
export class PickerConfiguration {
|
||||||
|
constructor(
|
||||||
|
public display_configs: DisplayConfig[] = [],
|
||||||
|
public config_version: number = 1,
|
||||||
|
) {}
|
||||||
|
}
|
12
src/configurator/models/screenshot.dto.ts
Normal file
12
src/configurator/models/screenshot.dto.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { DisplayConfig } from './display-config';
|
||||||
|
|
||||||
|
export class ScreenshotDto {
|
||||||
|
encode_image!: string;
|
||||||
|
config!: DisplayConfig;
|
||||||
|
colors!: {
|
||||||
|
top: Uint8Array;
|
||||||
|
bottom: Uint8Array;
|
||||||
|
left: Uint8Array;
|
||||||
|
right: Uint8Array;
|
||||||
|
};
|
||||||
|
}
|
@ -1,2 +1,2 @@
|
|||||||
export const borders = ['Top', 'Right', 'Bottom', 'Left'] as const;
|
export const borders = ['top', 'right', 'bottom', 'left'] as const;
|
||||||
export type Borders = typeof borders[number];
|
export type Borders = typeof borders[number];
|
@ -1,24 +0,0 @@
|
|||||||
import { createContext } from 'solid-js';
|
|
||||||
import { Borders } from '../constants/border';
|
|
||||||
|
|
||||||
export type LedStripConfigurationContextType = [
|
|
||||||
{
|
|
||||||
selectedStripPart: {
|
|
||||||
displayId: number;
|
|
||||||
border: Borders;
|
|
||||||
} | null;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
setSelectedStripPart: (v: { displayId: number; border: Borders } | null) => void;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const LedStripConfigurationContext =
|
|
||||||
createContext<LedStripConfigurationContextType>([
|
|
||||||
{
|
|
||||||
selectedStripPart: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
setSelectedStripPart: () => {},
|
|
||||||
},
|
|
||||||
]);
|
|
@ -1,15 +0,0 @@
|
|||||||
/* @refresh reload */
|
|
||||||
import { render } from "solid-js/web";
|
|
||||||
|
|
||||||
import "./styles.css";
|
|
||||||
import App from "./App";
|
|
||||||
import { Router } from '@solidjs/router';
|
|
||||||
|
|
||||||
render(
|
|
||||||
() => (
|
|
||||||
<Router>
|
|
||||||
<App />
|
|
||||||
</Router>
|
|
||||||
),
|
|
||||||
document.getElementById('root') as HTMLElement,
|
|
||||||
);
|
|
14
src/main.tsx
Normal file
14
src/main.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
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>,
|
||||||
|
);
|
@ -1,9 +0,0 @@
|
|||||||
export type BoardInfo = {
|
|
||||||
fullname: string;
|
|
||||||
host: string;
|
|
||||||
address: string;
|
|
||||||
port: number;
|
|
||||||
ttl: number;
|
|
||||||
connect_status: 'Connected' | 'Disconnected' | { Connecting: number };
|
|
||||||
checked_at: Date;
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
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,
|
|
||||||
) {}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
export type DisplayState = {
|
|
||||||
brightness: number;
|
|
||||||
max_brightness: number;
|
|
||||||
min_brightness: number;
|
|
||||||
contrast: number;
|
|
||||||
max_contrast: number;
|
|
||||||
min_contrast: number;
|
|
||||||
mode: number;
|
|
||||||
max_mode: number;
|
|
||||||
min_mode: number;
|
|
||||||
last_modified_at: Date;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type RawDisplayState = DisplayState & {
|
|
||||||
last_modified_at: { secs_since_epoch: number };
|
|
||||||
};
|
|
@ -1,27 +0,0 @@
|
|||||||
import { Borders } from '../constants/border';
|
|
||||||
|
|
||||||
export type LedStripPixelMapper = {
|
|
||||||
start: number;
|
|
||||||
end: number;
|
|
||||||
pos: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class ColorCalibration {
|
|
||||||
r: number = 1;
|
|
||||||
g: number = 1;
|
|
||||||
b: number = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type LedStripConfigContainer = {
|
|
||||||
strips: LedStripConfig[];
|
|
||||||
mappers: LedStripPixelMapper[];
|
|
||||||
color_calibration: ColorCalibration;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class LedStripConfig {
|
|
||||||
constructor(
|
|
||||||
public readonly display_id: number,
|
|
||||||
public readonly border: Borders,
|
|
||||||
public len: number,
|
|
||||||
) {}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
import { createStore } from 'solid-js/store';
|
|
||||||
import { DisplayInfo } from '../models/display-info.model';
|
|
||||||
|
|
||||||
export const [displayStore, setDisplayStore] = createStore({
|
|
||||||
displays: new Array<DisplayInfo>(),
|
|
||||||
viewScale: 0.2,
|
|
||||||
});
|
|
@ -1,26 +0,0 @@
|
|||||||
import { createStore } from 'solid-js/store';
|
|
||||||
import {
|
|
||||||
ColorCalibration,
|
|
||||||
LedStripConfig,
|
|
||||||
LedStripPixelMapper,
|
|
||||||
} from '../models/led-strip-config';
|
|
||||||
|
|
||||||
export const [ledStripStore, setLedStripStore] = createStore({
|
|
||||||
strips: new Array<LedStripConfig>(),
|
|
||||||
mappers: new Array<LedStripPixelMapper>(),
|
|
||||||
colorCalibration: new ColorCalibration(),
|
|
||||||
colors: new Uint8ClampedArray(),
|
|
||||||
sortedColors: new Uint8ClampedArray(),
|
|
||||||
get totalLedCount() {
|
|
||||||
return Math.max(
|
|
||||||
0,
|
|
||||||
...ledStripStore.mappers.map((m) => {
|
|
||||||
if (m.start === m.end) {
|
|
||||||
return 0;
|
|
||||||
} else {
|
|
||||||
return Math.max(m.start, m.end);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
106
src/style.css
Normal file
106
src/style.css
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
3
src/styles/fill-parent.ts
Normal file
3
src/styles/fill-parent.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import tw from 'twin.macro';
|
||||||
|
|
||||||
|
export const fillParentCss = tw`w-full h-full overflow-hidden`;
|
35
src/styles/global-styles.tsx
Normal file
35
src/styles/global-styles.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user