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 { ReactNode, useContext } from 'react';
|
||||
import * as React from 'react';
|
||||
|
@ -12,7 +12,13 @@ import { ImageCard } from './card/ImageCard.js';
|
|||
import { LoadingCard } from './card/LoadingCard.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 { history, limit } = useStore(store, selectParams, 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) {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
||||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { TabContext, TabList, TabPanel } from '@mui/lab';
|
||||
import { Box, Container, CssBaseline, Divider, Tab, useMediaQuery } from '@mui/material';
|
||||
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import { Box, Button, Container, CssBaseline, Divider, Stack, Tab, useMediaQuery } from '@mui/material';
|
||||
import { Breakpoint, SxProps, Theme, ThemeProvider, createTheme } from '@mui/material/styles';
|
||||
import * as React from 'react';
|
||||
import { useContext, useMemo } from 'react';
|
||||
import { useHash } from 'react-use/lib/useHash';
|
||||
|
@ -29,6 +30,7 @@ export function OnnxWeb(props: OnnxWebProps) {
|
|||
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
|
||||
const store = mustExist(useContext(StateContext));
|
||||
const stateTheme = useStore(store, selectTheme);
|
||||
const layout = useStore(store, selectLayout);
|
||||
|
||||
const theme = useMemo(
|
||||
() => createTheme({
|
||||
|
@ -41,48 +43,58 @@ export function OnnxWeb(props: OnnxWebProps) {
|
|||
|
||||
const [hash, setHash] = useHash();
|
||||
|
||||
const historyStyle: SxProps<Theme> = {
|
||||
mx: 4,
|
||||
my: 4,
|
||||
...LAYOUT_STYLES[layout.direction].history.style,
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Container>
|
||||
<Container maxWidth={LAYOUT_STYLES[layout.direction].container}>
|
||||
<Box sx={{ my: 4 }}>
|
||||
<Logo />
|
||||
</Box>
|
||||
{props.motd && <Motd />}
|
||||
<TabContext value={getTab(hash)}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<TabList onChange={(_e, idx) => {
|
||||
setHash(idx);
|
||||
}}>
|
||||
{TAB_LABELS.map((name) => <Tab key={name} label={name} value={name} />)}
|
||||
</TabList>
|
||||
<Stack direction={LAYOUT_STYLES[layout.direction].direction} spacing={2}>
|
||||
<Stack direction='column'>
|
||||
<TabContext value={getTab(hash)}>
|
||||
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
|
||||
<TabList onChange={(_e, idx) => {
|
||||
setHash(idx);
|
||||
}}>
|
||||
{TAB_LABELS.map((name) => <Tab key={name} label={name} value={name} />)}
|
||||
</TabList>
|
||||
</Box>
|
||||
<TabPanel value='txt2img'>
|
||||
<Txt2Img />
|
||||
</TabPanel>
|
||||
<TabPanel value='img2img'>
|
||||
<Img2Img />
|
||||
</TabPanel>
|
||||
<TabPanel value='inpaint'>
|
||||
<Inpaint />
|
||||
</TabPanel>
|
||||
<TabPanel value='upscale'>
|
||||
<Upscale />
|
||||
</TabPanel>
|
||||
<TabPanel value='blend'>
|
||||
<Blend />
|
||||
</TabPanel>
|
||||
<TabPanel value='models'>
|
||||
<Models />
|
||||
</TabPanel>
|
||||
<TabPanel value='settings'>
|
||||
<Settings />
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
</Stack>
|
||||
<Divider flexItem variant='middle' orientation={LAYOUT_STYLES[layout.direction].divider} />
|
||||
<Box sx={historyStyle}>
|
||||
<ImageHistory width={layout.width} />
|
||||
</Box>
|
||||
<TabPanel value='txt2img'>
|
||||
<Txt2Img />
|
||||
</TabPanel>
|
||||
<TabPanel value='img2img'>
|
||||
<Img2Img />
|
||||
</TabPanel>
|
||||
<TabPanel value='inpaint'>
|
||||
<Inpaint />
|
||||
</TabPanel>
|
||||
<TabPanel value='upscale'>
|
||||
<Upscale />
|
||||
</TabPanel>
|
||||
<TabPanel value='blend'>
|
||||
<Blend />
|
||||
</TabPanel>
|
||||
<TabPanel value='models'>
|
||||
<Models />
|
||||
</TabPanel>
|
||||
<TabPanel value='settings'>
|
||||
<Settings />
|
||||
</TabPanel>
|
||||
</TabContext>
|
||||
<Divider variant='middle' />
|
||||
<Box sx={{ mx: 4, my: 4 }}>
|
||||
<ImageHistory />
|
||||
</Box>
|
||||
</Stack>
|
||||
</Container>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -91,3 +103,34 @@ export function OnnxWeb(props: OnnxWebProps) {
|
|||
export function selectTheme(state: OnnxState) {
|
||||
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) {
|
||||
if (image.status === JobStatus.FAILED) {
|
||||
const error = image.reason;
|
||||
if (doesExist(error) && error.startsWith(ANY_ERROR)) {
|
||||
if (doesExist(error)) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return `${IMAGE_ERROR}${error}`;
|
||||
}
|
||||
|
||||
return UNKNOWN_ERROR;
|
||||
|
|
|
@ -40,13 +40,22 @@ export function Settings() {
|
|||
|
||||
return <Stack spacing={2}>
|
||||
<NumericField
|
||||
label={t('setting.history')}
|
||||
label={t('setting.history.limit')}
|
||||
min={2}
|
||||
max={20}
|
||||
max={40}
|
||||
step={1}
|
||||
value={state.limit}
|
||||
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) => {
|
||||
state.setDefaults({
|
||||
prompt: event.target.value,
|
||||
|
|
|
@ -21,11 +21,11 @@ export const DEFAULT_HISTORY = {
|
|||
/**
|
||||
* 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
|
||||
* 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,
|
||||
createResetSlice,
|
||||
createProfileSlice,
|
||||
createSettingsSlice,
|
||||
} = createStateSlices(params);
|
||||
const state = createStore<OnnxState, [['zustand/persist', OnnxState]]>(persist((...slice) => ({
|
||||
...createDefaultSlice(...slice),
|
||||
|
@ -77,6 +78,7 @@ export async function renderApp(config: Config, params: ServerParams, logger: Lo
|
|||
...createBlendSlice(...slice),
|
||||
...createResetSlice(...slice),
|
||||
...createProfileSlice(...slice),
|
||||
...createSettingsSlice(...slice),
|
||||
}), {
|
||||
migrate(persistedState, version) {
|
||||
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 { ProfileSlice, createProfileSlice } from './profile.js';
|
||||
import { ResetSlice, createResetSlice } from './reset.js';
|
||||
import { SettingsSlice, createSettingsSlice } from './settings.js';
|
||||
import { Txt2ImgSlice, createTxt2ImgSlice } from './txt2img.js';
|
||||
import { UpscaleSlice, createUpscaleSlice } from './upscale.js';
|
||||
import {
|
||||
|
@ -39,7 +40,8 @@ export type OnnxState
|
|||
& UpscaleSlice
|
||||
& BlendSlice
|
||||
& ResetSlice
|
||||
& ProfileSlice;
|
||||
& ProfileSlice
|
||||
& SettingsSlice;
|
||||
|
||||
/**
|
||||
* React context binding for API client.
|
||||
|
@ -69,7 +71,7 @@ export const STATE_KEY = 'onnx-web';
|
|||
/**
|
||||
* Current state version for zustand persistence.
|
||||
*/
|
||||
export const STATE_VERSION = 11;
|
||||
export const STATE_VERSION = 13;
|
||||
|
||||
export function baseParamsFromServer(defaults: ServerParams): Required<BaseImgParams> {
|
||||
return {
|
||||
|
@ -144,6 +146,7 @@ export function createStateSlices(server: ServerParams) {
|
|||
createModelSlice: createModelSlice(),
|
||||
createProfileSlice: createProfileSlice(),
|
||||
createResetSlice: createResetSlice(),
|
||||
createSettingsSlice: createSettingsSlice(),
|
||||
createTxt2ImgSlice: createTxt2ImgSlice(server, defaultParams, defaultHighres, defaultModel, defaultUpscale, defaultGrid),
|
||||
createUpscaleSlice: createUpscaleSlice(defaultParams, defaultHighres, defaultModel, defaultUpscale),
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ export interface HistorySlice {
|
|||
|
||||
pushHistory(image: JobResponse, retry?: RetryParams): void;
|
||||
removeHistory(image: JobResponse): void;
|
||||
|
||||
setLimit(limit: number): void;
|
||||
setReady(image: JobResponse): void;
|
||||
}
|
||||
|
|
|
@ -212,7 +212,10 @@ export const I18N_STRINGS_DE = {
|
|||
},
|
||||
setting: {
|
||||
connectServer: 'verbinden zum Server',
|
||||
history: 'Bildgeschichte',
|
||||
history: {
|
||||
limit: 'Bildgeschichte',
|
||||
width: '',
|
||||
},
|
||||
loadState: 'Laden',
|
||||
prompt: 'Standard-Eingabeaufforderung',
|
||||
reset: {
|
||||
|
|
|
@ -275,7 +275,10 @@ export const I18N_STRINGS_EN = {
|
|||
},
|
||||
setting: {
|
||||
connectServer: 'Connect',
|
||||
history: 'Image History',
|
||||
history: {
|
||||
limit: 'Image History Length',
|
||||
width: 'Image History Width',
|
||||
},
|
||||
loadState: 'Load',
|
||||
prompt: 'Default Prompt',
|
||||
reset: {
|
||||
|
|
|
@ -212,7 +212,10 @@ export const I18N_STRINGS_ES = {
|
|||
},
|
||||
setting: {
|
||||
connectServer: 'Conectar al servidor',
|
||||
history: 'Historia de la imagen',
|
||||
history: {
|
||||
limit: 'Historia de la imagen',
|
||||
width: '',
|
||||
},
|
||||
loadState: 'Carga estado',
|
||||
prompt: 'Solicitud predeterminada',
|
||||
reset: {
|
||||
|
|
|
@ -212,7 +212,10 @@ export const I18N_STRINGS_FR = {
|
|||
},
|
||||
setting: {
|
||||
connectServer: '',
|
||||
history: '',
|
||||
history: {
|
||||
limit: '',
|
||||
width: '',
|
||||
},
|
||||
loadState: '',
|
||||
prompt: '',
|
||||
reset: {
|
||||
|
|
Loading…
Reference in New Issue