feat: 基本支持拖拽配置灯条。

This commit is contained in:
Ivan Li 2023-01-16 00:56:19 +08:00
parent a3e2b5a234
commit 6802dbb7c0
5 changed files with 286 additions and 26 deletions

View File

@ -2,12 +2,13 @@ import { useCallback, useEffect, useState } from 'react';
import tw from 'twin.macro'; import tw from 'twin.macro';
import { invoke } from '@tauri-apps/api/tauri'; import { invoke } from '@tauri-apps/api/tauri';
import './App.css'; import './App.css';
import clsx from 'clsx';
import { Configurator } from './configurator/configurator'; import { Configurator } from './configurator/configurator';
import { ButtonSwitch } from './commons/components/button'; import { ButtonSwitch } from './commons/components/button';
type Mode = 'Flowing' | 'Follow' | null; type Mode = 'Flowing' | 'Follow' | null;
localStorage.setItem('debug', '*');
function App() { function App() {
const [screenshots, setScreenshots] = useState<string[]>([]); const [screenshots, setScreenshots] = useState<string[]>([]);
const [ledStripColors, setLedStripColors] = useState<string[]>([]); const [ledStripColors, setLedStripColors] = useState<string[]>([]);

View File

@ -1,26 +1,43 @@
import { isNil, splitEvery } from 'ramda'; import debug from 'debug';
import { FC, useMemo } from 'react'; 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 tw, { css, styled } from 'twin.macro';
import { borders } from '../../constants/border'; import { Borders, borders } from '../../constants/border';
import { DisplayConfig } from '../models/display-config'; import { useLedCount } from '../contents/led-count';
import { DisplayConfig, LedStripConfigOfBorders } from '../models/display-config';
import { LedStripConfig } from '../models/led-strip-config'; import { LedStripConfig } from '../models/led-strip-config';
import { PixelRgb } from '../models/pixel-rgb';
import { ScreenshotDto } from '../models/screenshot.dto'; import { ScreenshotDto } from '../models/screenshot.dto';
import { LedStrip } from './led-strip';
import { StyledPixel } from './styled-pixel'; import { StyledPixel } from './styled-pixel';
const logger = debug('app:completed-led-strip');
interface CompletedLedStripProps { interface CompletedLedStripProps {
screenshots: ScreenshotDto[]; screenshots: ScreenshotDto[];
onDisplayConfigChange?: (value: DisplayConfig) => void; onDisplayConfigChange?: (value: DisplayConfig) => void;
} }
type BorderLedStrip = { type BorderLedStrip = {
pixels: [number, number, number][]; pixels: PixelRgb[];
config: LedStripConfig | null; config: LedStripConfig | null;
}; };
const StyledContainer = styled.section( const StyledContainer = styled.section(
({ rows, columns }: { rows: number; columns: number }) => [ ({ 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` css`
grid-template-columns: repeat(${columns}, 1fr); grid-template-columns: repeat(${columns}, 1fr);
grid-template-rows: auto repeat(${rows}, 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`, tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center mb-2`,
css` css`
grid-column: 1 / -1; grid-column: 1 / -1;
justify-self: stretch;
`, `,
); );
@ -41,7 +59,7 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
const borderLedStrips: BorderLedStrip[] = useMemo(() => { const borderLedStrips: BorderLedStrip[] = useMemo(() => {
return screenshots.flatMap((ss) => return screenshots.flatMap((ss) =>
borders.map((b) => ({ 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], config: ss.config.led_strip_of_borders[b],
})), })),
); );
@ -51,9 +69,18 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
[borderLedStrips], [borderLedStrips],
); );
const { setLedCount } = useLedCount();
// setLedCount for context
useEffect(() => {
setLedCount(ledCount);
}, [ledCount, setLedCount]);
const [overrideBorderLedStrips, setOverrideBorderLedStrips] =
useState<BorderLedStrip[]>();
const completedPixels = useMemo(() => { const completedPixels = useMemo(() => {
const completed: [number, number, number][] = new Array(ledCount).fill([0, 0, 0]); const completed: PixelRgb[] = new Array(ledCount).fill([0, 0, 0]);
borderLedStrips.forEach(({ pixels, config }) => { (overrideBorderLedStrips ?? borderLedStrips).forEach(({ pixels, config }) => {
if (isNil(config)) { if (isNil(config)) {
return; return;
} }
@ -68,21 +95,45 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
} }
}); });
return completed.map((color) => <StyledPixel rgb={color} />); return completed.map((color, i) => <StyledPixel rgb={color} key={i} />);
}, [ledCount, borderLedStrips]); }, [ledCount, borderLedStrips, overrideBorderLedStrips]);
const strips = useMemo(() => { const strips = useMemo(() => {
return borderLedStrips.map(({ config, pixels }, index) => ( return (overrideBorderLedStrips ?? borderLedStrips).map(({ config, pixels }, index) =>
<LedStrip config ? (
<DraggableStrip
key={index} key={index}
colors={Uint8Array.from(pixels.flat())} {...{ config, pixels, index: index + 1 }}
config={config} onConfigChange={(c) => {
css={css` setOverrideBorderLedStrips(
grid-column-start: ${(config?.global_start_position ?? 0) + 1}; update(index, { config: c, pixels }, borderLedStrips),
grid-column-end: ${(config?.global_end_position ?? 0) + 1}; );
`} }}
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]); }, [borderLedStrips]);
return ( return (
<StyledContainer rows={screenshots.length * borders.length} columns={ledCount}> <StyledContainer rows={screenshots.length * borders.length} columns={ledCount}>
@ -91,3 +142,173 @@ export const CompletedLedStrip: FC<CompletedLedStripProps> = ({
</StyledContainer> </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>
);
};

View File

@ -11,6 +11,7 @@ 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'; import { CompletedLedStrip } from './components/completed-led-strip';
import { LedCountProvider } from './contents/led-count';
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config'); const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
const getScreenshotOfDisplays = () => const getScreenshotOfDisplays = () =>
@ -31,7 +32,7 @@ const StyledConfiguratorContainer = styled.section(tw`flex flex-col items-stretc
const StyledDisplayContainer = styled.section(tw`overflow-auto`); const StyledDisplayContainer = styled.section(tw`overflow-auto`);
export const Configurator: FC = () => { const ConfiguratorInner: FC = () => {
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync( const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
getPickerConfig, getPickerConfig,
[], [],
@ -96,7 +97,10 @@ export const Configurator: FC = () => {
return ( return (
<StyledConfiguratorContainer> <StyledConfiguratorContainer>
<CompletedLedStrip screenshots={screenshotOfDisplays} /> <CompletedLedStrip
screenshots={screenshotOfDisplays}
onDisplayConfigChange={onDisplayConfigChange}
/>
<StyledDisplayContainer>{displays}</StyledDisplayContainer>; <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%' }}>
@ -106,3 +110,11 @@ export const Configurator: FC = () => {
</StyledConfiguratorContainer> </StyledConfiguratorContainer>
); );
}; };
export const Configurator = () => {
return (
<LedCountProvider>
<ConfiguratorInner />
</LedCountProvider>
);
};

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

View File

@ -0,0 +1 @@
export type PixelRgb = [number, number, number];