feature/gui-configuration:支持从 GUI 配置程序。 #4
@ -2,12 +2,13 @@ import { useCallback, useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { invoke } from '@tauri-apps/api/tauri';
|
||||
import './App.css';
|
||||
import clsx from 'clsx';
|
||||
import { Configurator } from './configurator/configurator';
|
||||
import { ButtonSwitch } from './commons/components/button';
|
||||
|
||||
type Mode = 'Flowing' | 'Follow' | null;
|
||||
|
||||
localStorage.setItem('debug', '*');
|
||||
|
||||
function App() {
|
||||
const [screenshots, setScreenshots] = useState<string[]>([]);
|
||||
const [ledStripColors, setLedStripColors] = useState<string[]>([]);
|
||||
|
@ -1,26 +1,43 @@
|
||||
import { isNil, splitEvery } from 'ramda';
|
||||
import { FC, useMemo } from 'react';
|
||||
import debug from 'debug';
|
||||
import { isNil, lensPath, set, splitEvery, update } from 'ramda';
|
||||
import {
|
||||
createRef,
|
||||
FC,
|
||||
Fragment,
|
||||
MouseEventHandler,
|
||||
ReactEventHandler,
|
||||
ReactNode,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import tw, { css, styled } from 'twin.macro';
|
||||
import { borders } from '../../constants/border';
|
||||
import { DisplayConfig } from '../models/display-config';
|
||||
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 { LedStrip } from './led-strip';
|
||||
import { StyledPixel } from './styled-pixel';
|
||||
|
||||
const logger = debug('app:completed-led-strip');
|
||||
|
||||
interface CompletedLedStripProps {
|
||||
screenshots: ScreenshotDto[];
|
||||
onDisplayConfigChange?: (value: DisplayConfig) => void;
|
||||
}
|
||||
|
||||
type BorderLedStrip = {
|
||||
pixels: [number, number, number][];
|
||||
pixels: PixelRgb[];
|
||||
config: LedStripConfig | null;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.section(
|
||||
({ rows, columns }: { rows: number; columns: number }) => [
|
||||
tw`grid m-4 pb-2`,
|
||||
tw`grid m-4 pb-2 items-center justify-items-center select-none`,
|
||||
css`
|
||||
grid-template-columns: repeat(${columns}, 1fr);
|
||||
grid-template-rows: auto repeat(${rows}, 1fr);
|
||||
@ -31,6 +48,7 @@ const StyledCompletedContainer = styled.section(
|
||||
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center mb-2`,
|
||||
css`
|
||||
grid-column: 1 / -1;
|
||||
justify-self: stretch;
|
||||
`,
|
||||
);
|
||||
|
||||
@ -41,7 +59,7 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
const borderLedStrips: BorderLedStrip[] = useMemo(() => {
|
||||
return screenshots.flatMap((ss) =>
|
||||
borders.map((b) => ({
|
||||
pixels: splitEvery(3, Array.from(ss.colors[b])) as [number, number, number][],
|
||||
pixels: splitEvery(3, Array.from(ss.colors[b])) as PixelRgb[],
|
||||
config: ss.config.led_strip_of_borders[b],
|
||||
})),
|
||||
);
|
||||
@ -51,9 +69,18 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
[borderLedStrips],
|
||||
);
|
||||
|
||||
const { setLedCount } = useLedCount();
|
||||
// setLedCount for context
|
||||
useEffect(() => {
|
||||
setLedCount(ledCount);
|
||||
}, [ledCount, setLedCount]);
|
||||
|
||||
const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
|
||||
useState<BorderLedStrip[]>();
|
||||
|
||||
const completedPixels = useMemo(() => {
|
||||
const completed: [number, number, number][] = new Array(ledCount).fill([0, 0, 0]);
|
||||
borderLedStrips.forEach(({ pixels, config }) => {
|
||||
const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]);
|
||||
(overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => {
|
||||
if (isNil(config)) {
|
||||
return;
|
||||
}
|
||||
@ -68,21 +95,45 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
return completed.map((color) => <StyledPixel rgb={color} />);
|
||||
}, [ledCount, borderLedStrips]);
|
||||
return completed.map((color, i) => <StyledPixel rgb={color} key={i} />);
|
||||
}, [ledCount, borderLedStrips, overrideBorderLedStrips]);
|
||||
|
||||
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};
|
||||
`}
|
||||
/>
|
||||
));
|
||||
return (overrideBorderLedStrips ?? 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.round(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,
|
||||
),
|
||||
};
|
||||
logger('Change DisplayConfig. %o', displayConfig);
|
||||
onDisplayConfigChange?.(displayConfig);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div key={index} />
|
||||
),
|
||||
);
|
||||
}, [borderLedStrips, overrideBorderLedStrips]);
|
||||
|
||||
useEffect(() => {
|
||||
setOverrideBorderLedStrips(undefined);
|
||||
}, [borderLedStrips]);
|
||||
return (
|
||||
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}>
|
||||
@ -91,3 +142,173 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
interface DraggableStripProp {
|
||||
config: LedStripConfig;
|
||||
pixels: PixelRgb[];
|
||||
index: number;
|
||||
onConfigChange?: (config: LedStripConfig) => void;
|
||||
onConfigFinish?: (config: LedStripConfig) => void;
|
||||
}
|
||||
|
||||
const DraggableStrip: FC<DraggableStripProp> = ({
|
||||
config,
|
||||
pixels,
|
||||
index,
|
||||
onConfigChange,
|
||||
onConfigFinish,
|
||||
}) => {
|
||||
const ledItems = pixels.map((rgb, i) => (
|
||||
<StyledPixel
|
||||
key={i}
|
||||
rgb={rgb}
|
||||
css={css`
|
||||
grid-column: ${(config?.global_start_position ?? 0) + i + 1} / span 1;
|
||||
grid-row-start: ${index + 1};
|
||||
pointer-events: none;
|
||||
`}
|
||||
/>
|
||||
));
|
||||
|
||||
const { ledCount } = useLedCount();
|
||||
|
||||
const startXRef = useRef(0);
|
||||
const currentXRef = useRef(0);
|
||||
const configRef = useRef<LedStripConfig>();
|
||||
// const currentDiffRef = useRef(0);
|
||||
const [currentDiff, setCurrentDiff] = useState(0);
|
||||
const isDragRef = useRef(false);
|
||||
const handleMouseMoveRef = useRef<(ev: MouseEvent) => void>();
|
||||
const [boxTranslateX, setBoxTranslateX] = useState(0);
|
||||
|
||||
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="opacity-30 bg-red-500 h-full w-full border border-yellow-400"
|
||||
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],
|
||||
);
|
||||
|
||||
const handleMouseDown: MouseEventHandler<HTMLDivElement> = useCallback(
|
||||
(ev) => {
|
||||
startXRef.current = ev.pageX;
|
||||
ev.currentTarget.requestPointerLock();
|
||||
isDragRef.current = true;
|
||||
|
||||
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,
|
||||
);
|
||||
setCurrentDiff(initPos);
|
||||
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;
|
||||
logger('change config. new: $o', newValue);
|
||||
onConfigChange?.(newValue);
|
||||
};
|
||||
document.body.addEventListener('mousemove', handleMouseMoveRef.current);
|
||||
},
|
||||
[placeholderRefs],
|
||||
);
|
||||
|
||||
// move event.
|
||||
useEffect(() => {
|
||||
const handleMouseUp = (ev: MouseEvent) => {
|
||||
startXRef.current = 0;
|
||||
isDragRef.current = false;
|
||||
if (configRef.current) {
|
||||
onConfigFinish?.(configRef.current);
|
||||
}
|
||||
document.exitPointerLock();
|
||||
if (handleMouseMoveRef.current) {
|
||||
document.body.removeEventListener('mousemove', handleMouseMoveRef.current);
|
||||
}
|
||||
};
|
||||
document.body.addEventListener('mouseup', handleMouseUp);
|
||||
return () => {
|
||||
document.body.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, []);
|
||||
// reset translateX when config updated.
|
||||
useEffect(() => {
|
||||
startXRef.current = currentXRef.current;
|
||||
setCurrentDiff(0);
|
||||
}, [config]);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
{placeholders}
|
||||
{ledItems}
|
||||
<div
|
||||
tw="border border-gray-700 h-3 w-full rounded-full"
|
||||
css={css`
|
||||
grid-column-start: ${(config?.global_start_position ?? 0) + 1 - currentDiff};
|
||||
grid-column-end: ${(config?.global_end_position ?? 0) + 1 - currentDiff};
|
||||
grid-row-start: ${index + 1};
|
||||
cursor: ew-resize;
|
||||
transform: translateX(${boxTranslateX}px);
|
||||
`}
|
||||
onMouseDown={handleMouseDown}
|
||||
></div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { update } from 'ramda';
|
||||
import { CompletedLedStrip } from './components/completed-led-strip';
|
||||
import { LedCountProvider } from './contents/led-count';
|
||||
|
||||
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
|
||||
const getScreenshotOfDisplays = () =>
|
||||
@ -31,7 +32,7 @@ const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretc
|
||||
|
||||
const StyledDisplayContainer = styled.section(tw`overflow-auto`);
|
||||
|
||||
export const Configurator: FC = () => {
|
||||
const ConfiguratorInner: FC = () => {
|
||||
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
||||
getPickerConfig,
|
||||
[],
|
||||
@ -96,7 +97,10 @@ export const Configurator: FC = () => {
|
||||
|
||||
return (
|
||||
<StyledConfiguratorContainer>
|
||||
<CompletedLedStrip screenshots={screenshotOfDisplays} />
|
||||
<CompletedLedStrip
|
||||
screenshots={screenshotOfDisplays}
|
||||
onDisplayConfigChange={onDisplayConfigChange}
|
||||
/>
|
||||
<StyledDisplayContainer>{displays}</StyledDisplayContainer>;
|
||||
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
|
||||
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
||||
@ -106,3 +110,11 @@ export const Configurator: FC = () => {
|
||||
</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);
|
1
src/configurator/models/pixel-rgb.ts
Normal file
1
src/configurator/models/pixel-rgb.ts
Normal file
@ -0,0 +1 @@
|
||||
export type PixelRgb = [number, number, number];
|
Loading…
Reference in New Issue
Block a user