feat(gui): make layout direction and history width persist
This commit is contained in:
parent
055f6e2956
commit
2b562d9464
|
@ -1,4 +1,4 @@
|
||||||
import { doesExist, mustExist } from '@apextoaster/js-utils';
|
import { mustExist } from '@apextoaster/js-utils';
|
||||||
import { Grid, Typography } from '@mui/material';
|
import { Grid, Typography } from '@mui/material';
|
||||||
import { ReactNode, useContext } from 'react';
|
import { ReactNode, useContext } from 'react';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
@ -12,7 +12,13 @@ import { ImageCard } from './card/ImageCard.js';
|
||||||
import { LoadingCard } from './card/LoadingCard.js';
|
import { LoadingCard } from './card/LoadingCard.js';
|
||||||
import { JobStatus } from '../types/api-v2.js';
|
import { JobStatus } from '../types/api-v2.js';
|
||||||
|
|
||||||
export function ImageHistory() {
|
export interface ImageHistoryProps {
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageHistory(props: ImageHistoryProps) {
|
||||||
|
const { width } = props;
|
||||||
|
|
||||||
const store = mustExist(useContext(StateContext));
|
const store = mustExist(useContext(StateContext));
|
||||||
const { history, limit } = useStore(store, selectParams, shallow);
|
const { history, limit } = useStore(store, selectParams, shallow);
|
||||||
const { removeHistory } = useStore(store, selectActions, shallow);
|
const { removeHistory } = useStore(store, selectActions, shallow);
|
||||||
|
@ -42,7 +48,8 @@ export function ImageHistory() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return <Grid container spacing={2}>{children.map(([key, child]) => <Grid item key={key} xs={6}>{child}</Grid>)}</Grid>;
|
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||||
|
return <Grid container spacing={2}>{children.map(([key, child]) => <Grid item key={key} xs={12 / width}>{child}</Grid>)}</Grid>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectActions(state: OnnxState) {
|
export function selectActions(state: OnnxState) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||||
import { mustExist } from '@apextoaster/js-utils';
|
import { mustExist } from '@apextoaster/js-utils';
|
||||||
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||||
import { Box, Container, CssBaseline, Divider, Tab, useMediaQuery } from '@mui/material';
|
import { Box, Button, Container, CssBaseline, Divider, Stack, Tab, useMediaQuery } from '@mui/material';
|
||||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
import { Breakpoint, SxProps, Theme, ThemeProvider, createTheme } from '@mui/material/styles';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { useContext, useMemo } from 'react';
|
import { useContext, useMemo } from 'react';
|
||||||
import { useHash } from 'react-use/lib/useHash';
|
import { useHash } from 'react-use/lib/useHash';
|
||||||
|
@ -29,6 +30,7 @@ export function OnnxWeb(props: OnnxWebProps) {
|
||||||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||||
const store = mustExist(useContext(StateContext));
|
const store = mustExist(useContext(StateContext));
|
||||||
const stateTheme = useStore(store, selectTheme);
|
const stateTheme = useStore(store, selectTheme);
|
||||||
|
const layout = useStore(store, selectLayout);
|
||||||
|
|
||||||
const theme = useMemo(
|
const theme = useMemo(
|
||||||
() => createTheme({
|
() => createTheme({
|
||||||
|
@ -41,14 +43,22 @@ export function OnnxWeb(props: OnnxWebProps) {
|
||||||
|
|
||||||
const [hash, setHash] = useHash();
|
const [hash, setHash] = useHash();
|
||||||
|
|
||||||
|
const historyStyle: SxProps<Theme> = {
|
||||||
|
mx: 4,
|
||||||
|
my: 4,
|
||||||
|
...LAYOUT_STYLES[layout.direction].history.style,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
<Container>
|
<Container maxWidth={LAYOUT_STYLES[layout.direction].container}>
|
||||||
<Box sx={{ my: 4 }}>
|
<Box sx={{ my: 4 }}>
|
||||||
<Logo />
|
<Logo />
|
||||||
</Box>
|
</Box>
|
||||||
{props.motd && <Motd />}
|
{props.motd && <Motd />}
|
||||||
|
<Stack direction={LAYOUT_STYLES[layout.direction].direction} spacing={2}>
|
||||||
|
<Stack direction='column'>
|
||||||
<TabContext value={getTab(hash)}>
|
<TabContext value={getTab(hash)}>
|
||||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||||
<TabList onChange={(_e, idx) => {
|
<TabList onChange={(_e, idx) => {
|
||||||
|
@ -79,10 +89,12 @@ export function OnnxWeb(props: OnnxWebProps) {
|
||||||
<Settings />
|
<Settings />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
</TabContext>
|
</TabContext>
|
||||||
<Divider variant='middle' />
|
</Stack>
|
||||||
<Box sx={{ mx: 4, my: 4 }}>
|
<Divider flexItem variant='middle' orientation={LAYOUT_STYLES[layout.direction].divider} />
|
||||||
<ImageHistory />
|
<Box sx={historyStyle}>
|
||||||
|
<ImageHistory width={layout.width} />
|
||||||
</Box>
|
</Box>
|
||||||
|
</Stack>
|
||||||
</Container>
|
</Container>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -91,3 +103,34 @@ export function OnnxWeb(props: OnnxWebProps) {
|
||||||
export function selectTheme(state: OnnxState) {
|
export function selectTheme(state: OnnxState) {
|
||||||
return state.theme;
|
return state.theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectLayout(state: OnnxState) {
|
||||||
|
return {
|
||||||
|
direction: state.layout,
|
||||||
|
width: state.historyWidth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LAYOUT_STYLES = {
|
||||||
|
horizontal: {
|
||||||
|
container: false,
|
||||||
|
direction: 'row',
|
||||||
|
divider: 'vertical',
|
||||||
|
history: {
|
||||||
|
style: {
|
||||||
|
maxHeight: '85vb',
|
||||||
|
overflowY: 'auto',
|
||||||
|
},
|
||||||
|
width: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
container: 'lg' as Breakpoint,
|
||||||
|
direction: 'column',
|
||||||
|
divider: 'horizontal',
|
||||||
|
history: {
|
||||||
|
style: {},
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
|
@ -90,11 +90,9 @@ export const UNKNOWN_ERROR = `${IMAGE_ERROR}unknown`;
|
||||||
export function getImageErrorReason(image: FailedJobResponse | UnknownJobResponse) {
|
export function getImageErrorReason(image: FailedJobResponse | UnknownJobResponse) {
|
||||||
if (image.status === JobStatus.FAILED) {
|
if (image.status === JobStatus.FAILED) {
|
||||||
const error = image.reason;
|
const error = image.reason;
|
||||||
if (doesExist(error) && error.startsWith(ANY_ERROR)) {
|
if (doesExist(error)) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${IMAGE_ERROR}${error}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return UNKNOWN_ERROR;
|
return UNKNOWN_ERROR;
|
||||||
|
|
|
@ -40,13 +40,22 @@ export function Settings() {
|
||||||
|
|
||||||
return <Stack spacing={2}>
|
return <Stack spacing={2}>
|
||||||
<NumericField
|
<NumericField
|
||||||
label={t('setting.history')}
|
label={t('setting.history.limit')}
|
||||||
min={2}
|
min={2}
|
||||||
max={20}
|
max={40}
|
||||||
step={1}
|
step={1}
|
||||||
value={state.limit}
|
value={state.limit}
|
||||||
onChange={(value) => state.setLimit(value)}
|
onChange={(value) => state.setLimit(value)}
|
||||||
/>
|
/>
|
||||||
|
<NumericField
|
||||||
|
label={t('setting.history.width')}
|
||||||
|
min={2}
|
||||||
|
max={6}
|
||||||
|
step={1}
|
||||||
|
value={state.historyWidth}
|
||||||
|
onChange={(value) => state.setWidth(value)}
|
||||||
|
/>
|
||||||
|
<Button onClick={() => state.setLayout(state.layout === 'horizontal' ? 'vertical' : 'horizontal')}>Toggle Layout</Button>
|
||||||
<TextField variant='outlined' label={t('setting.prompt')} value={state.defaults.prompt} onChange={(event) => {
|
<TextField variant='outlined' label={t('setting.prompt')} value={state.defaults.prompt} onChange={(event) => {
|
||||||
state.setDefaults({
|
state.setDefaults({
|
||||||
prompt: event.target.value,
|
prompt: event.target.value,
|
||||||
|
|
|
@ -21,11 +21,11 @@ export const DEFAULT_HISTORY = {
|
||||||
/**
|
/**
|
||||||
* The number of images to be shown.
|
* The number of images to be shown.
|
||||||
*/
|
*/
|
||||||
limit: 4,
|
limit: 8,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of additional images to be kept in history, so they can scroll
|
* The number of additional images to be kept in history, so they can scroll
|
||||||
* back into view when you delete one. Does not include deleted images.
|
* back into view when you delete one. Does not include deleted images.
|
||||||
*/
|
*/
|
||||||
scrollback: 2,
|
scrollback: 4,
|
||||||
};
|
};
|
||||||
|
|
|
@ -65,6 +65,7 @@ export async function renderApp(config: Config, params: ServerParams, logger: Lo
|
||||||
createBlendSlice,
|
createBlendSlice,
|
||||||
createResetSlice,
|
createResetSlice,
|
||||||
createProfileSlice,
|
createProfileSlice,
|
||||||
|
createSettingsSlice,
|
||||||
} = createStateSlices(params);
|
} = createStateSlices(params);
|
||||||
const state = createStore<OnnxState, [['zustand/persist', OnnxState]]>(persist((...slice) => ({
|
const state = createStore<OnnxState, [['zustand/persist', OnnxState]]>(persist((...slice) => ({
|
||||||
...createDefaultSlice(...slice),
|
...createDefaultSlice(...slice),
|
||||||
|
@ -77,6 +78,7 @@ export async function renderApp(config: Config, params: ServerParams, logger: Lo
|
||||||
...createBlendSlice(...slice),
|
...createBlendSlice(...slice),
|
||||||
...createResetSlice(...slice),
|
...createResetSlice(...slice),
|
||||||
...createProfileSlice(...slice),
|
...createProfileSlice(...slice),
|
||||||
|
...createSettingsSlice(...slice),
|
||||||
}), {
|
}), {
|
||||||
migrate(persistedState, version) {
|
migrate(persistedState, version) {
|
||||||
return applyStateMigrations(params, persistedState as UnknownState, version, logger);
|
return applyStateMigrations(params, persistedState as UnknownState, version, logger);
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { InpaintSlice, createInpaintSlice } from './inpaint.js';
|
||||||
import { ModelSlice, createModelSlice } from './model.js';
|
import { ModelSlice, createModelSlice } from './model.js';
|
||||||
import { ProfileSlice, createProfileSlice } from './profile.js';
|
import { ProfileSlice, createProfileSlice } from './profile.js';
|
||||||
import { ResetSlice, createResetSlice } from './reset.js';
|
import { ResetSlice, createResetSlice } from './reset.js';
|
||||||
|
import { SettingsSlice, createSettingsSlice } from './settings.js';
|
||||||
import { Txt2ImgSlice, createTxt2ImgSlice } from './txt2img.js';
|
import { Txt2ImgSlice, createTxt2ImgSlice } from './txt2img.js';
|
||||||
import { UpscaleSlice, createUpscaleSlice } from './upscale.js';
|
import { UpscaleSlice, createUpscaleSlice } from './upscale.js';
|
||||||
import {
|
import {
|
||||||
|
@ -39,7 +40,8 @@ export type OnnxState
|
||||||
& UpscaleSlice
|
& UpscaleSlice
|
||||||
& BlendSlice
|
& BlendSlice
|
||||||
& ResetSlice
|
& ResetSlice
|
||||||
& ProfileSlice;
|
& ProfileSlice
|
||||||
|
& SettingsSlice;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* React context binding for API client.
|
* React context binding for API client.
|
||||||
|
@ -69,7 +71,7 @@ export const STATE_KEY = 'onnx-web';
|
||||||
/**
|
/**
|
||||||
* Current state version for zustand persistence.
|
* Current state version for zustand persistence.
|
||||||
*/
|
*/
|
||||||
export const STATE_VERSION = 11;
|
export const STATE_VERSION = 13;
|
||||||
|
|
||||||
export function baseParamsFromServer(defaults: ServerParams): Required<BaseImgParams> {
|
export function baseParamsFromServer(defaults: ServerParams): Required<BaseImgParams> {
|
||||||
return {
|
return {
|
||||||
|
@ -144,6 +146,7 @@ export function createStateSlices(server: ServerParams) {
|
||||||
createModelSlice: createModelSlice(),
|
createModelSlice: createModelSlice(),
|
||||||
createProfileSlice: createProfileSlice(),
|
createProfileSlice: createProfileSlice(),
|
||||||
createResetSlice: createResetSlice(),
|
createResetSlice: createResetSlice(),
|
||||||
|
createSettingsSlice: createSettingsSlice(),
|
||||||
createTxt2ImgSlice: createTxt2ImgSlice(server, defaultParams, defaultHighres, defaultModel, defaultUpscale, defaultGrid),
|
createTxt2ImgSlice: createTxt2ImgSlice(server, defaultParams, defaultHighres, defaultModel, defaultUpscale, defaultGrid),
|
||||||
createUpscaleSlice: createUpscaleSlice(defaultParams, defaultHighres, defaultModel, defaultUpscale),
|
createUpscaleSlice: createUpscaleSlice(defaultParams, defaultHighres, defaultModel, defaultUpscale),
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ export interface HistorySlice {
|
||||||
|
|
||||||
pushHistory(image: JobResponse, retry?: RetryParams): void;
|
pushHistory(image: JobResponse, retry?: RetryParams): void;
|
||||||
removeHistory(image: JobResponse): void;
|
removeHistory(image: JobResponse): void;
|
||||||
|
|
||||||
setLimit(limit: number): void;
|
setLimit(limit: number): void;
|
||||||
setReady(image: JobResponse): void;
|
setReady(image: JobResponse): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,7 +212,10 @@ export const I18N_STRINGS_DE = {
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
connectServer: 'verbinden zum Server',
|
connectServer: 'verbinden zum Server',
|
||||||
history: 'Bildgeschichte',
|
history: {
|
||||||
|
limit: 'Bildgeschichte',
|
||||||
|
width: '',
|
||||||
|
},
|
||||||
loadState: 'Laden',
|
loadState: 'Laden',
|
||||||
prompt: 'Standard-Eingabeaufforderung',
|
prompt: 'Standard-Eingabeaufforderung',
|
||||||
reset: {
|
reset: {
|
||||||
|
|
|
@ -275,7 +275,10 @@ export const I18N_STRINGS_EN = {
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
connectServer: 'Connect',
|
connectServer: 'Connect',
|
||||||
history: 'Image History',
|
history: {
|
||||||
|
limit: 'Image History Length',
|
||||||
|
width: 'Image History Width',
|
||||||
|
},
|
||||||
loadState: 'Load',
|
loadState: 'Load',
|
||||||
prompt: 'Default Prompt',
|
prompt: 'Default Prompt',
|
||||||
reset: {
|
reset: {
|
||||||
|
|
|
@ -212,7 +212,10 @@ export const I18N_STRINGS_ES = {
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
connectServer: 'Conectar al servidor',
|
connectServer: 'Conectar al servidor',
|
||||||
history: 'Historia de la imagen',
|
history: {
|
||||||
|
limit: 'Historia de la imagen',
|
||||||
|
width: '',
|
||||||
|
},
|
||||||
loadState: 'Carga estado',
|
loadState: 'Carga estado',
|
||||||
prompt: 'Solicitud predeterminada',
|
prompt: 'Solicitud predeterminada',
|
||||||
reset: {
|
reset: {
|
||||||
|
|
|
@ -212,7 +212,10 @@ export const I18N_STRINGS_FR = {
|
||||||
},
|
},
|
||||||
setting: {
|
setting: {
|
||||||
connectServer: '',
|
connectServer: '',
|
||||||
history: '',
|
history: {
|
||||||
|
limit: '',
|
||||||
|
width: '',
|
||||||
|
},
|
||||||
loadState: '',
|
loadState: '',
|
||||||
prompt: '',
|
prompt: '',
|
||||||
reset: {
|
reset: {
|
||||||
|
|
Loading…
Reference in New Issue