feat: GUI 配置支持添加和减少 LED。
This commit is contained in:
58
src/App.tsx
58
src/App.tsx
@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import reactLogo from './assets/react.svg';
|
||||
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;
|
||||
|
||||
@ -39,63 +40,30 @@ function App() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between">
|
||||
<div tw="flex justify-between">
|
||||
{ledStripColors.map((it) => (
|
||||
<span className=" h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
||||
<span tw="h-8 flex-auto" style={{ backgroundColor: it }}></span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 justify-center w-screen overflow-hidden">
|
||||
<div tw="flex gap-1 justify-center w-screen overflow-hidden">
|
||||
{screenshots.map((screenshot) => (
|
||||
<div className="flex-auto">
|
||||
<div tw="flex-auto">
|
||||
<img src={screenshot} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-5 justify-center ">
|
||||
<button
|
||||
className="bg-black bg-opacity-20"
|
||||
type="button"
|
||||
onClick={() => readPickerConfig()}
|
||||
>
|
||||
Refresh Displays
|
||||
</button>
|
||||
<button
|
||||
className="bg-black bg-opacity-20"
|
||||
type="button"
|
||||
onClick={() => takeSnapshot()}
|
||||
>
|
||||
Take Snapshot
|
||||
</button>
|
||||
<button
|
||||
className="bg-black bg-opacity-20"
|
||||
type="button"
|
||||
onClick={() => getLedStripColors()}
|
||||
>
|
||||
Get Colors
|
||||
</button>
|
||||
<button
|
||||
className={clsx('bg-black', 'bg-opacity-20', {
|
||||
'bg-gradient-to-r from-purple-500 to-blue-500': currentMode === 'Flowing',
|
||||
})}
|
||||
type="button"
|
||||
onClick={() => switchCurrentMode('Flowing')}
|
||||
>
|
||||
<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
|
||||
</button>
|
||||
<button
|
||||
className={clsx('bg-black', 'bg-opacity-20', {
|
||||
'bg-gradient-to-r from-purple-500 to-blue-500': currentMode === 'Follow',
|
||||
})}
|
||||
type="button"
|
||||
onClick={() => switchCurrentMode('Follow')}
|
||||
>
|
||||
Follow
|
||||
</button>
|
||||
</ButtonSwitch>
|
||||
<ButtonSwitch onClick={() => switchCurrentMode('Follow')}>Follow</ButtonSwitch>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-5 justify-center">
|
||||
<div tw="flex gap-5 justify-center">
|
||||
<Configurator />
|
||||
</div>
|
||||
</div>
|
||||
|
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,25 +1,97 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { HTMLAttributes, useCallback, useMemo } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { DisplayConfig } 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';
|
||||
|
||||
export interface DisplayWithLedStripsProps extends HTMLAttributes<HTMLElement> {
|
||||
export interface DisplayWithLedStripsProps
|
||||
extends Omit<HTMLAttributes<HTMLElement>, 'onChange'> {
|
||||
config: DisplayConfig;
|
||||
screenshot: string;
|
||||
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/png;base64,${screenshot.encode_image}`,
|
||||
[screenshot.encode_image],
|
||||
);
|
||||
|
||||
const onLedStripConfigChange = useCallback(
|
||||
(
|
||||
position:
|
||||
| 'top_led_strip'
|
||||
| 'left_led_strip'
|
||||
| 'right_led_strip'
|
||||
| 'bottom_led_strip',
|
||||
value: LedStripConfig | null,
|
||||
) => {
|
||||
const c = { ...config, [position]: value };
|
||||
onChange?.(c);
|
||||
},
|
||||
[config],
|
||||
);
|
||||
return (
|
||||
<section className="m-4 grid grid-rows-3 grid-cols-3 gr" {...htmlAttrs}>
|
||||
<img src={screenshot} className="row-start-2 col-start-2" />
|
||||
<LedStrip config={config.top_led_strip} className="row-start-1 col-start-2 h-1" />
|
||||
<LedStrip config={config.left_led_strip} className="row-start-2 col-start-1 w-1" />
|
||||
<LedStrip config={config.right_led_strip} className="row-start-2 col-start-3" />
|
||||
<LedStrip config={config.bottom_led_strip} className="row-start-3 col-start-2" />
|
||||
</section>
|
||||
<StyledContainer {...htmlAttrs}>
|
||||
<img src={screenshotUrl} tw="row-start-3 col-start-3" />
|
||||
<LedStrip
|
||||
config={config.top_led_strip}
|
||||
colors={screenshot.colors.top}
|
||||
tw="row-start-2 col-start-3"
|
||||
/>
|
||||
<LedStrip
|
||||
config={config.left_led_strip}
|
||||
colors={screenshot.colors.left}
|
||||
tw="row-start-3 col-start-2"
|
||||
/>
|
||||
<LedStrip
|
||||
config={config.right_led_strip}
|
||||
colors={screenshot.colors.right}
|
||||
tw="row-start-3 col-start-4"
|
||||
/>
|
||||
<LedStrip
|
||||
config={config.bottom_led_strip}
|
||||
colors={screenshot.colors.bottom}
|
||||
tw="row-start-4 col-start-3"
|
||||
/>
|
||||
<LedStripEditor
|
||||
config={config.top_led_strip}
|
||||
tw="row-start-1 col-start-3"
|
||||
onChange={(value) => onLedStripConfigChange('top_led_strip', value)}
|
||||
/>
|
||||
<LedStripEditor
|
||||
config={config.left_led_strip}
|
||||
tw="row-start-3 col-start-1"
|
||||
onChange={(value) => onLedStripConfigChange('left_led_strip', value)}
|
||||
/>
|
||||
<LedStripEditor
|
||||
config={config.right_led_strip}
|
||||
tw="row-start-3 col-start-5"
|
||||
onChange={(value) => onLedStripConfigChange('right_led_strip', value)}
|
||||
/>
|
||||
<LedStripEditor
|
||||
config={config.bottom_led_strip}
|
||||
tw="row-start-5 col-start-3"
|
||||
onChange={(value) => onLedStripConfigChange('bottom_led_strip', value)}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
57
src/configurator/components/led-strip-editor.tsx
Normal file
57
src/configurator/components/led-strip-editor.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
import { HTMLAttributes, useCallback } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { LedStripConfig } from '../models/led-strip-config';
|
||||
import tw, { css, styled, theme } 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 rounded-full h-4 w-4 text-xs shadow select-none`,
|
||||
tw`hocus:scale-105 hocus:active:scale-95 active:bg-amber-600`,
|
||||
);
|
||||
|
||||
export const LedStripEditor: FC<LedStripEditorProps> = ({
|
||||
config,
|
||||
onChange,
|
||||
...htmlAttrs
|
||||
}) => {
|
||||
const addLed = useCallback(() => {
|
||||
if (config) {
|
||||
onChange?.({ ...config, global_end_position: config.global_end_position + 1 });
|
||||
} else {
|
||||
onChange?.(new LedStripConfig(0, 0, 1));
|
||||
}
|
||||
}, [config, onChange]);
|
||||
const removeLed = useCallback(() => {
|
||||
if (!config) {
|
||||
onChange?.(null);
|
||||
} else {
|
||||
onChange?.({ ...config, global_end_position: config.global_end_position - 1 });
|
||||
}
|
||||
}, [config, onChange]);
|
||||
|
||||
return (
|
||||
<StyledContainer {...htmlAttrs}>
|
||||
<StyledButton title="Add LED" onClick={addLed}>
|
||||
<FontAwesomeIcon icon={faPlus} />
|
||||
</StyledButton>
|
||||
<StyledButton title="Remove LED" onClick={removeLed}>
|
||||
<FontAwesomeIcon icon={faMinus} />
|
||||
</StyledButton>
|
||||
<StyledButton title="Reverse">
|
||||
<FontAwesomeIcon icon={faLeftRight} />
|
||||
</StyledButton>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
@ -1,11 +1,36 @@
|
||||
import { HTMLAttributes } from 'react';
|
||||
import { HTMLAttributes, useMemo } from 'react';
|
||||
import { FC } from 'react';
|
||||
import { LedStripConfig } from '../models/led-strip-config';
|
||||
import tw, { css, styled, theme } from 'twin.macro';
|
||||
import { splitEvery } from 'ramda';
|
||||
|
||||
export interface LedStripProps extends HTMLAttributes<HTMLElement> {
|
||||
config: LedStripConfig;
|
||||
config: LedStripConfig | null;
|
||||
colors: Uint8Array;
|
||||
}
|
||||
|
||||
export const LedStrip: FC<LedStripProps> = ({ config, ...htmlAttrs }) => {
|
||||
return <section {...htmlAttrs}>...</section>;
|
||||
const StyledContainer = styled.section(
|
||||
tw`dark:bg-transparent shadow-xl border-gray-500 border rounded-full flex flex-wrap justify-around items-center`,
|
||||
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 }) => {
|
||||
const pixels = useMemo(() => {
|
||||
const pixels = splitEvery(3, Array.from(colors)) as Array<[number, number, number]>;
|
||||
return pixels.map((rgb, index) => <StyledPixel key={index} rgb={rgb}></StyledPixel>);
|
||||
}, [colors]);
|
||||
return (
|
||||
<StyledContainer {...htmlAttrs} css={[!config && tw`bg-gray-200`]}>
|
||||
{pixels}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
@ -1,46 +1,96 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { FC, useMemo } from 'react';
|
||||
import { useAsync } from 'react-async-hook';
|
||||
import { FC, Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import tw 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, Snackbar } from '@mui/material';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons';
|
||||
import { update } from 'ramda';
|
||||
|
||||
const getPickerConfig = () => invoke<PickerConfiguration>('get_picker_config');
|
||||
const getScreenshotOfDisplays = () =>
|
||||
invoke<string[]>('take_snapshot').then((items) =>
|
||||
items?.map((it) => `data:image/webp;base64,${it}`),
|
||||
);
|
||||
invoke<ScreenshotDto[]>('take_snapshot').then((items) => {
|
||||
console.log(items);
|
||||
return items;
|
||||
});
|
||||
const getScreenshotByConfig = async (config: DisplayConfig) => {
|
||||
return await invoke<ScreenshotDto>('get_screenshot_by_config', {
|
||||
config,
|
||||
});
|
||||
};
|
||||
|
||||
export const Configurator: FC = () => {
|
||||
const { loading: pendingPickerConfig, result: pickerConfig } = useAsync(
|
||||
const { loading: pendingPickerConfig, result: savedPickerConfig } = useAsync(
|
||||
getPickerConfig,
|
||||
[],
|
||||
);
|
||||
|
||||
const { loading: pendingScreenshotOfDisplays, result: screenshotOfDisplays } = useAsync(
|
||||
getScreenshotOfDisplays,
|
||||
[],
|
||||
);
|
||||
const { loading: pendingScreenshotOfDisplays, result: defaultScreenshotOfDisplays } =
|
||||
useAsync(getScreenshotOfDisplays, []);
|
||||
|
||||
const [screenshotOfDisplays, setScreenshotOfDisplays] = useState<ScreenshotDto[]>([]);
|
||||
|
||||
const { loading: pendingGetLedColorsByConfig, execute: onPickerChange } =
|
||||
useAsyncCallback(async (value: DisplayConfig) => {
|
||||
console.log(value);
|
||||
const screenshot = await getScreenshotByConfig(value);
|
||||
setScreenshotOfDisplays((old) => {
|
||||
const index = old.findIndex((it) => it.config.id === screenshot.config.id);
|
||||
console.log({ old, n: update(index, screenshot, old) });
|
||||
return update(index, screenshot, old);
|
||||
});
|
||||
|
||||
console.log('screenshot', screenshot);
|
||||
});
|
||||
|
||||
const [displayConfigs, setDisplayConfigs] = useState<DisplayConfig[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const displayConfigs = savedPickerConfig?.display_configs;
|
||||
if (displayConfigs) {
|
||||
setDisplayConfigs(displayConfigs);
|
||||
}
|
||||
}, [savedPickerConfig]);
|
||||
useEffect(() => {
|
||||
if (defaultScreenshotOfDisplays) {
|
||||
setScreenshotOfDisplays(defaultScreenshotOfDisplays);
|
||||
}
|
||||
}, [defaultScreenshotOfDisplays]);
|
||||
|
||||
const displays = useMemo(() => {
|
||||
if (pickerConfig && screenshotOfDisplays) {
|
||||
if (screenshotOfDisplays) {
|
||||
console.log({ c: screenshotOfDisplays });
|
||||
return screenshotOfDisplays.map((screenshot, index) => (
|
||||
<DisplayWithLedStrips
|
||||
key={index}
|
||||
config={pickerConfig.display_configs[index] ?? {}}
|
||||
config={screenshot.config}
|
||||
screenshot={screenshot}
|
||||
onChange={(value) => onPickerChange(value)}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}, [pickerConfig, screenshotOfDisplays]);
|
||||
}, [displayConfigs, screenshotOfDisplays]);
|
||||
|
||||
if (pendingPickerConfig || pendingScreenshotOfDisplays) {
|
||||
return (
|
||||
<section>
|
||||
等待 {JSON.stringify({ pendingPickerConfig, pendingScreenshotOfDisplays })}
|
||||
{displays}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return <section>{displays}</section>;
|
||||
return (
|
||||
<Fragment>
|
||||
<section>{displays}</section>;
|
||||
<Snackbar open={pendingGetLedColorsByConfig} autoHideDuration={3000}>
|
||||
<Alert icon={<FontAwesomeIcon icon={faSpinner} />} sx={{ width: '100%' }}>
|
||||
This is a success message!
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { LedStripConfig } from './led-strip-config';
|
||||
|
||||
export class DisplayConfig {
|
||||
index_of_display!: number;
|
||||
display_width!: number;
|
||||
display_height!: number;
|
||||
top_led_strip!: LedStripConfig;
|
||||
bottom_led_strip!: LedStripConfig;
|
||||
left_led_strip!: LedStripConfig;
|
||||
right_led_strip!: LedStripConfig;
|
||||
top_led_strip: LedStripConfig | null = null;
|
||||
bottom_led_strip: LedStripConfig | null = null;
|
||||
left_led_strip: LedStripConfig | null = null;
|
||||
right_led_strip: LedStripConfig | null = null;
|
||||
|
||||
constructor(
|
||||
public id: number,
|
||||
public index_of_display: number,
|
||||
public display_width: number,
|
||||
public display_height: number,
|
||||
) {}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
export class LedStripConfig {
|
||||
index!: number;
|
||||
global_start_position!: number;
|
||||
global_end_position!: number;
|
||||
constructor(
|
||||
public index: number,
|
||||
public global_start_position: number,
|
||||
public global_end_position: number,
|
||||
) {}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { DisplayConfig } from './display-config';
|
||||
|
||||
export class PickerConfiguration {
|
||||
config_version!: number;
|
||||
display_configs!: DisplayConfig[];
|
||||
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,10 +1,11 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import "./style.css";
|
||||
import GlobalStyles from './styles/global-styles';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<GlobalStyles />
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
@ -5,7 +5,8 @@ import tw, { theme, GlobalStyles as BaseStyles } from 'twin.macro';
|
||||
const customStyles = css({
|
||||
body: {
|
||||
WebkitTapHighlightColor: theme`colors.purple.500`,
|
||||
...tw`antialiased dark:bg-dark-800 bg-dark-100`,
|
||||
...tw`antialiased`,
|
||||
...tw`dark:bg-dark-800 bg-dark-100 dark:text-gray-100 text-gray-800`,
|
||||
},
|
||||
});
|
||||
|
||||
|
Reference in New Issue
Block a user