diff --git a/api/gui/.gitignore b/api/gui/.gitignore index e9b37fd2..229bf879 100644 --- a/api/gui/.gitignore +++ b/api/gui/.gitignore @@ -1,3 +1,4 @@ +bundle/main.css bundle/main.js config.json index.html diff --git a/gui/Makefile b/gui/Makefile index b4d21f7a..f35c134c 100644 --- a/gui/Makefile +++ b/gui/Makefile @@ -22,6 +22,7 @@ docs-local: build: deps yarn tsc + cp -v src/components/main.css out/src/components/ build-shebang: build sed -i '1s;^;#! /usr/bin/env node\n\n;g' $(shell pwd)/out/src/main.js @@ -35,6 +36,7 @@ bundle: build # copy everything into the server's default path cp -v src/index.html ../api/gui/ cp -v src/config.json ../api/gui/ + cp -v out/bundle/main.css ../api/gui/bundle/ cp -v out/bundle/main.js ../api/gui/bundle/ COVER_OPTS := --all \ diff --git a/gui/package.json b/gui/package.json index afc832b5..a02e18e7 100644 --- a/gui/package.json +++ b/gui/package.json @@ -16,6 +16,7 @@ "@tanstack/react-query": "^4.0.5", "@types/lodash": "^4.14.192", "@types/node": "^18.15.11", + "allotment": "^1.19.5", "browser-bunyan": "^1.8.0", "exifreader": "^4.13.0", "i18next": "^22.4.14", diff --git a/gui/src/components/ImageHistory.tsx b/gui/src/components/ImageHistory.tsx index 03c82dcd..648dffce 100644 --- a/gui/src/components/ImageHistory.tsx +++ b/gui/src/components/ImageHistory.tsx @@ -48,8 +48,13 @@ export function ImageHistory(props: ImageHistoryProps) { } } - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - return {children.map(([key, child]) => {child})}; + return { + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + children.map(([key, child]) => {child}) + }; } export function selectActions(state: OnnxState) { diff --git a/gui/src/components/OnnxWeb.tsx b/gui/src/components/OnnxWeb.tsx index 53adb35a..2e37174d 100644 --- a/gui/src/components/OnnxWeb.tsx +++ b/gui/src/components/OnnxWeb.tsx @@ -1,13 +1,16 @@ /* eslint-disable @typescript-eslint/no-magic-numbers */ import { mustExist } from '@apextoaster/js-utils'; import { TabContext, TabList, TabPanel } from '@mui/lab'; -import { Box, Button, Container, CssBaseline, Divider, Stack, Tab, useMediaQuery } from '@mui/material'; +import { Box, Container, CssBaseline, Divider, Stack, Tab, useMediaQuery } from '@mui/material'; import { Breakpoint, SxProps, Theme, ThemeProvider, createTheme } from '@mui/material/styles'; +import { Allotment } from 'allotment'; import * as React from 'react'; import { useContext, useMemo } from 'react'; import { useHash } from 'react-use/lib/useHash'; import { useStore } from 'zustand'; +import { shallow } from 'zustand/shallow'; +import { Motd } from '../Motd.js'; import { OnnxState, StateContext } from '../state/full.js'; import { ImageHistory } from './ImageHistory.js'; import { Logo } from './Logo.js'; @@ -19,7 +22,9 @@ import { Settings } from './tab/Settings.js'; import { Txt2Img } from './tab/Txt2Img.js'; import { Upscale } from './tab/Upscale.js'; import { TAB_LABELS, getTab, getTheme } from './utils.js'; -import { Motd } from '../Motd.js'; + +import 'allotment/dist/style.css'; +import './main.css'; export interface OnnxWebProps { motd: boolean; @@ -30,7 +35,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 layout = useStore(store, selectLayout, shallow); const theme = useMemo( () => createTheme({ @@ -41,13 +46,7 @@ export function OnnxWeb(props: OnnxWebProps) { [prefersDarkMode, stateTheme], ); - const [hash, setHash] = useHash(); - - const historyStyle: SxProps = { - mx: 4, - my: 4, - ...LAYOUT_STYLES[layout.direction].history.style, - }; + const historyStyle: SxProps = LAYOUT_STYLES[layout.direction].history.style; return ( @@ -57,44 +56,7 @@ export function OnnxWeb(props: OnnxWebProps) { {props.motd && } - - - - - { - setHash(idx); - }}> - {TAB_LABELS.map((name) => )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + {layout.direction === 'vertical' ? : } ); @@ -114,10 +76,14 @@ export function selectLayout(state: OnnxState) { export const LAYOUT_STYLES = { horizontal: { container: false, + control: { + width: '30%', + }, direction: 'row', divider: 'vertical', history: { style: { + marginLeft: 4, maxHeight: '85vb', overflowY: 'auto', }, @@ -126,11 +92,91 @@ export const LAYOUT_STYLES = { }, vertical: { container: 'lg' as Breakpoint, + control: { + width: undefined, + }, direction: 'column', divider: 'horizontal', history: { - style: {}, + style: { + mx: 4, + my: 4, + }, width: 2, }, }, } as const; + +// used for both horizontal and vertical +export interface BodyProps { + layout: ReturnType; + style: SxProps; +} + +export function HorizontalBody(props: BodyProps) { + const store = mustExist(useContext(StateContext)); + const {direction} = useStore(store, selectLayout, shallow); + const layout = LAYOUT_STYLES[direction]; + + return + + + + + ; +} + +export function VerticalBody(props: BodyProps) { + const store = mustExist(useContext(StateContext)); + const {direction, width} = useStore(store, selectLayout, shallow); + const layout = LAYOUT_STYLES[direction]; + + return + + + + + + ; +} + +export function TabGroup() { + const store = mustExist(useContext(StateContext)); + const {direction} = useStore(store, selectLayout, shallow); + const layout = LAYOUT_STYLES[direction]; + + const [hash, setHash] = useHash(); + + return + + + { + setHash(idx); + }}> + {TAB_LABELS.map((name) => )} + + + + + + + + + + + + + + + + + + + + + + + + + ; +} diff --git a/gui/src/components/Profiles.tsx b/gui/src/components/Profiles.tsx index f24104ac..0cf839c3 100644 --- a/gui/src/components/Profiles.tsx +++ b/gui/src/components/Profiles.tsx @@ -25,6 +25,7 @@ import { OnnxState, StateContext } from '../state/full.js'; import { ImageMetadata } from '../types/api.js'; import { DeepPartial } from '../types/model.js'; import { BaseImgParams, HighresParams, ModelParams, Txt2ImgParams, UpscaleParams } from '../types/params.js'; +import { downloadAsJson } from '../utils.js'; export const ALLOWED_EXTENSIONS = ['.json','.jpg','.jpeg','.png','.txt','.webp']; export const EXTENSION_FILTER = ALLOWED_EXTENSIONS.join(','); @@ -119,10 +120,10 @@ export function Profiles(props: ProfilesProps) { onClick={() => { const state = store.getState(); saveProfile({ - model: props.selectModel(state), - params: props.selectParams(state), name: profileName, highres: props.selectHighres(state), + model: props.selectModel(state), + params: props.selectParams(state), upscale: props.selectUpscale(state), }); setDialogOpen(false); @@ -166,9 +167,9 @@ export function Profiles(props: ProfilesProps) { + { state.setDefaults({ prompt: event.target.value, @@ -82,14 +83,19 @@ export function Settings() { - { + { setJson(event.target.value); }} /> + ONNX Web +
diff --git a/gui/src/state/settings.ts b/gui/src/state/settings.ts new file mode 100644 index 00000000..02822dd5 --- /dev/null +++ b/gui/src/state/settings.ts @@ -0,0 +1,34 @@ +import { Slice } from './types.js'; + +export type Layout = 'horizontal' | 'vertical'; + +export const DEFAULT_LAYOUT = { + historyWidth: 4, + layout: 'vertical' as Layout, +} as const; + +export interface SettingsSlice { + historyWidth: number; + layout: Layout; + + setLayout(layout: Layout): void; + setWidth(width: number): void; +} + +export function createSettingsSlice(): Slice { + return (set) => ({ + ...DEFAULT_LAYOUT, + setLayout(layout) { + set((prev) => ({ + ...prev, + layout, + })); + }, + setWidth(width) { + set((prev) => ({ + ...prev, + historyWidth: width, + })); + }, + }); +} diff --git a/gui/src/strings/de.ts b/gui/src/strings/de.ts index 31a4975b..d2904913 100644 --- a/gui/src/strings/de.ts +++ b/gui/src/strings/de.ts @@ -216,7 +216,6 @@ export const I18N_STRINGS_DE = { limit: 'Bildgeschichte', width: '', }, - loadState: 'Laden', prompt: 'Standard-Eingabeaufforderung', reset: { all: 'Alles zurücksetzen', @@ -226,7 +225,11 @@ export const I18N_STRINGS_DE = { }, scheduler: 'Standardplaner', server: 'API-Server', - state: 'Kundenstatus', + state: { + label: 'Kundenstatus', + load: 'Laden', + save: '', + }, darkMode: 'Dunkelmodus', }, sourceFilter: { diff --git a/gui/src/strings/en.ts b/gui/src/strings/en.ts index 30f1d75d..17f24ac6 100644 --- a/gui/src/strings/en.ts +++ b/gui/src/strings/en.ts @@ -279,7 +279,6 @@ export const I18N_STRINGS_EN = { limit: 'Image History Length', width: 'Image History Width', }, - loadState: 'Load', prompt: 'Default Prompt', reset: { all: 'Reset All', @@ -289,7 +288,11 @@ export const I18N_STRINGS_EN = { }, scheduler: 'Default Scheduler', server: 'API Server', - state: 'Client State', + state: { + label: 'Client State', + save: 'Save', + load: 'Load', + }, darkMode: 'Dark Mode', }, scheduler: { diff --git a/gui/src/strings/es.ts b/gui/src/strings/es.ts index 6dd12274..d9670d48 100644 --- a/gui/src/strings/es.ts +++ b/gui/src/strings/es.ts @@ -216,7 +216,6 @@ export const I18N_STRINGS_ES = { limit: 'Historia de la imagen', width: '', }, - loadState: 'Carga estado', prompt: 'Solicitud predeterminada', reset: { all: 'Resetear todo', @@ -226,7 +225,11 @@ export const I18N_STRINGS_ES = { }, scheduler: 'Programador predeterminado', server: 'Servidor API', - state: 'Estado del cliente', + state: { + label: 'Estado del cliente', + load: 'Carga estado', + save: '', + }, darkMode: 'Modo Oscuro', }, sourceFilter: { diff --git a/gui/src/strings/fr.ts b/gui/src/strings/fr.ts index 8f6e2b35..779d4b25 100644 --- a/gui/src/strings/fr.ts +++ b/gui/src/strings/fr.ts @@ -216,7 +216,6 @@ export const I18N_STRINGS_FR = { limit: '', width: '', }, - loadState: '', prompt: '', reset: { all: '', @@ -226,7 +225,11 @@ export const I18N_STRINGS_FR = { }, scheduler: '', server: '', - state: '', + state: { + label: '', + load: '', + save: '', + }, darkMode: 'Mode Sombre', }, sourceFilter: { diff --git a/gui/src/utils.ts b/gui/src/utils.ts index 6fbdbab9..31faf53d 100644 --- a/gui/src/utils.ts +++ b/gui/src/utils.ts @@ -26,3 +26,16 @@ export function trimHash(val: string): string { return val; } + +/** + * from https://stackoverflow.com/a/30800715 + */ +export function downloadAsJson(data: object, filename = 'parameters.json'): void { + const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(data)); + const elem = document.createElement('a'); + elem.setAttribute('href', dataStr); + elem.setAttribute('download', filename); + document.body.appendChild(elem); // required for firefox + elem.click(); + elem.remove(); +} diff --git a/gui/yarn.lock b/gui/yarn.lock index f93b5e70..d36e3edc 100644 --- a/gui/yarn.lock +++ b/gui/yarn.lock @@ -389,6 +389,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@juggle/resize-observer@^3.3.1": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" + integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== + "@mochajs/multi-reporter@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@mochajs/multi-reporter/-/multi-reporter-1.1.0.tgz#378aafd9b9ecbd612753899a3be35026b79b62a5" @@ -806,6 +811,18 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +allotment@^1.19.5: + version "1.19.5" + resolved "https://registry.yarnpkg.com/allotment/-/allotment-1.19.5.tgz#238e5457cba8ebfc5a6a59c411b4c563048b38ed" + integrity sha512-lQDeuqMkEEzITT56NAXNOWxRXycqVyV62w3X7jVZQS9n+cznDx6RLJMD1cnWcRMfCdPUmAj07FAIR69ueytxDQ== + dependencies: + classnames "^2.3.0" + eventemitter3 "^5.0.0" + lodash.clamp "^4.0.0" + lodash.debounce "^4.0.0" + lodash.isequal "^4.5.0" + use-resize-observer "^9.0.0" + ansi-colors@4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -1056,6 +1073,11 @@ chokidar@3.5.3, chokidar@^3.5.3: optionalDependencies: fsevents "~2.3.2" +classnames@^2.3.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" + integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== + cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -1568,6 +1590,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eventemitter3@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== + exifreader@^4.13.0: version "4.13.0" resolved "https://registry.yarnpkg.com/exifreader/-/exifreader-4.13.0.tgz#f380b33cfc85630a0dbd56edd41e28710a9e9679" @@ -2233,11 +2260,26 @@ locate-path@^6.0.0: dependencies: p-locate "^5.0.0" +lodash.clamp@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/lodash.clamp/-/lodash.clamp-4.0.3.tgz#5c24bedeeeef0753560dc2b4cb4671f90a6ddfaa" + integrity sha512-HvzRFWjtcguTW7yd8NJBshuNaCa8aqNFtnswdT7f/cMd/1YKy5Zzoq4W/Oxvnx9l7aeY258uSdDfM793+eLsVg== + +lodash.debounce@^4.0.0: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + lodash.merge@^4.6.2: version "4.6.2" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" @@ -3101,6 +3143,13 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-resize-observer@^9.0.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/use-resize-observer/-/use-resize-observer-9.1.0.tgz#14735235cf3268569c1ea468f8a90c5789fc5c6c" + integrity sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow== + dependencies: + "@juggle/resize-observer" "^3.3.1" + use-sync-external-store@1.2.0, use-sync-external-store@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a"