feat: 配置结构调整和初步灯条配置界面。

This commit is contained in:
Ivan Li 2023-01-15 18:38:25 +08:00
parent 56f65fed10
commit 5384a30872
11 changed files with 533 additions and 214 deletions

View File

@ -16,9 +16,10 @@
"@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@mui/material": "^5.11.2", "@mui/material": "^5.11.4",
"@tauri-apps/api": "^1.2.0", "@tauri-apps/api": "^1.2.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"debug": "^4.3.4",
"ramda": "^0.28.0", "ramda": "^0.28.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-async-hook": "^4.0.0", "react-async-hook": "^4.0.0",
@ -29,6 +30,7 @@
"@emotion/babel-plugin-jsx-pragmatic": "^0.2.0", "@emotion/babel-plugin-jsx-pragmatic": "^0.2.0",
"@emotion/serialize": "^1.1.1", "@emotion/serialize": "^1.1.1",
"@tauri-apps/cli": "^1.2.2", "@tauri-apps/cli": "^1.2.2",
"@types/debug": "^4.1.7",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/ramda": "^0.28.20", "@types/ramda": "^0.28.20",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
@ -36,13 +38,13 @@
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^2.2.0",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.26.0", "eslint-plugin-import": "^2.27.4",
"eslint-plugin-jsx-a11y": "^6.6.1", "eslint-plugin-jsx-a11y": "^6.7.1",
"eslint-plugin-prettier": "^4.2.1", "eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-simple-import-sort": "^8.0.0", "eslint-plugin-simple-import-sort": "^8.0.0",
"postcss": "^8.4.20", "postcss": "^8.4.21",
"prettier": "^2.8.1", "prettier": "^2.8.3",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"twin.macro": "^3.1.0", "twin.macro": "^3.1.0",
"typescript": "^4.9.4", "typescript": "^4.9.4",

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,13 @@
use serde::{Deserialize, Serialize}; 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)] #[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct LedStripConfig { pub struct LedStripConfig {
pub index: usize, pub index: usize,
@ -13,23 +21,33 @@ pub struct DisplayConfig {
pub index_of_display: usize, pub index_of_display: usize,
pub display_width: usize, pub display_width: usize,
pub display_height: usize, pub display_height: usize,
pub top_led_strip: Option<LedStripConfig>, pub led_strip_of_borders: LedStripConfigOfBorders,
pub bottom_led_strip: Option<LedStripConfig>, }
pub left_led_strip: Option<LedStripConfig>,
pub right_led_strip: Option<LedStripConfig>, impl LedStripConfigOfBorders {
pub fn default() -> Self {
Self {
top: None,
bottom: None,
left: None,
right: None,
}
}
} }
impl DisplayConfig { impl DisplayConfig {
pub fn default(id: usize, index_of_display: usize, display_width: usize, display_height: usize) -> Self { pub fn default(
id: usize,
index_of_display: usize,
display_width: usize,
display_height: usize,
) -> Self {
Self { Self {
id, id,
index_of_display, index_of_display,
display_width, display_width,
display_height, display_height,
top_led_strip: None, led_strip_of_borders: LedStripConfigOfBorders::default(),
bottom_led_strip: None,
left_led_strip: None,
right_led_strip: None,
} }
} }
} }

View File

