Merge pull request #29 from canisminor1990/dev

feat: add react layout
This commit is contained in:
CanisMinor 2023-04-20 12:12:25 +08:00 committed by GitHub
commit cdadb6a7ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 1845 additions and 2450 deletions

View File

@ -1,3 +1,13 @@
module.exports = {
displayTypes: ['feat', 'fix', 'styles', 'pref'],
};
/scripts
/config
/example
_test_
__test__
/node_modules
jest*
/es
/lib
/docs
/dist
/javascript

View File

@ -1,7 +0,0 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120
}

37
.umirc.ts Normal file
View File

@ -0,0 +1,37 @@
import { defineConfig } from 'umi'
import WebpackShellPlugin from 'webpack-shell-plugin-next'
const mac = [
'rm ./javascript/index.js',
'rm ./style.css',
'cp ./dist/index.js ./javascript/index.js',
'cp ./dist/index.css ./style.css',
]
const win = [
'del javascript\\index.js',
'del style.css',
'copy dist\\index.js javascript\\index.js',
'copy dist\\index.css style.css',
]
export default defineConfig({
routes: [{ path: '/', component: 'index' }],
npmClient: 'yarn',
mpa: {},
codeSplitting: false,
define: {
'process.env': process.env,
},
chainWebpack(memo) {
memo.plugin('shell').use(WebpackShellPlugin, [
{
onBuildExit: {
scripts: process.platform === 'darwin' ? mac : win,
blocking: false,
parallel: false,
},
},
])
},
})

View File

@ -1,19 +0,0 @@
const gulp = require('gulp')
const less = require('gulp-less')
const ts = require('gulp-typescript')
const tsProject = ts.createProject('tsconfig.json')
gulp.task('compile', () => {
return gulp.src('src/script/**/*.ts').pipe(tsProject()).pipe(gulp.dest('javascript'))
})
gulp.task('less', () => {
return gulp.src('src/theme/*.less').pipe(less()).pipe(gulp.dest('./'))
})
gulp.task('build', gulp.parallel('compile', 'less'))
gulp.task('watch', () => {
gulp.watch('src/theme/**/*', gulp.parallel('less'))
gulp.watch('src/script/**/*', gulp.parallel('compile'))
})

View File

@ -1,19 +0,0 @@
"use strict";
/**
* 处理网站的 favicon 图标
*/
class FaviconHandler {
/**
* 设置网站的 favicon 图标
*/
static setFavicon() {
const link = document.createElement('link');
link.rel = 'icon';
link.type = 'image/svg+xml';
link.href = 'https://gw.alipayobjects.com/zos/bmw-prod/51a51720-8a30-4430-b6c9-be5712364f04.svg';
document.getElementsByTagName('head')[0].appendChild(link);
}
}
onUiLoaded(() => {
FaviconHandler.setFavicon();
});

View File

