feat: GUI 配置支持添加和减少 LED。

This commit is contained in:
2023-01-02 16:53:20 +08:00
parent 4ad78ae5cc
commit 366b137258
21 changed files with 732 additions and 106 deletions

View File

@ -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>

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

View File

@ -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>
);
};

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

View File

@ -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>
);
};

View File

@ -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>
);
};

View File

@ -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,
) {}
}

View File

@ -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,
) {}
}

View File

@ -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,
) {}
}

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

View File

@ -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>,
);

View File

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