@ -34,7 +34,7 @@ impl Screenshot {
} }
fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints { fn get_sample_points(config: DisplayConfig) -> ScreenSamplePoints {
let top = match config.top_led_strip { let top = match config.led_strip_of_borders.top {
Some(led_strip_config) => Self::get_one_edge_sample_points( Some(led_strip_config) => Self::get_one_edge_sample_points(
config.display_height / 8, config.display_height / 8,
config.display_width, config.display_width,
@ -48,7 +48,7 @@ impl Screenshot {
} }
}; };
let bottom: Vec<LedSamplePoints> = match config.bottom_led_strip { let bottom: Vec<LedSamplePoints> = match config.led_strip_of_borders.bottom {
Some(led_strip_config) => { Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points( let points = Self::get_one_edge_sample_points(
config.display_height / 9, config.display_height / 9,
@ -73,7 +73,7 @@ impl Screenshot {
} }
}; };
let left: Vec<LedSamplePoints> = match config.left_led_strip { let left: Vec<LedSamplePoints> = match config.led_strip_of_borders.left {
Some(led_strip_config) => { Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points( let points = Self::get_one_edge_sample_points(
config.display_width / 16, config.display_width / 16,
@ -95,7 +95,7 @@ impl Screenshot {
} }
}; };
let right: Vec<LedSamplePoints> = match config.right_led_strip { let right: Vec<LedSamplePoints> = match config.led_strip_of_borders.right {
Some(led_strip_config) => { Some(led_strip_config) => {
let points = Self::get_one_edge_sample_points( let points = Self::get_one_edge_sample_points(
config.display_width / 16, config.display_width / 16,
@ -217,13 +217,13 @@ impl Screenshot {
pub fn get_top_of_led_start_at(&self) -> usize { pub fn get_top_of_led_start_at(&self) -> usize {
self.config self.config
.top_led_strip .led_strip_of_borders.top
.and_then(|c| Some(c.global_start_position)) .and_then(|c| Some(c.global_start_position))
.unwrap_or(0) .unwrap_or(0)
} }
pub fn get_top_of_led_end_at(&self) -> usize { pub fn get_top_of_led_end_at(&self) -> usize {
self.config self.config
.top_led_strip .led_strip_of_borders.top
.and_then(|c| Some(c.global_end_position)) .and_then(|c| Some(c.global_end_position))
.unwrap_or(0) .unwrap_or(0)
} }

View File

@ -0,0 +1,93 @@
import { isNil, splitEvery } from 'ramda';
import { FC, useMemo } from 'react';
import tw, { css, styled } from 'twin.macro';
import { borders } from '../../constants/border';
import { DisplayConfig } from '../models/display-config';
import { LedStripConfig } from '../models/led-strip-config';
import { ScreenshotDto } from '../models/screenshot.dto';
import { LedStrip } from './led-strip';
import { StyledPixel } from './styled-pixel';
interface CompletedLedStripProps {
screenshots: ScreenshotDto[];
onDisplayConfigChange?: (value: DisplayConfig) => void;
}
type BorderLedStrip = {
pixels: [number, number, number][];
config: LedStripConfig | null;
};
const StyledContainer = styled.section(
({ rows, columns }: { rows: number; columns: number }) => [
tw`grid m-4 pb-2`,
css`
grid-template-columns: repeat(${columns}, 1fr);
grid-template-rows: auto repeat(${rows}, 1fr);
`,
],
);
const StyledCompletedContainer = styled.section(
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center mb-2`,
css`
grid-column: 1 / -1;
`,
);
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 [number, number, number][],
config: ss.config.led_strip_of_borders[b],
})),
);
}, [screenshots]);
const ledCount = useMemo(
() => borderLedStrips.reduce((prev, curr) => prev + curr.pixels.length, 0),
[borderLedStrips],
);
const completedPixels = useMemo(() => {
const completed: [number, number, number][] = new Array(ledCount).fill([0, 0, 0]);
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) => <StyledPixel rgb={color} />);
}, [ledCount, borderLedStrips]);
const strips = useMemo(() => {
return borderLedStrips.map(({ config, pixels }, index) => (
<LedStrip
key={index}
colors={Uint8Array.from(pixels.flat())}
config={config}
css={css`
grid-column-start: ${(config?.global_start_position ?? 0) + 1};
grid-column-end: ${(config?.global_end_position ?? 0) + 1};
`}
/>
));
}, [borderLedStrips]);
return (
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}>
<StyledCompletedContainer>{completedPixels}</StyledCompletedContainer>
{strips}
</StyledContainer>
);
};

View File

@ -1,11 +1,15 @@
import { HTMLAttributes, useCallback, useMemo } from 'react'; import { HTMLAttributes, useCallback, useMemo } from 'react';
import { FC } from 'react'; import { FC } from 'react';
import { DisplayConfig } from '../models/display-config'; import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
import { LedStrip } from './led-strip'; import { LedStrip } from './led-strip';
import tw, { css, styled, theme } from 'twin.macro'; import tw, { css, styled, theme } from 'twin.macro';
import { ScreenshotDto } from '../models/screenshot.dto'; import { ScreenshotDto } from '../models/screenshot.dto';
import { LedStripEditor } from './led-strip-editor'; import { LedStripEditor } from './led-strip-editor';
import { LedStripConfig } from '../models/led-strip-config'; 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 export interface DisplayWithLedStripsProps
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> { extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
@ -36,61 +40,60 @@ export const DisplayWithLedStrips: FC<DisplayWithLedStripsProps> = ({
); );
const onLedStripConfigChange = useCallback( const onLedStripConfigChange = useCallback(
( (position: keyof LedStripConfigOfBorders, value: LedStripConfig | null) => {
position: const xLens = lensPath<
| 'top_led_strip' DisplayConfig,
| 'left_led_strip' 'led_strip_of_borders',
| 'right_led_strip' keyof LedStripConfigOfBorders
| 'bottom_led_strip', >(['led_strip_of_borders', position]);
value: LedStripConfig | null, const c = set(xLens, value, config);
) => { logger('on change. prev: %o, curr: %o', view(xLens, config), value);
const c = { ...config, [position]: value };
onChange?.(c); onChange?.(c);
}, },
[config], [config],
); );
return ( return (
<StyledContainer {...htmlAttrs}> <StyledContainer {...htmlAttrs}>
<img src={screenshotUrl} tw="row-start-3 col-start-3" /> <img src={screenshotUrl} tw="row-start-3 col-start-3 w-full" />
<LedStrip <LedStrip
config={config.top_led_strip} config={config.led_strip_of_borders.top}
colors={screenshot.colors.top} colors={screenshot.colors.top}
tw="row-start-2 col-start-3" tw="row-start-2 col-start-3"
/> />
<LedStrip <LedStrip
config={config.left_led_strip} config={config.led_strip_of_borders.left}
colors={screenshot.colors.left} colors={screenshot.colors.left}
tw="row-start-3 col-start-2" tw="row-start-3 col-start-2"
/> />
<LedStrip <LedStrip
config={config.right_led_strip} config={config.led_strip_of_borders.right}
colors={screenshot.colors.right} colors={screenshot.colors.right}
tw="row-start-3 col-start-4" tw="row-start-3 col-start-4"
/> />
<LedStrip <LedStrip
config={config.bottom_led_strip} config={config.led_strip_of_borders.bottom}
colors={screenshot.colors.bottom} colors={screenshot.colors.bottom}
tw="row-start-4 col-start-3" tw="row-start-4 col-start-3"
/> />
<LedStripEditor <LedStripEditor
config={config.top_led_strip} config={config.led_strip_of_borders.top}
tw="row-start-1 col-start-3" tw="row-start-1 col-start-3"
onChange={(value) => onLedStripConfigChange('top_led_strip', value)} onChange={(value) => onLedStripConfigChange('top', value)}
/> />
<LedStripEditor <LedStripEditor
config={config.left_led_strip} config={config.led_strip_of_borders.left}
tw="row-start-3 col-start-1" tw="row-start-3 col-start-1"
onChange={(value) => onLedStripConfigChange('left_led_strip', value)} onChange={(value) => onLedStripConfigChange('left', value)}
/> />
<LedStripEditor <LedStripEditor
config={config.right_led_strip} config={config.led_strip_of_borders.right}
tw="row-start-3 col-start-5" tw="row-start-3 col-start-5"
onChange={(value) => onLedStripConfigChange('right_led_strip', value)} onChange={(value) => onLedStripConfigChange('right', value)}
/> />
<LedStripEditor <LedStripEditor
config={config.bottom_led_strip} config={config.led_strip_of_borders.bottom}
tw="row-start-5 col-start-3" tw="row-start-5 col-start-3"
onChange={(value) => onLedStripConfigChange('bottom_led_strip', value)} onChange={(value) => onLedStripConfigChange('bottom', value)}
/> />
</StyledContainer> </StyledContainer>
); );

View File

@ -3,6 +3,7 @@ import { FC } from 'react';
import { LedStripConfig } from '../models/led-strip-config'; import { LedStripConfig } from '../models/led-strip-config';
import tw, { css, styled } from 'twin.macro'; import tw, { css, styled } from 'twin.macro';
import { splitEvery } from 'ramda'; import { splitEvery } from 'ramda';
import { StyledPixel } from './styled-pixel';
export interface LedStripProps extends HTMLAttributes<HTMLElement> { export interface LedStripProps extends HTMLAttributes<HTMLElement> {
config: LedStripConfig | null; config: LedStripConfig | null;
@ -10,19 +11,10 @@ export interface LedStripProps extends HTMLAttributes<HTMLElement> {
} }
const StyledContainer = styled.section( const StyledContainer = styled.section(
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center`, tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center -mx-px -mt-px`,
css``, css``,
); );
const StyledPixel = styled.span(
({ rgb: [r, g, b] }: { rgb: [number, number, number] }) => [
tw`rounded-full h-3 w-3 bg-current block border border-gray-700`,
css`
color: rgb(${r}, ${g}, ${b});
`,
],
);
export const LedStrip: FC<LedStripProps> = ({ config, colors, ...htmlAttrs }) => { export const LedStrip: FC<LedStripProps> = ({ config, colors, ...htmlAttrs }) => {
const pixels = useMemo(() => { const pixels = useMemo(() => {
const pixels = splitEvery(3, Array.from(colors)) as Array<[number, number, number]>; const pixels = splitEvery(3, Array.from(colors)) as Array<[number, number, number]>;

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

View File

@ -1,6 +1,6 @@
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react';
import tw from 'twin.macro'; import tw, { styled } from 'twin.macro';
import { useAsync, useAsyncCallback } from 'react-async-hook'; import { useAsync, useAsyncCallback } from 'react-async-hook';
import { DisplayWithLedStrips } from './components/display-with-led-strips'; import { DisplayWithLedStrips } from './components/display-with-led-strips';
import { PickerConfiguration } from './models/picker-configuration'; import { PickerConfiguration } from './models/picker-configuration';
@ -10,6 +10,7 @@ import { Alert, Snackbar } from '@mui/material';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { faSpinner } from '@fortawesome/free-solid-svg-icons';
import { update } from 'ramda'; import { update } from 'ramda';
import { CompletedLedStrip } from './components/completed-led-strip';
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config'); const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
const getScreenshotOfDisplays = () => const getScreenshotOfDisplays = () =>
@ -26,6 +27,9 @@ const writePickerConfig = async (config: PickerConfiguration) => {
config, config,
}); });
}; };
const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretch`);
const StyledDisplayContainer = styled.section(tw`overflow-auto`);
export const Configurator: FC = () => { export const Configurator: FC = () => {
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync( const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
@ -91,13 +95,14 @@ export const Configurator: FC = () => {
} }
return ( return (
<Fragment> <StyledConfiguratorContainer>
<section>{displays}</section>; <CompletedLedStrip screenshots={screenshotOfDisplays} />
<StyledDisplayContainer>{displays}</StyledDisplayContainer>;
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}> <Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}> <Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
This is a success message! This is a success message!
</Alert> </Alert>
</Snackbar> </Snackbar>
</Fragment> </StyledConfiguratorContainer>
); );
}; };

View File

@ -1,10 +1,16 @@
import { LedStripConfig } from './led-strip-config'; 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 { export class DisplayConfig {
top_led_strip: LedStripConfig | null = null; led_strip_of_borders = new LedStripConfigOfBorders();
bottom_led_strip: LedStripConfig | null = null;
left_led_strip: LedStripConfig | null = null;
right_led_strip: LedStripConfig | null = null;
constructor( constructor(
public id: number, public id: number,

2
src/constants/border.ts Normal file
View File

@ -0,0 +1,2 @@
export const borders = ['top', 'right', 'bottom', 'left'] as const;
export type Borders = typeof borders[number];