@ -1,256 +0,0 @@
"use strict";
/**
* 转换器工具类
*/
class Converter {
/**
* 将数字四舍五入到小数点后四位
* @param value 数字
* @returns 四舍五入后的数字
*/
static round(value) {
return Math.round(value * 10000) / 10000;
}
/**
* 将字符串中的中文冒号和括号转换成英文冒号和括号
* @param srt 字符串
* @returns 转换后的字符串
*/
static convertStr(srt) {
return srt.replace(//g, ':').replace(//g, '(').replace(//g, ')');
}
/**
* 将字符串按照括号分割成数组
* @param str 字符串
* @returns 分割后的数组
*/
static convertStr2Array(str) {
// 匹配各种括号中的内容,包括括号本身
const bracketRegex = /([()<>[\]])/g;
/**
* 将字符串按照各种括号分割成数组
* @param str 字符串
* @returns 分割后的数组
*/
const splitByBracket = (str) => {
const arr = [];
let start = 0;
let depth = 0;
let match;
while ((match = bracketRegex.exec(str)) !== null) {
if (depth === 0 && match.index > start) {
arr.push(str.substring(start, match.index));
start = match.index;
}
if (match[0] === '(' || match[0] === '<' || match[0] === '[') {
depth++;
}
else if (match[0] === ')' || match[0] === '>' || match[0] === ']') {
depth--;
}
if (depth === 0) {
arr.push(str.substring(start, match.index + 1));
start = match.index + 1;
}
}
if (start < str.length) {
arr.push(str.substring(start));
}
return arr;
};
/**
* 将字符串按照逗号和各种括号分割成数组
* @param str 字符串
* @returns 分割后的数组
*/
const splitByComma = (str) => {
const arr = [];
let start = 0;
let inBracket = false;
for (let i = 0; i < str.length; i++) {
if (str[i] === ',' && !inBracket) {
arr.push(str.substring(start, i).trim());
start = i + 1;
}
else if (str[i].match(bracketRegex)) {
inBracket = !inBracket;
}
}
arr.push(str.substring(start).trim());
return arr;
};
/**
* 清洗字符串并输出数组
* @param str 字符串
* @returns 清洗后的数组
*/
const cleanStr = (str) => {
let arr = splitByBracket(str);
arr = arr.flatMap((s) => splitByComma(s));
return arr.filter((s) => s !== '');
};
return cleanStr(str)
.filter((item) => {
const pattern = /^[,\s ]+$/;
return !pattern.test(item);
})
.filter(Boolean)
.sort((a, b) => {
return a.includes('<') && !b.includes('<') ? 1 : b.includes('<') && !a.includes('<') ? -1 : 0;
});
}
/**
* 将数组转换成字符串
* @param array 数组
* @returns 转换后的字符串
*/
static convertArray2Str(array) {
const newArray = array.map((item) => {
if (item.includes('<'))
return item;
const newItem = item
.replace(/\s+/g, ' ')
.replace(/|\.\|。/g, ',')
.replace(/“||”|"|\/'/g, '')
.replace(/, /g, ',')
.replace(/,,/g, ',')
.replace(/,/g, ', ');
return Converter.convertStr2Array(newItem).join(', ');
});
return newArray.join(', ');
}
/**
* 将输入的字符串转换成特定格式的字符串
* @param input 输入的字符串
* @returns 转换后的字符串
*/
static convert(input) {
const re_attention = /\{|\[|\}|\]|[^{}[\]]+/gmu;
let text = Converter.convertStr(input);
const textArray = Converter.convertStr2Array(text);
text = Converter.convertArray2Str(textArray);
let res = [];
const curly_bracket_multiplier = 1.05;
const square_bracket_multiplier = 1 / 1.05;
const brackets = {
'{': { stack: [], multiplier: curly_bracket_multiplier },
'[': { stack: [], multiplier: square_bracket_multiplier },
};
/**
* 将指定范围内的数字乘以指定倍数
* @param start_position 起始位置
* @param multiplier 倍数
*/
function multiply_range(start_position, multiplier) {
for (let pos = start_position; pos < res.length; pos++) {
res[pos][1] = Converter.round(res[pos][1] * multiplier);
}
}
for (const match of text.matchAll(re_attention)) {
let word = match[0];
if (word in brackets) {
brackets[word].stack.push(res.length);
}
else if (word === '}' || word === ']') {
const bracket = brackets[word === '}' ? '{' : '['];
if (bracket.stack.length > 0) {
multiply_range(bracket.stack.pop(), bracket.multiplier);
}
}
else {
res.push([word, 1.0]);
}
}
Object.keys(brackets).forEach((bracketType) => {
brackets[bracketType].stack.forEach((pos) => {
multiply_range(pos, brackets[bracketType].multiplier);
});
});
if (res.length === 0) {
res = [['', 1.0]];
}
let i = 0;
while (i + 1 < res.length) {
if (res[i][1] === res[i + 1][1]) {
res[i][0] += res[i + 1][0];
res.splice(i + 1, 1);
}
else {
i += 1;
}
}
let result = '';
for (const [word, value] of res) {
result += value === 1.0 ? word : `(${word}:${value.toString()})`;
}
return result;
}
/**
* 触发 input 事件
* @param target 目标元素
*/
static dispatchInputEvent(target) {
let inputEvent = new Event('input');
Object.defineProperty(inputEvent, 'target', { value: target });
target.dispatchEvent(inputEvent);
}
/**
* 点击转换按钮的事件处理函数
* @param type 类型
*/
static onClickConvert(type) {
const default_prompt = '';
const default_negative = '';
const prompt = gradioApp().querySelector(`#${type}_prompt > label > textarea`);
const result = Converter.convert(prompt.value);
prompt.value = result.match(/^masterpiece, best quality,/) === null ? default_prompt + result : result;
Converter.dispatchInputEvent(prompt);
const negprompt = gradioApp().querySelector(`#${type}_neg_prompt > label > textarea`);
const negResult = Converter.convert(negprompt.value);
negprompt.value =
negResult.match(/^lowres,/) === null
? negResult.length === 0
? default_negative
: default_negative + negResult
: negResult;
Converter.dispatchInputEvent(negprompt);
}
/**
* 创建转换按钮
* @param id 按钮 id
* @param innerHTML 按钮文本
* @param onClick 点击事件处理函数
* @returns 新建的按钮元素
*/
static createButton(id, innerHTML, onClick) {
const button = document.createElement('button');
button.id = id;
button.type = 'button';
button.innerHTML = innerHTML;
button.title = 'Format prompt~🪄';
button.className = 'lg secondary gradio-button tool svelte-1ipelgc';
button.addEventListener('click', onClick);
return button;
}
/**
* 添加转换按钮
* @param type - 组件类型
*/
static addPromptButton(type) {
const generateBtn = gradioApp().querySelector(`#${type}_generate`);
const actionsColumn = gradioApp().querySelector(`#${type}_style_create`);
const nai2local = gradioApp().querySelector(`#${type}_formatconvert`);
if (!generateBtn || !actionsColumn || nai2local)
return;
const convertBtn = Converter.createButton(`${type}_formatconvert`, '🪄', () => Converter.onClickConvert(type));
actionsColumn.parentNode?.append(convertBtn);
}
}
/**
* 注册UI更新回调函数
* 在UI更新时添加提示按钮
*/
onUiUpdate(() => {
Converter.addPromptButton('txt2img');
Converter.addPromptButton('img2img');
});

587
javascript/index.js Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,63 +0,0 @@
"use strict";
class BracketChecker {
textArea;
counterElt;
errorStrings;
constructor(textArea, counterElt) {
this.textArea = textArea;
this.counterElt = counterElt;
this.errorStrings = [
{
regex: '\\(',
error: '(...) - Different number of opening and closing parentheses detected.\n',
},
{
regex: '\\[',
error: '[...] - Different number of opening and closing square brackets detected.\n',
},
{
regex: '\\{',
error: '{...} - Different number of opening and closing curly brackets detected.\n',
},
];
}
/**
* 检查文本框中的括号是否匹配并更新计数器元素的标题和样式
*/
check = () => {
let title = '';
this.errorStrings.forEach(({ regex, error }) => {
const openMatches = (this.textArea.value.match(new RegExp(regex, 'g')) || []).length;
const closeMatches = (this.textArea.value.match(new RegExp(regex.replace(/\(/g, ')').replace(/\[/g, ']').replace(/\{/g, '}'), 'g')) ||
[]).length;
if (openMatches !== closeMatches) {
if (!this.counterElt.title.includes(error)) {
title += error;
}
}
else {
title = this.counterElt.title.replace(error, '');
}
});
this.counterElt.title = title;
this.counterElt.classList.toggle('error', !!title);
};
}
/**
* 初始化括号匹配检查器
* @param id_prompt 包含文本框的元素的 ID
* @param id_counter 显示计数器的元素的 ID
*/
const setupBracketChecking = (idPrompt, idCounter) => {
const textarea = gradioApp().querySelector(`#${idPrompt} > label > textarea`);
const counter = gradioApp().getElementById(idCounter);
const bracketChecker = new BracketChecker(textarea, counter);
textarea.addEventListener('input', bracketChecker.check);
};
onUiUpdate(() => {
const elements = ['txt2img', 'txt2img_neg', 'img2img', 'img2img_neg'];
elements.forEach((prompt) => {
setupBracketChecking(`${prompt}_prompt`, `${prompt}_token_counter`);
setupBracketChecking(`${prompt}_prompt`, `${prompt}_negative_token_counter`);
});
});

View File

@ -9,18 +9,16 @@
"license": "MIT",
"author": "canisminor1990 <i@canisminor.cc>",
"sideEffects": false,
"main": "style.css",
"scripts": {
"build": "gulp build",
"dev": "gulp watch",
"dev:with-sd": "concurrently \"gulp watch\" \"npm run sd-debug\"",
"build": "umi build",
"dev": "umi build",
"lint": "eslint \"{src,javascript}/**/*.{js,jsx,ts,tsx}\" --fix",
"prepare": "husky install",
"prettier": "prettier -c --write \"**/**\"",
"release": "semantic-release",
"sd-debug": "cd ../../ && ./webui.sh ",
"sd-debug": "cd ../../ && ./webui.sh",
"setup": "umi setup",
"stat": "npm run dev",
"start": "umi build",
"stylelint": "stylelint \"src/**/*.less\" --fix && stylelint \"./style.css\" --fix",
"test": "npm run lint",
"type-check": "tsc -p tsconfig-check.json"
@ -44,29 +42,43 @@
},
"dependencies": {},
"devDependencies": {
"@ant-design/icons": "^5.0.1",
"@commitlint/cli": "^17",
"@types/node": "^18",
"@types/react": "^18",
"@types/react-dom": "^18",
"@types/react-rnd": "^8.0.0",
"@types/styled-components": "^5.1.26",
"@umijs/lint": "^4.0.64",
"antd": "^5.4.2",
"antd-style": "^3.0.0",
"commitlint": "^17",
"commitlint-config-gitmoji": "^2",
"concurrently": "^8.0.1",
"eslint": "^8",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-import-resolver-typescript": "^3.5.5",
"gulp": "^4.0.2",
"gulp-less": "^5.0.0",
"gulp-typescript": "^6.0.0-alpha.1",
"husky": "^8.0.3",
"lint-staged": "^13.2.1",
"object-to-css-variables": "^0.2.1",
"polished": "^4.2.2",
"prettier": "^2",
"prettier-plugin-organize-imports": "^3",
"prettier-plugin-packagejson": "^2",
"query-string": "^8.1.0",
"re-resizable": "^6.9.9",
"react": "^18",
"react-dom": "^18",
"react-layout-kit": "^1.6.1",
"react-rnd": "^10.4.1",
"semantic-release": "^21",
"semantic-release-config-gitmoji": "^1",
"styled-components": "^5.3.9",
"stylelint": "^15.4.0",
"stylelint-less": "^1.0.6",
"typescript": "^5.0.0",
"umi": "^4.0.64"
"umi": "^4.0.64",
"use-merge-value": "^1.2.0",
"webpack-shell-plugin-next": "^2.3.1"
}
}

View File

@ -0,0 +1,171 @@
import type { NumberSize, Size } from 're-resizable'
import type { CSSProperties, FC, ReactNode } from 'react'
import { memo } from 'react'
import type { Props as RndProps } from 'react-rnd'
import { FixMode, placementType } from './FixMode'
import { FloatMode } from './FloatMode'
export interface DraggablePanelProps {
/**
*
* 使
*/
mode?: 'fixed' | 'float'
/**
*
* @default right
*/
placement?: placementType
/**
*
*/
minWidth?: number
/**
*
*/
minHeight?: number
/**
*
*/
resize?: RndProps['enableResizing']
/**
*
*
*/
size?: Partial<Size>
onSizeChange?: (delta: NumberSize, size?: Size) => void
/**
*
* @param delta
* @param size
*/
onSizeDragging?: (delta: NumberSize, size?: Size) => void
/**
*
* @default true
*/
expandable?: boolean
/**
*
*/
isExpand?: boolean
/**
*
* @param expand
*/
onExpandChange?: (expand: boolean) => void
/**
*
*
*/
position?: RndProps['position']
/**
*
* width 320px height 100%
* width 320px height 400px
*/
defaultSize?: Partial<Size>
/**
*
* @default [100,100]
*/
defaultPosition?: RndProps['position']
/**
*
*/
onPositionChange?: (position: RndProps['position']) => void
/**
*
*/
style?: CSSProperties
/**
*
*/
className?: string
/**
*
*/
children: ReactNode
/**
*
*/
prefixCls?: string
}
export const Draggable: FC<DraggablePanelProps> = memo(
({
children,
className,
mode,
placement = 'right',
resize,
style,
position,
onPositionChange,
size,
defaultSize,
defaultPosition,
minWidth,
minHeight,
onSizeChange,
onSizeDragging,
expandable = true,
isExpand,
onExpandChange,
}) => {
const prefixCls = 'draggable-panel'
switch (mode) {
case 'fixed':
default:
return (
<FixMode
prefixCls={prefixCls}
// 尺寸
size={size}
defaultSize={defaultSize}
onSizeDragging={onSizeDragging}
onSizeChange={onSizeChange}
minHeight={minHeight}
minWidth={minWidth}
// 缩放
resize={resize}
onExpandChange={onExpandChange}
expandable={expandable}
isExpand={isExpand}
className={className}
placement={placement}
style={style}
>
{children}
</FixMode>
)
case 'float':
return (
<FloatMode
prefixCls={prefixCls}
// 坐标
defaultPosition={defaultPosition}
position={position}
onPositionChange={onPositionChange}
// 尺寸
minHeight={minHeight}
minWidth={minWidth}
defaultSize={defaultSize}
size={size}
onSizeDragging={onSizeDragging}
onSizeChange={onSizeChange}
// 缩放
resize={resize}
canResizing={resize !== false}
className={className}
style={style}
>
{children}
</FloatMode>
)
}
}
)

View File

@ -0,0 +1,248 @@
import { DownOutlined, LeftOutlined, RightOutlined, UpOutlined } from '@ant-design/icons'
import type { Enable, NumberSize, Size } from 're-resizable'
import { HandleClassName, Resizable } from 're-resizable'
import type { CSSProperties, FC, ReactNode } from 'react'
import { memo, useMemo } from 'react'
import { Center } from 'react-layout-kit'
import type { Props as RndProps } from 'react-rnd'
import useControlledState from 'use-merge-value'
import { useStyle } from './style'
export type placementType = 'right' | 'left' | 'top' | 'bottom'
export interface FixModePanelProps {
/**
*
* 使
*/
mode?: 'fixed' | 'float'
/**
*
* @default right
*/
placement: placementType
/**
*
*/
minWidth?: number
/**
*
*/
minHeight?: number
/**
*
*/
resize?: RndProps['enableResizing']
/**
*
*
*/
size?: Partial<Size>
onSizeChange?: (delta: NumberSize, size?: Size) => void
/**
*
* @param delta
* @param size
*/
onSizeDragging?: (delta: NumberSize, size?: Size) => void
/**
*
* @default true
*/
expandable?: boolean
/**
*
*/
isExpand?: boolean
/**
*
* @param expand
*/
onExpandChange?: (expand: boolean) => void
/**
*
*
*/
position?: RndProps['position']
/**
*
* width 320px height 100%
* width 320px height 400px
*/
defaultSize?: Partial<Size>
/**
*
* @default [100,100]
*/
defaultPosition?: RndProps['position']
/**
*
*/
onPositionChange?: (position: RndProps['position']) => void
/**
*
*/
style?: CSSProperties
className?: string
/**
*
*/
children: ReactNode
/**
*
*/
prefixCls?: string
}
const DEFAULT_HEIGHT = 150
const DEFAULT_WIDTH = 400
const revesePlacement = (placement: placementType) => {
switch (placement) {
case 'bottom':
return 'top'
case 'top':
return 'bottom'
case 'right':
return 'left'
case 'left':
return 'right'
}
}
export const FixMode: FC<FixModePanelProps> = memo<FixModePanelProps>(
({
children,
placement = 'right',
resize,
style,
size,
defaultSize: customizeDefaultSize,
minWidth,
minHeight,
onSizeChange,
onSizeDragging,
expandable = true,
isExpand: expand,
onExpandChange,
className,
}) => {
const prefixCls = 'draggable-panel'
const isVertical = placement === 'top' || placement === 'bottom'
const { styles, cx } = useStyle(prefixCls)
const [isExpand, setIsExpand] = useControlledState(true, {
value: expand,
onChange: onExpandChange,
})
// 只有配置了 resize 和 isExpand 属性后才可拖拽
const canResizing = resize !== false && isExpand
const resizeHandleClassNames: HandleClassName = useMemo(() => {
if (!canResizing) return {}
return {
[revesePlacement(placement)]: styles[`${revesePlacement(placement)}Handle`],
}
}, [canResizing, placement])
const resizing = {
top: false,
bottom: false,
right: false,
left: false,
topRight: false,
bottomRight: false,
bottomLeft: false,
topLeft: false,
[revesePlacement(placement)]: true,
...(resize as Enable),
}
const defaultSize: Size = useMemo(() => {
if (isVertical)
return {
width: '100%',
height: DEFAULT_HEIGHT,
...customizeDefaultSize,
}
return {
width: DEFAULT_WIDTH,
height: '100%',
...customizeDefaultSize,
}
}, [isVertical])
const sizeProps = isExpand
? {
minWidth: typeof minWidth === 'number' ? Math.max(minWidth, 0) : 280,
minHeight: typeof minHeight === 'number' ? Math.max(minHeight, 0) : undefined,
defaultSize,
size: size as Size,
style,
}
: {
minWidth: 0,
minHeight: 0,
size: { width: 0, height: 0 },
}
const { Arrow, className: arrowPlacement } = useMemo(() => {
switch (placement) {
case 'top':
return { className: 'Bottom', Arrow: DownOutlined }
case 'bottom':
return { className: 'Top', Arrow: UpOutlined }
case 'right':
return { className: 'Left', Arrow: LeftOutlined }
case 'left':
return { className: 'Right', Arrow: RightOutlined }
}
}, [styles, placement])
return (
<div className={cx(styles.container, className)} style={{ [`border${arrowPlacement}Width`]: 1 }}>
{expandable && (
<Center
// @ts-ignore
className={cx(styles[`toggle${arrowPlacement}`])}
onClick={() => {
setIsExpand(!isExpand)
}}
style={{ opacity: isExpand ? undefined : 1 }}
>
<Arrow rotate={isExpand ? 180 : 0} />
</Center>
)}
{
<Resizable
{...sizeProps}
className={styles.fixed}
enable={canResizing ? (resizing as Enable) : undefined}
handleClasses={resizeHandleClassNames}
onResizeStop={(e, direction, ref, delta) => {
onSizeChange?.(delta, {
width: ref.style.width,
height: ref.style.height,
})
}}
onResize={(_, direction, ref, delta) => {
onSizeDragging?.(delta, {
width: ref.style.width,
height: ref.style.height,
})
}}
>
{children}
</Resizable>
}
</div>
)
}
)

View File

@ -0,0 +1,174 @@
import type { Enable, NumberSize, Size } from 're-resizable'
import { HandleClassName } from 're-resizable'
import type { CSSProperties, FC, ReactNode } from 'react'
import { memo, useMemo } from 'react'
import type { Position, Props as RndProps } from 'react-rnd'
import { Rnd } from 'react-rnd'
import { useStyle } from './style'
export interface FloatProps {
/**
*
* 使
*/
mode?: 'fixed' | 'float'
/**
*
* @default horizontal
*/
direction?: 'vertical' | 'horizontal'
/**
*
*/
minWidth?: number
/**
*
*/
minHeight?: number
/**
*
*/
resize?: RndProps['enableResizing']
/**
*
*
*/
size?: Partial<Size>
onSizeChange?: (delta: NumberSize, size?: Size) => void
/**
*
* @param delta
* @param size
*/
onSizeDragging?: (delta: NumberSize, size?: Size) => void
canResizing?: boolean
/**
*
*
*/
position?: RndProps['position']
/**
*
* width 320px height 100%
* width 320px height 400px
*/
defaultSize?: Partial<Size>
/**
*
* @default [100,100]
*/
defaultPosition?: RndProps['position']
/**
*
*/
onPositionChange?: (position: RndProps['position']) => void
/**
*
*/
style?: CSSProperties
/**
*
*/
className?: string
/**
*
*/
children: ReactNode
/**
*
*/
prefixCls?: string
}
const DEFAULT_HEIGHT = 300
const DEFAULT_WIDTH = 400
export const FloatMode: FC<FloatProps> = memo(
({
children,
direction,
resize,
style,
position,
onPositionChange,
size,
defaultSize: customizeDefaultSize,
defaultPosition: customizeDefaultPosition,
minWidth = 280,
minHeight = 200,
prefixCls,
canResizing,
}) => {
const { styles } = useStyle(prefixCls)
const resizeHandleClassNames: HandleClassName = useMemo(() => {
if (!canResizing) return {}
return {
right: styles.rightHandle,
left: styles.leftHandle,
top: styles.topHandle,
bottom: styles.bottomHandle,
}
}, [canResizing, direction])
const resizing = useMemo(() => {
if (canResizing) return resize
return {
top: true,
bottom: true,
right: true,
left: true,
topRight: true,
bottomRight: true,
bottomLeft: true,
topLeft: true,
...(resize as Enable),
}
}, [canResizing, resize])
const defaultSize: Size = {
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
...customizeDefaultSize,
}
const defaultPosition: Position = {
x: 100,
y: 100,
...customizeDefaultPosition,
}
const sizeProps = {
minWidth: Math.max(minWidth, 0),
minHeight: Math.max(minHeight, 0),
defaultSize,
size: size as Size,
style,
}
return (
<Rnd
position={position}
resizeHandleClasses={resizeHandleClassNames}
default={{
...defaultPosition,
...defaultSize,
}}
onDragStop={(e, data) => {
onPositionChange?.({ x: data.x, y: data.y })
}}
bound={'parent'}
enableResizing={resizing}
{...sizeProps}
className={styles.float}
>
{children}
</Rnd>
)
}
)

View File

@ -0,0 +1,3 @@
export type { Position } from 'react-rnd'
export { Draggable as DraggablePanel } from './DraggablePanel'
export type { DraggablePanelProps } from './DraggablePanel'

View File

@ -0,0 +1,177 @@
import { createStyles, css, cx } from 'antd-style'
import { rgba } from 'polished'
export const useStyle = createStyles(({ token }, prefix: string) => {
const commonHandle = css`
position: relative;
&::before {
position: absolute;
z-index: 50;
transition: all 0.3s ease-in-out;
content: '';
}
&:hover,
&:active {
&::before {
background: ${token.colorPrimary};
}
}
`
const commonToggle = css`
position: absolute;
opacity: 0;
z-index: 1001;
transition: opacity 0.1s;
border-radius: 4px;
cursor: pointer;
background: ${token.colorBgElevated};
border-width: 1px;
border-style: solid;
color: ${token.colorTextTertiary};
border-color: ${token.colorBorder};
&:hover {
color: ${token.colorTextSecondary};
background: ${token.colorFillQuaternary};
}
`
const offset = 17
const toggleLength = 40
const toggleShort = 16
return {
container: cx(
prefix,
css`
flex-shrink: 0;
position: relative;
border: 0 solid ${token.colorSplit};
&:hover {
.${prefix}-toggle {
opacity: 1;
}
}
`
),
toggleLeft: cx(
`${prefix}-toggle`,
`${prefix}-toggle-left`,
commonToggle,
css`
width: ${toggleShort}px;
height: ${toggleLength}px;
left: -${offset}px;
top: 50%;
margin-top: -20px;
border-radius: 4px 0 0 4px;
border-right-width: 0;
`
),
toggleRight: cx(
`${prefix}-toggle`,
`${prefix}-toggle-right`,
commonToggle,
css`
width: ${toggleShort}px;
height: ${toggleLength}px;
right: -${offset}px;
top: 50%;
margin-top: -20px;
border-radius: 0 4px 4px 0;
border-left-width: 0;
`
),
toggleTop: cx(
`${prefix}-toggle`,
`${prefix}-toggle-top`,
commonToggle,
css`
height: ${toggleShort}px;
width: ${toggleLength}px;
top: -${offset}px;
left: 50%;
margin-left: -20px;
border-radius: 4px 4px 0 0;
border-bottom-width: 0;
`
),
toggleBottom: cx(
`${prefix}-toggle`,
`${prefix}-toggle-bottom`,
commonToggle,
css`
height: 16px;
width: ${toggleLength}px;
bottom: -${offset}px;
left: 50%;
margin-left: -20px;
border-radius: 0 0 4px 4px;
border-top-width: 0;
`
),
fixed: cx(
`${prefix}-fixed`,
css`
background: ${rgba(token.colorBgContainer, 0.75)};
backdrop-filter: blur(40px);
overflow: hidden;
`
),
float: cx(
`${prefix}-float`,
css`
overflow: hidden;
border-radius: 8px;
background: ${rgba(token.colorBgElevated, 0.75)};
backdrop-filter: blur(40px);
box-shadow: ${token.boxShadowSecondary};
z-index: 2000;
`
),
leftHandle: cx(
css`
${commonHandle};
&::before {
left: 50%;
width: 2px;
height: 100%;
}
`,
`${prefix}-left-handle`
),
rightHandle: cx(
css`
${commonHandle};
&::before {
right: 50%;
width: 2px;
height: 100%;
}
`,
`${prefix}-right-handle`
),
topHandle: cx(
`${prefix}-top-handle`,
css`
${commonHandle};
&::before {
top: 50%;
height: 2px;
width: 100%;
}
`
),
bottomHandle: cx(
`${prefix}-bottom-handle`,
css`
${commonHandle};
&::before {
bottom: 50%;
height: 2px;
width: 100%;
}
`
),
}
})

View File

@ -0,0 +1,14 @@
import React from 'react'
import { darkLogo, lightLogo } from './style'
interface LogoProps {
size?: number
style?: React.CSSProperties
themeMode: 'dark' | 'light'
}
const Logo: React.FC<LogoProps> = ({ size = 20, style, themeMode }) => {
return <img src={themeMode === 'dark' ? darkLogo : lightLogo} alt="logo" style={{ height: size, ...style }} />
}
export default React.memo(Logo)

View File

@ -0,0 +1,78 @@
import { GithubOutlined } from '@ant-design/icons'
import { Button, Space } from 'antd'
import { rgba } from 'polished'
import qs from 'query-string'
import React, { useCallback } from 'react'
import styled from 'styled-components'
import Logo from './Logo'
import { themeIcon } from './style'
const HeaderView = styled.div`
padding: 16px 24px;
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between;
background: ${({ theme }) => rgba(theme.colorBgContainer, 0.5)};
border-bottom: 1px solid ${({ theme }) => theme.colorBorderSecondary};
backdrop-filter: blur(40px);
#header {
.tab-nav {
border: none !important;
margin: 0 !important;
}
button {
cursor: pointer;
border: none !important;
background: transparent !important;
flex: none;
transition: all 0.2s ease-in-out;
padding: 8px !important;
border-radius: 4px !important;
margin: 0 !important;
flex: 0 !important;
&:hover {
border: none !important;
color: var(--color-text) !important;
background: var(--color-fill-tertiary) !important;
flex: none;
}
&.selected {
border: none !important;
background: transparent !important;
color: var(--color-text) !important;
flex: none;
font-weight: 600;
}
}
}
`
interface HeaderProps {
children: React.ReactNode
themeMode: 'dark' | 'light'
}
const Header: React.FC<HeaderProps> = ({ children, themeMode }) => {
const handleSetTheme = useCallback(() => {
const theme = themeMode === 'light' ? 'dark' : 'light'
const gradioURL = qs.parseUrl(window.location.href)
gradioURL.query.__theme = theme
window.location.replace(qs.stringifyUrl(gradioURL))
}, [themeMode])
return (
<HeaderView>
<Logo themeMode={themeMode} style={{ paddingRight: 16 }} />
{children}
<Space.Compact>
<a href="https://github.com/canisminor1990/sd-web-ui-kitchen-theme" target="_blank">
<Button icon={<GithubOutlined />} />
</a>
<Button icon={themeIcon[themeMode]} onClick={handleSetTheme} />
</Space.Compact>
</HeaderView>
)
}
export default React.memo(Header)

View File

@ -0,0 +1,19 @@
export const themeIcon = {
light: (
<span role="img" className="anticon anticon-github">
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor">
<path d="M8 13a1 1 0 0 1 1 1v1a1 1 0 1 1-2 0v-1a1 1 0 0 1 1-1ZM8 3a1 1 0 0 1-1-1V1a1 1 0 1 1 2 0v1a1 1 0 0 1-1 1Zm7 4a1 1 0 1 1 0 2h-1a1 1 0 1 1 0-2h1ZM3 8a1 1 0 0 1-1 1H1a1 1 0 1 1 0-2h1a1 1 0 0 1 1 1Zm9.95 3.536.707.707a1 1 0 0 1-1.414 1.414l-.707-.707a1 1 0 0 1 1.414-1.414Zm-9.9-7.072-.707-.707a1 1 0 0 1 1.414-1.414l.707.707A1 1 0 0 1 3.05 4.464Zm9.9 0a1 1 0 0 1-1.414-1.414l.707-.707a1 1 0 0 1 1.414 1.414l-.707.707Zm-9.9 7.072a1 1 0 0 1 1.414 1.414l-.707.707a1 1 0 0 1-1.414-1.414l.707-.707ZM8 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8Zm0 6.5a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"></path>
</svg>
</span>
),
dark: (
<span role="img" className="anticon anticon-github">
<svg viewBox="0 0 16 16" width="1em" height="1em" fill="currentColor">
<path d="M8.218 1.455c3.527.109 6.327 3.018 6.327 6.545 0 3.6-2.945 6.545-6.545 6.545a6.562 6.562 0 0 1-6.036-4h.218c3.6 0 6.545-2.945 6.545-6.545 0-.91-.182-1.745-.509-2.545m0-1.455c-.473 0-.909.218-1.2.618-.29.4-.327.946-.145 1.382.254.655.4 1.31.4 2 0 2.8-2.291 5.09-5.091 5.09h-.218c-.473 0-.91.22-1.2.62-.291.4-.328.945-.146 1.38C1.891 14.074 4.764 16 8 16c4.4 0 8-3.6 8-8a7.972 7.972 0 0 0-7.745-8h-.037Z"></path>
</svg>
</span>
),
}
export const darkLogo = 'https://gw.alipayobjects.com/zos/bmw-prod/9ecb2822-1592-4cb0-a087-ce0097fef2ca.svg'
export const lightLogo = 'https://gw.alipayobjects.com/zos/bmw-prod/e146116d-c65a-4306-a3d2-bb8d05e1c49b.svg'

View File

@ -0,0 +1,49 @@
import { DraggablePanel } from '@/components'
import React from 'react'
import styled from 'styled-components'
const SidebarView = styled.div`
padding: 16px;
#quicksettings {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: stretch;
> * {
flex: 1;
max-width: unset !important;
min-width: unset !important;
width: 100%;
margin: 0;
padding: 0;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.dropdown-arrow {
min-width: 16px;
min-height: 16px;
}
}
`
interface SidebarProps {
children: React.ReactNode
}
const Sidebar: React.FC<SidebarProps> = ({ children }) => {
return (
<DraggablePanel placement="right" defaultSize={{ width: 280 }}>
<SidebarView>{children}</SidebarView>
</DraggablePanel>
)
}
export default React.memo(Sidebar)

3
src/components/index.tsx Normal file
View File

@ -0,0 +1,3 @@
export * from './DraggablePanel'
export { default as Header } from './Header'
export { default as Sidebar } from './Sidebar'

62
src/pages/index/App.tsx Normal file
View File

@ -0,0 +1,62 @@
import { Header, Sidebar } from '@/components'
import React, { useEffect, useRef } from 'react'
import styled from 'styled-components'
const View = styled.div`
width: 100vw;
height: 100vh;
display: flex;
flex-direction: row !important;
`
const MainView = styled.div`
flex: 1;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
`
const Content = styled.div`
overflow-x: hidden;
overflow-y: auto;
flex: 1;
`
interface AppProps {
themeMode: 'light' | 'dark'
}
const App: React.FC<AppProps> = ({ themeMode }) => {
const sidebarRef: any = useRef<HTMLElement>()
const mainRef: any = useRef<HTMLElement>()
const headerRef: any = useRef<HTMLElement>()
useEffect(() => {
onUiLoaded(() => {
const sidebar = gradioApp().querySelector('#quicksettings')
const header = gradioApp().querySelector('#tabs > .tab-nav:first-child')
const main = gradioApp().querySelector('.app')
if (sidebar) sidebarRef.current?.appendChild(sidebar)
if (header) headerRef.current?.appendChild(header)
if (main) mainRef.current?.appendChild(main)
})
}, [])
return (
<View>
<MainView>
<Header themeMode={themeMode}>
<div id="header" ref={headerRef} />
</Header>
<Content>
<div ref={mainRef} />
</Content>
</MainView>
<Sidebar>
<div ref={sidebarRef} />
</Sidebar>
</View>
)
}
export default React.memo(App)

45
src/pages/index/index.tsx Normal file
View File

@ -0,0 +1,45 @@
import favicon from '@/script/favicon'
import formatPrompt from '@/script/format-prompt'
import promptBracketChecker from '@/script/prompt-bracket-checker'
import '@/theme/style.less'
import { ThemeProvider, setupStyled } from 'antd-style'
import qs from 'query-string'
import React, { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'
import { ThemeContext } from 'styled-components'
import App from './App'
const Root: React.FC = () => {
setupStyled({ ThemeContext })
const [appearance, setAppearance] = useState<'light' | 'dark'>('light')
useEffect(() => {
const themeMode: any = String(qs.parseUrl(window.location.href).query.__theme) || 'light'
setAppearance(themeMode)
document.body.classList.add(themeMode)
}, [])
return (
<ThemeProvider appearance={appearance}>
<App themeMode={appearance} />
</ThemeProvider>
)
}
document.addEventListener('DOMContentLoaded', () => {
const root = document.createElement('div')
root.setAttribute('id', 'root')
gradioApp().append(root)
const client = createRoot(root)
client.render(<Root />)
})
onUiLoaded(() => {
favicon()
})
onUiUpdate(() => {
formatPrompt()
promptBracketChecker()
})
export default () => null

View File

@ -14,6 +14,8 @@ class FaviconHandler {
}
}
onUiLoaded(() => {
onUiLoaded(() => {})
export default () => {
FaviconHandler.setFavicon()
})
}

View File

@ -269,7 +269,9 @@ class Converter {
* UI更新回调函数
* UI更新时添加提示按钮
*/
onUiUpdate(() => {
onUiUpdate(() => {})
export default () => {
Converter.addPromptButton('txt2img')
Converter.addPromptButton('img2img')
})
}

View File

@ -63,10 +63,12 @@ const setupBracketChecking = (idPrompt: string, idCounter: string): void => {
textarea.addEventListener('input', bracketChecker.check)
}
onUiUpdate(() => {
onUiUpdate(() => {})
export default () => {
const elements = ['txt2img', 'txt2img_neg', 'img2img', 'img2img_neg']
elements.forEach((prompt) => {
setupBracketChecking(`${prompt}_prompt`, `${prompt}_token_counter`)
setupBracketChecking(`${prompt}_prompt`, `${prompt}_negative_token_counter`)
})
})
}

View File

@ -1,6 +1,48 @@
.tabitem,
.gradio-tabitem {
background: var(--color-fill-quaternary);
border: none !important;
border-radius: var(--container-radius);
background: var(--panel-background-fill);
padding: var(--spacing-lg);
margin-bottom: var(--spacing-lg);
}
#tabs {
> .tabitem,
> .gradio-tabitem {
background: transparent !important;
padding: 0 !important;
}
}
.tab-nav {
border: none !important;
gap: 8px;
margin-bottom: 8px;
button {
cursor: pointer;
border: none !important;
background: var(--color-fill-quaternary) !important;
flex: none;
transition: all 0.2s ease-in-out;
padding: 8px !important;
border-radius: 4px !important;
flex: 1 !important;
&:hover {
border: none !important;
color: var(--color-text) !important;
background: var(--color-fill-tertiary) !important;
flex: none;
}
&.selected {
border: none !important;
background: var(--color-fill-secondary) !important;
color: var(--color-text) !important;
flex: none;
font-weight: 600;
}
}
}
.selected.svelte-1g805jl {
@ -10,4 +52,41 @@
[id$='2img_tools'] > div {
display: flex;
justify-content: center;
button {
max-width: unset !important;
}
}
.image-buttons button {
min-width: min(160px,100%) !important;
}
#img2img_label_copy_to_img2img {
display: none;
}
#img2img_copy_to_img2img, .gap.compact, .image-buttons, .image_buttons_extras {
gap: 8px !important;
}
.padded.svelte-mppz8v {
padding: 6px;
}
.wrap.svelte-1p9xokt.svelte-1p9xokt.svelte-1p9xokt {
gap: 8px !important;
> label {
flex: 1 !important;
white-space: nowrap;
border-radius: var(--border-radius)!important;
}
}
[id$="_settings"] {
div.svelte-15lo0d8>*, div.svelte-15lo0d8>.form>* {
min-width: unset !important;
flex: 1;
}
}

View File

@ -1,101 +0,0 @@
#quicksettings {
position: fixed;
z-index: 1000;
flex-wrap: nowrap;
top: 12px;
display: flex;
align-items: center;
> * {
@media screen and (max-width: 640px) {
display: none;
}
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
&::before {
content: '';
display: block;
background: var(--logo) no-repeat;
width: 129px;
height: 26px;
z-index: 1000;
margin-right: 36px;
margin-left: 16px;
margin-top: 12px;
}
.dropdown-arrow {
min-width: 16px;
min-height: 16px;
}
> div,
> fieldset {
min-width: 240px !important;
@media screen and (max-width: 640px) {
max-width: 160px;
}
}
button.svelte-1ipelgc {
margin-bottom: 3px;
}
}
#tabs {
> .tabitem {
background: transparent;
border: none;
padding: var(--size-lg);
margin-top: 118px;
}
> .tab-nav:first-child {
position: fixed;
top: 90px;
z-index: 999;
flex-wrap: nowrap;
overflow-y: auto;
width: 100%;
border: none;
&::before {
content: '';
display: block;
position: fixed;
width: 100vw;
height: 121px;
top: 0;
left: 0;
border-block-end: 1px solid var(--color-border-secondary);
background: var(--color-header);
backdrop-filter: blur(24px);
z-index: -1;
}
> button {
border: none;
border-bottom: 3px solid transparent !important;
flex: none;
transition: all 0.2s ease-in-out;
&:hover {
border: none;
border-bottom: 3px solid var(--color-primary) !important;
flex: none;
}
&.selected {
background: transparent;
border: none;
border-bottom: 3px solid var(--color-primary) !important;
}
}
}
}

View File

@ -2,9 +2,12 @@ ul.options {
border: 2px solid var(--color-border) !important;
border-radius: var(--border-radius) !important;
display: block !important;
padding: 0 !important;
margin: 0 !important;
background: var(--color-bg-elevated) !important;
li {
display: block !important;
margin: 0 !important;
&.selected {
background: var(--color-primary) !important;
color: #fff !important;

View File

@ -0,0 +1,8 @@
.prose {
> p {
border: var(--input-border-width) solid var(--color-primary-border);
border-radius: var(--input-radius);
background: var(--color-primary-bg);
padding: 12px;
}
}

View File

@ -1,7 +1,7 @@
/* width */
::-webkit-scrollbar {
width: 2px;
height: 2px;
width: 0;
height: 0;
}
/* Track */

View File

@ -96,3 +96,7 @@ input[type='range'] {
background: var(--color-bg-elevated) !important;
}
}
.gradio-slider input[type="number"] {
padding: var(--spacing-sm) !important;
}

View File

@ -9,9 +9,9 @@
/* Components */
@import 'components/container';
@import 'components/prose';
@import 'components/scrollbar';
@import 'components/options';
@import 'components/header';
@import 'components/modal';
@import 'components/sliders';
@import 'components/button';
@ -35,8 +35,11 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-size-adjust: 100%;
background-image: var(--color-body-background);
background-repeat: no-repeat;
}
code {
font-family: var(--font-family-code);
}
@ -44,22 +47,33 @@ code {
h1 {
font-size: var(--font-size-heading1);
line-height: var(--line-height-heading1);
color: var(--color-text);
}
h2 {
font-size: var(--font-size-heading2);
line-height: var(--line-height-heading2);
color: var(--color-text);
}
h3 {
font-size: var(--font-size-heading3);
line-height: var(--line-height-heading3);
color: var(--color-text);
}
h4 {
font-size: var(--font-size-heading4);
line-height: var(--line-height-heading4);
color: var(--color-text);
}
h5 {
font-size: var(--font-size-heading5);
line-height: var(--line-height-heading5);
color: var(--color-text);
}
.dark, .light {
div {
color: var(--color-text);
}
}
/* Theme Fix */
@ -67,8 +81,7 @@ h5 {
font-size: var(--font-size);
color: var(--color-text);
margin: 0;
background-image: var(--color-body-background);
background-repeat: no-repeat;
background: transparent !important;
}
#txtimg_hr_finalres {
@ -77,5 +90,6 @@ h5 {
#interrogate,
#deepbooru {
max-height: 72px;
display: block !important;
}

View File

@ -189,8 +189,8 @@
--checkbox-label-text-color: var(--body-text-color);
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
--checkbox-border-radius: var(--radius-sm);
--checkbox-label-gap: var(--spacing-lg);
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
--checkbox-label-gap: var(--spacing-sm);
--checkbox-label-padding: var(--spacing-sm);
--checkbox-label-text-size: var(--text-md);
--checkbox-shadow: var(--input-shadow);
@ -205,8 +205,8 @@
--input-border-color-focus: var(--neutral-700);
--input-border-color-hover: var(--input-border-color);
--input-placeholder-color: var(--neutral-500);
--input-padding: var(--spacing-xl);
--input-radius: var(--radius-lg);
--input-padding: var(--spacing-sm);
--input-radius: var(--radius-sm);
--input-shadow-focus: var(--input-shadow);
--input-text-size: var(--text-md);

View File

@ -189,8 +189,8 @@
--checkbox-label-text-color: var(--body-text-color);
--checkbox-label-text-color-selected: var(--checkbox-label-text-color);
--checkbox-border-radius: var(--radius-sm);
--checkbox-label-gap: var(--spacing-lg);
--checkbox-label-padding: var(--spacing-md) calc(2 * var(--spacing-md));
--checkbox-label-gap: var(--spacing-sm);
--checkbox-label-padding: var(--spacing-sm);
--checkbox-label-text-size: var(--text-md);
--checkbox-shadow: var(--input-shadow);
@ -205,11 +205,12 @@
--input-border-color-focus: var(--neutral-700);
--input-border-color-hover: var(--input-border-color);
--input-placeholder-color: var(--neutral-500);
--input-padding: var(--spacing-xl);
--input-radius: var(--radius-lg);
--input-padding: var(--spacing-sm);
--input-radius: var(--radius-sm);
--input-shadow-focus: var(--input-shadow);
--input-text-size: var(--text-md);
/* Table */
--table-border-color: var(--neutral-700);
--table-even-background-fill: var(--neutral-950);

1951
style.css

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,5 @@
{
"extends": "./src/.umi-production/tsconfig.json",
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
@ -14,7 +15,11 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"]
"include": ["src"],
"exclude": ["javascript"]
}