♻️ refactor: Refactor inject with react hook (#489)

* ♻️ refactor: Refactor inject with react hook

* ♻️ refactor: Refactor inject with react hook

* ♻️ refactor: Refactor inject
pull/484/head
CanisMinor 2023-12-12 23:50:23 +08:00 committed by GitHub
parent d864e39ce0
commit c376aa6c29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 800 additions and 691 deletions

View File

@ -3,5 +3,4 @@
git add . git add .
npx --no-install lint-staged npx --no-install lint-staged
npm run test git add .
git add .

File diff suppressed because one or more lines are too long

View File

@ -89,7 +89,7 @@
"react-rnd": "^10", "react-rnd": "^10",
"react-tag-input": "^6", "react-tag-input": "^6",
"semver": "^7", "semver": "^7",
"shikiji": "^0.7", "shikiji": "^0.8",
"swr": "^2", "swr": "^2",
"zustand": "^4.4.1", "zustand": "^4.4.1",
"zustand-utils": "^1.3.1" "zustand-utils": "^1.3.1"
@ -97,6 +97,8 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^18", "@commitlint/cli": "^18",
"@lobehub/lint": "latest", "@lobehub/lint": "latest",
"@testing-library/jest-dom": "^6",
"@testing-library/react": "^14",
"@types/lodash-es": "^4", "@types/lodash-es": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^18", "@types/react": "^18",
@ -105,7 +107,7 @@
"@types/react-tag-input": "^6", "@types/react-tag-input": "^6",
"@types/semver": "^7", "@types/semver": "^7",
"@vitejs/plugin-react-swc": "^3", "@vitejs/plugin-react-swc": "^3",
"@vitest/coverage-v8": "latest", "@vitest/coverage-v8": "^1",
"commitlint": "^18", "commitlint": "^18",
"dotenv": "^16", "dotenv": "^16",
"eslint": "^8", "eslint": "^8",

View File

@ -2,6 +2,7 @@ import { LayoutHeader, LayoutMain, LayoutSidebar } from '@lobehub/ui';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { memo, useEffect } from 'react'; import { memo, useEffect } from 'react';
import PromptFormator from '@/features/PromptFormator';
import '@/locales/config'; import '@/locales/config';
import ImageInfo from '@/modules/ImageInfo/page'; import ImageInfo from '@/modules/ImageInfo/page';
import PromptHighlight from '@/modules/PromptHighlight/page'; import PromptHighlight from '@/modules/PromptHighlight/page';
@ -50,6 +51,7 @@ const Index = memo(() => {
</LayoutSidebar> </LayoutSidebar>
)} )}
<Content className={cx(!setting.enableSidebar && styles.quicksettings)} /> <Content className={cx(!setting.enableSidebar && styles.quicksettings)} />
<PromptFormator />
<Share /> <Share />
{setting?.enableExtraNetworkSidebar && ( {setting?.enableExtraNetworkSidebar && (
<LayoutSidebar <LayoutSidebar

View File

@ -7,15 +7,22 @@ import {
type ModalProps, type ModalProps,
} from '@lobehub/ui'; } from '@lobehub/ui';
import { Button } from 'antd'; import { Button } from 'antd';
import { useTheme } from 'antd-style'; import { useTheme, useThemeMode } from 'antd-style';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { Github } from 'lucide-react'; import { Github, Heart } from 'lucide-react';
import { memo } from 'react'; import { memo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Center, Flexbox } from 'react-layout-kit'; import { Center, Flexbox } from 'react-layout-kit';
import { homepage } from '@/../package.json';
import VersionTag from '@/components/VersionTag'; import VersionTag from '@/components/VersionTag';
import {
DISCORD_URL,
GISCUS_REPO_ID,
GITHUB_REPO_URL,
REPO_NAME,
SPONSOR_IMG,
SPONSOR_URL,
} from '@/const/url';
import { selectors, useAppStore } from '@/store'; import { selectors, useAppStore } from '@/store';
export interface GiscusProps { export interface GiscusProps {
@ -23,11 +30,10 @@ export interface GiscusProps {
open?: ModalProps['open']; open?: ModalProps['open'];
} }
const repoName = homepage.replace('https://github.com/', '') as `${string}/${string}`;
const Giscus = memo<GiscusProps>(({ open, onCancel }) => { const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
const setting = useAppStore(selectors.currentSetting, isEqual); const setting = useAppStore(selectors.currentSetting, isEqual);
const theme = useTheme(); const theme = useTheme();
const { isDarkMode } = useThemeMode();
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Modal <Modal
@ -44,7 +50,6 @@ const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
<Flexbox gap={32}> <Flexbox gap={32}>
<Center <Center
gap={16} gap={16}
horizontal
style={{ style={{
background: theme.colorBgLayout, background: theme.colorBgLayout,
border: `1px solid ${theme.colorBorderSecondary}`, border: `1px solid ${theme.colorBorderSecondary}`,
@ -52,16 +57,32 @@ const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
padding: '16px 0', padding: '16px 0',
}} }}
> >
<a href={'https://discord.gg/AYFPHvv2jT'} rel="noreferrer" target="_blank"> <Flexbox gap={16} horizontal>
<Button icon={<Icon icon={DiscordIcon} />} size={'large'}> <a href={DISCORD_URL} rel="noreferrer" target="_blank">
Join Discover <Button icon={<Icon icon={DiscordIcon} />} size={'large'}>
</Button> Join Discover
</a> </Button>
<a href={homepage} rel="noreferrer" target="_blank"> </a>
<GradientButton icon={<Icon icon={Github} />}>LobeTheme Github</GradientButton> <a href={GITHUB_REPO_URL} rel="noreferrer" target="_blank">
<Button icon={<Icon icon={Github} />} size={'large'}>
Github
</Button>
</a>
<a href={SPONSOR_URL} rel="noreferrer" target="_blank">
<GradientButton icon={<Icon icon={Heart} />}>Sponsor</GradientButton>
</a>
</Flexbox>
<a href={SPONSOR_URL} rel="noreferrer" target="_blank">
<img alt={'sponsor'} src={`${SPONSOR_IMG}${isDarkMode ? '?themeMode=dark' : ''}`} />
</a> </a>
</Center> </Center>
<G lang={setting.i18n} mapping="number" repo={repoName} repoId="R_kgDOJCPcNg" term="53" /> <G
lang={setting.i18n}
mapping="number"
repo={REPO_NAME}
repoId={GISCUS_REPO_ID}
term="53"
/>
</Flexbox> </Flexbox>
</Modal> </Modal>
); );

View File

@ -2,7 +2,7 @@ import { Tag, TagProps } from 'antd';
import { memo } from 'react'; import { memo } from 'react';
import semver from 'semver'; import semver from 'semver';
import { homepage } from '@/../package.json'; import { GITHUB_REPO_URL } from '@/const/url';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
const VersionTag = memo<TagProps>((props) => { const VersionTag = memo<TagProps>((props) => {
@ -14,7 +14,7 @@ const VersionTag = memo<TagProps>((props) => {
const isLatest = semver.gte(version, latestVersion); const isLatest = semver.gte(version, latestVersion);
return ( return (
<a href={homepage} rel="noreferrer" target="_blank"> <a href={GITHUB_REPO_URL} rel="noreferrer" target="_blank">
{isLatest ? ( {isLatest ? (
<Tag color="success" {...props}> <Tag color="success" {...props}>
v{version} v{version}

11
src/const/url.ts Normal file
View File

@ -0,0 +1,11 @@
import pkg from '@/../package.json';
export const DISCORD_URL = 'https://discord.gg/AYFPHvv2jT';
export const SPONSOR_URL = 'https://opencollective.com/lobehub';
export const SPONSOR_IMG = 'https://readme-wizard.lobehub.com/api/sponsor';
export const GISCUS_REPO_ID = 'R_kgDOJCPcNg';
export const GITHUB_REPO_URL = pkg.homepage;
export const REPO_NAME = GITHUB_REPO_URL.replace(
'https://github.com/',
'',
) as `${string}/${string}`;

View File

@ -1,9 +1,9 @@
import { useResponsive } from 'antd-style'; import { useResponsive } from 'antd-style';
import { consola } from 'consola';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { memo, useEffect, useRef } from 'react'; import { memo, useRef } from 'react';
import formatPrompt from '@/scripts/formatPrompt'; import { removePromptScrollHide } from '@/features/Content/removePromptScrollHide';
import { useInject } from '@/hooks/useInject';
import { selectors, useAppStore } from '@/store'; import { selectors, useAppStore } from '@/store';
import { type DivProps } from '@/types'; import { type DivProps } from '@/types';
@ -14,46 +14,17 @@ const Content = memo<DivProps>(({ className, ...props }) => {
const mainReference = useRef<HTMLDivElement>(null); const mainReference = useRef<HTMLDivElement>(null);
const { mobile } = useResponsive(); const { mobile } = useResponsive();
const setting = useAppStore(selectors.currentSetting, isEqual); const setting = useAppStore(selectors.currentSetting, isEqual);
const { cx, styles } = useStyles({ const { cx, styles } = useStyles({
isPromptResizable: setting.promptTextareaType === 'resizable', isPromptResizable: setting.promptTextareaType === 'resizable',
layoutSplitPreview: setting.layoutSplitPreview, layoutSplitPreview: setting.layoutSplitPreview,
}); });
useEffect(() => { useInject(mainReference, '.app', {
try { debug: '[layout] inject - Content',
// Content onSuccess: () => {
const main = gradioApp().querySelector('.app'); removePromptScrollHide();
if (main) { },
mainReference.current?.append(main); });
}
// remove prompt scroll-hide
const textares = gradioApp().querySelectorAll(
`[id$="_prompt_container"] textarea`,
) as NodeListOf<HTMLTextAreaElement>;
if (textares) {
for (const textarea of textares) {
textarea.classList.remove('scroll-hide');
textarea.style.height = 'auto';
}
}
// textarea
const interrogate = gradioApp().querySelector(
'#img2img_toprow .interrogate-col',
) as HTMLDivElement;
const actions = gradioApp().querySelector('#img2img_actions_column') as HTMLDivElement;
if (interrogate && actions) {
actions.append(interrogate);
}
formatPrompt();
consola.success('🤯 [layout] inject - Content');
} catch (error) {
consola.error('🤯 [layout] inject - Content', error);
}
}, []);
return ( return (
<> <>
@ -61,13 +32,14 @@ const Content = memo<DivProps>(({ className, ...props }) => {
className={cx( className={cx(
styles.container, styles.container,
styles.textares, styles.textares,
styles.text2img, styles.txt2img,
setting.layoutSplitPreview && styles.splitView, setting.layoutSplitPreview && styles.splitView,
className, className,
)} )}
ref={mainReference} ref={mainReference}
{...props} {...props}
/> />
{setting.layoutSplitPreview && mobile === false && <SplitView />} {setting.layoutSplitPreview && mobile === false && <SplitView />}
</> </>
); );

View File

@ -0,0 +1,10 @@
export const removePromptScrollHide = () => {
const textares = gradioApp().querySelectorAll(
`[id$="_prompt_container"] textarea`,
) as NodeListOf<HTMLTextAreaElement>;
if (!textares) return;
for (const textarea of textares) {
textarea.classList.remove('scroll-hide');
textarea.style.height = 'auto';
}
};

View File

@ -171,7 +171,96 @@ export const useStyles = createStyles(
background: transparent !important; background: transparent !important;
} }
`, `,
text2img: css` textares: css`
[id$='2img_prompt'],
[id$='2img_neg_prompt'] {
textarea {
resize: ${isPromptResizable ? 'vertical' : 'none'};
overflow-y: auto;
padding: 8px !important;
font-family: ${token.fontFamilyCode} !important;
font-size: 13px !important;
line-height: 1.5 !important;
word-wrap: break-word !important;
white-space: pre-wrap !important;
transition:
all 0.3s,
height 0s;
}
}
[id$='2img_prompt'] > label > textarea {
color: ${token.colorSuccessTextHover};
&:focus {
color: ${token.colorSuccessText};
}
}
[id$='2img_neg_prompt'] > label > textarea {
color: ${token.colorErrorTextHover};
&:focus {
color: ${token.colorError};
}
}
.block.token-counter {
z-index: 10 !important;
top: -14px;
right: 4px;
scale: 0.8;
background: ${token.colorBgContainer} !important;
border-radius: 0.4em !important;
> .translucent {
display: none;
}
span {
display: inline-block;
font-family: var(--font-mono);
border: 2px solid ${token.colorFillSecondary} !important;
}
span,
&.error span {
box-shadow: none;
}
}
#lobe_txt2img_prompt .prompt_editor {
min-height: ${TEXT2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${TEXT2IMG_PROMPT_HEIGHT}px`};
}
#lobe_img2img_prompt .prompt_editor {
min-height: ${IMG2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${IMG2IMG_PROMPT_HEIGHT}px`};
}
#txt2img_prompt,
#txt2img_neg_prompt {
textarea {
min-height: ${TEXT2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${TEXT2IMG_PROMPT_HEIGHT}px`};
}
}
#img2img_prompt,
#img2img_neg_prompt {
textarea {
min-height: ${IMG2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${IMG2IMG_PROMPT_HEIGHT}px`};
}
}
`,
txt2img: css`
button[id$='_generate'] { button[id$='_generate'] {
height: var(--button-lg-height) !important; height: var(--button-lg-height) !important;
min-height: var(--button-lg-height) !important; min-height: var(--button-lg-height) !important;
@ -336,95 +425,6 @@ export const useStyles = createStyles(
box-shadow: none; box-shadow: none;
} }
`, `,
textares: css`
[id$='2img_prompt'],
[id$='2img_neg_prompt'] {
textarea {
resize: ${isPromptResizable ? 'vertical' : 'none'};
overflow-y: auto;
padding: 8px !important;
font-family: ${token.fontFamilyCode} !important;
font-size: 13px !important;
line-height: 1.5 !important;
word-wrap: break-word !important;
white-space: pre-wrap !important;
transition:
all 0.3s,
height 0s;
}
}
[id$='2img_prompt'] > label > textarea {
color: ${token.colorSuccessTextHover};
&:focus {
color: ${token.colorSuccessText};
}
}
[id$='2img_neg_prompt'] > label > textarea {
color: ${token.colorErrorTextHover};
&:focus {
color: ${token.colorError};
}
}
.block.token-counter {
z-index: 10 !important;
top: -14px;
right: 4px;
scale: 0.8;
background: ${token.colorBgContainer} !important;
border-radius: 0.4em !important;
> .translucent {
display: none;
}
span {
display: inline-block;
font-family: var(--font-mono);
border: 2px solid ${token.colorFillSecondary} !important;
}
span,
&.error span {
box-shadow: none;
}
}
#lobe_txt2img_prompt .prompt_editor {
min-height: ${TEXT2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${TEXT2IMG_PROMPT_HEIGHT}px`};
}
#lobe_img2img_prompt .prompt_editor {
min-height: ${IMG2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${IMG2IMG_PROMPT_HEIGHT}px`};
}
#text2img_prompt,
#text2img_neg_prompt {
textarea {
min-height: ${TEXT2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${TEXT2IMG_PROMPT_HEIGHT}px`};
}
}
#img2img_prompt,
#img2img_neg_prompt {
textarea {
min-height: ${IMG2IMG_PROMPT_HEIGHT}px;
max-height: ${isPromptResizable ? 'unset' : `${IMG2IMG_PROMPT_HEIGHT}px`};
}
}
`,
}; };
}, },
); );

View File

@ -1,116 +1,31 @@
import { ActionIcon, DraggablePanelBody, DraggablePanelFooter } from '@lobehub/ui'; import { ActionIcon, DraggablePanelBody, DraggablePanelFooter } from '@lobehub/ui';
import { useTimeout } from 'ahooks';
import { Skeleton, Slider } from 'antd'; import { Skeleton, Slider } from 'antd';
import { consola } from 'consola';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { ZoomIn, ZoomOut } from 'lucide-react'; import { ZoomIn, ZoomOut } from 'lucide-react';
import { memo, useEffect, useRef, useState } from 'react'; import { memo, useState } from 'react';
import { useStyles } from '@/features/ExtraNetworkSidebar/style'; import { useStyles } from '@/features/ExtraNetworkSidebar/style';
import civitaiHelperFix from '@/scripts/civitaiHelperFix'; import { useCivitaiHelperFix } from '@/features/ExtraNetworkSidebar/useCivitaiHelperFix';
import { useInjectExtraNetwork } from '@/features/ExtraNetworkSidebar/useInjectExtraNetwork';
import { selectors, useAppStore } from '@/store'; import { selectors, useAppStore } from '@/store';
const Inner = memo(() => { const Inner = memo(() => {
const txt2imgExtraNetworkSidebarReference = useRef<HTMLDivElement>(null); const txt2imgExtraNetworkSidebarReference = useInjectExtraNetwork('txt');
const img2imgExtraNetworkSidebarReference = useRef<HTMLDivElement>(null); const img2imgExtraNetworkSidebarReference = useInjectExtraNetwork('img');
const [extraLoading, setExtraLoading] = useState(true);
const setting = useAppStore(selectors.currentSetting, isEqual); const setting = useAppStore(selectors.currentSetting, isEqual);
const currentTab = useAppStore(selectors.currentTab); const currentTab = useAppStore(selectors.currentTab);
const [size, setSize] = useState<number>(setting.extraNetworkCardSize || 86); const [size, setSize] = useState<number>(setting.extraNetworkCardSize || 86);
const { styles } = useStyles({ size }); const { styles } = useStyles({ size });
useEffect(() => { const { isLoading } = useCivitaiHelperFix({
try { debug: '[layout] inject - ExtraNetworkSidebar',
if (setting.enableExtraNetworkSidebar) { });
const image2imageExtraNetworkButton = gradioApp().querySelectorAll(
'#txt2img_extra_tabs > .tab-nav > button',
)[1] as HTMLButtonElement;
const text2imageExtraNetworkButton = gradioApp().querySelectorAll(
'#img2img_extra_tabs > .tab-nav > button',
)[1] as HTMLButtonElement;
if (image2imageExtraNetworkButton) {
image2imageExtraNetworkButton.click();
}
if (text2imageExtraNetworkButton) {
text2imageExtraNetworkButton.click();
}
const txt2imgTab = gradioApp().querySelector('div#tab_txt2img') as HTMLDivElement;
const txt2imgExtraNetworks = gradioApp().querySelector(
'div#txt2img_extra_tabs',
) as HTMLDivElement;
const txt2imgRender = txt2imgExtraNetworks.querySelectorAll(
'div.tabitem.gradio-tabitem',
)[0] as HTMLDivElement;
const img2imgTab = gradioApp().querySelector('div#tab_img2img');
const img2imgExtraNetworks = gradioApp().querySelector(
'div#img2img_extra_tabs',
) as HTMLDivElement;
const img2imgRender = img2imgExtraNetworks.querySelectorAll(
'div.tabitem.gradio-tabitem',
)[0] as HTMLDivElement;
if (txt2imgExtraNetworks && img2imgExtraNetworks) {
txt2imgExtraNetworkSidebarReference.current?.append(txt2imgExtraNetworks);
txt2imgRender.id = 'txt2img_render';
txt2imgTab?.append(txt2imgRender);
img2imgExtraNetworkSidebarReference.current?.append(img2imgExtraNetworks);
img2imgRender.id = 'img2img_render';
img2imgTab?.append(img2imgRender);
}
if (document.querySelector('.extra-network-cards')) {
civitaiHelperFix();
setExtraLoading(false);
return;
}
}
consola.success('🤯 [layout] inject - ExtraNetworkSidebar');
} catch (error) {
consola.error('🤯 [layout] inject - ExtraNetworkSidebar', error);
}
}, []);
useTimeout(() => {
try {
const t2indexButton = document.querySelector('#txt2img_extra_refresh') as HTMLButtonElement;
const index2indexButton = document.querySelector(
'#img2img_extra_refresh',
) as HTMLButtonElement;
t2indexButton.click();
index2indexButton.click();
setExtraLoading(false);
const isCivitaiHelper = !!document.querySelector('#txt2img_extra_refresh');
if (isCivitaiHelper) {
const civitaiText2ImgButton = document.querySelector('#txt2img_extra_refresh')
?.nextSibling as HTMLButtonElement;
if (civitaiText2ImgButton) {
civitaiText2ImgButton.onclick = civitaiHelperFix;
}
const civitaiImg2ImgButton = document.querySelector('#img2img_extra_refresh')
?.nextSibling as HTMLButtonElement;
if (civitaiImg2ImgButton) {
civitaiImg2ImgButton.onclick = civitaiHelperFix;
}
civitaiHelperFix();
}
consola.success('🤯 [extranetwork] force reload');
} catch (error) {
consola.error('🤯 [extranetwork] force reload', error);
}
}, 2000);
return ( return (
<> <>
<DraggablePanelBody className={styles.body}> <DraggablePanelBody className={styles.body}>
{extraLoading && <Skeleton active />} {isLoading && <Skeleton active />}
<div style={extraLoading ? { display: 'none' } : {}}> <div style={isLoading ? { display: 'none' } : {}}>
<div <div
id="txt2img-extra-network-sidebar" id="txt2img-extra-network-sidebar"
ref={txt2imgExtraNetworkSidebarReference} ref={txt2imgExtraNetworkSidebarReference}

View File

@ -0,0 +1,6 @@
export const refreshExtraNetwork = (type: 'txt' | 'img') => {
const extraNetworkButton = document.querySelectorAll(
`#${type}2img_extra_tabs > .tab-nav > button`,
)[1] as HTMLButtonElement;
extraNetworkButton?.click();
};

View File

@ -11,6 +11,10 @@ export const useStyles = createStyles(
#img2img_extra_search { #img2img_extra_search {
width: 100% !important; width: 100% !important;
max-width: unset !important; max-width: unset !important;
textarea {
height: unset !important;
}
} }
#txt2img-extra-network-sidebar, #txt2img-extra-network-sidebar,

View File

@ -0,0 +1,60 @@
import { consola } from 'consola';
import { useEffect, useRef, useState } from 'react';
import civitaiHelperFix from '@/scripts/civitaiHelperFix';
const replaceCivitaiHelper = (type: 'txt' | 'img') => {
const button = document.querySelector(`#${type}2img_extra_refresh`) as HTMLButtonElement;
button.click();
const civitaiButton = document.querySelector(`#${type}2img_extra_refresh`)
?.nextSibling as HTMLButtonElement;
if (civitaiButton) {
civitaiButton.onclick = civitaiHelperFix;
}
};
interface CivitaiHelperFixOptions {
debug?: string;
onStart?: () => void;
onSuccess?: () => void;
timeout?: number;
}
export const useCivitaiHelperFix = ({
onStart,
onSuccess,
debug,
timeout = 1000,
}: CivitaiHelperFixOptions = {}) => {
const [isLoading, setIsLoading] = useState(true);
const isInject = useRef(false);
useEffect(() => {
if (isInject.current) return;
onStart?.();
const canInject =
!!document.querySelector('#tab_civitai_helper') &&
!!document.querySelector('#txt2img_extra_refresh');
if (canInject) {
try {
setTimeout(() => {
replaceCivitaiHelper('txt');
replaceCivitaiHelper('img');
civitaiHelperFix();
}, timeout);
} catch (error: any) {
setIsLoading(false);
if (debug) consola.success(`🤯 ${debug}`, error);
}
}
onSuccess?.();
isInject.current = true;
setIsLoading(false);
if (debug) consola.success(`🤯 ${debug}`);
}, []);
return {
isLoading,
};
};

View File

@ -0,0 +1,21 @@
import { useRef } from 'react';
import { useInject } from '@/hooks/useInject';
import { useSelectorRef } from '@/hooks/useSelectorRef';
import { refreshExtraNetwork } from './refreshExtraNetwork';
export const useInjectExtraNetwork = (type: 'txt' | 'img') => {
const tabReference = useSelectorRef(`div#tab_${type}2img`);
const extraNetworkSidebarReference = useRef<HTMLDivElement>(null);
useInject(extraNetworkSidebarReference, `div#${type}2img_extra_tabs`);
useInject(tabReference, 'div.tabitem.gradio-tabitem', {
id: `${type}2img_render`,
onSuccess: () => {
refreshExtraNetwork(type);
},
parent: `div#${type}2img_extra_tabs`,
});
return extraNetworkSidebarReference;
};

View File

@ -1,7 +1,7 @@
import { Icon } from '@lobehub/ui'; import { Icon } from '@lobehub/ui';
import { Bug, FileClock, GitFork, Github } from 'lucide-react'; import { Bug, FileClock, GitFork, Github, Heart } from 'lucide-react';
import { homepage } from '../../../package.json'; import { GITHUB_REPO_URL } from '@/const/url';
export const Resources = [ export const Resources = [
{ {
@ -37,17 +37,23 @@ export const Resources = [
]; ];
export const Community = [ export const Community = [
{
icon: <Icon icon={Heart} size="small" />,
openExternal: true,
title: 'Sponsor',
url: `https://opencollective.com/lobehub`,
},
{ {
icon: <Icon icon={Bug} size="small" />, icon: <Icon icon={Bug} size="small" />,
openExternal: true, openExternal: true,
title: 'Report Bug', title: 'Report Bug',
url: `${homepage}/issues/new/choose`, url: `${GITHUB_REPO_URL}/issues/new/choose`,
}, },
{ {
icon: <Icon icon={GitFork} size="small" />, icon: <Icon icon={GitFork} size="small" />,
openExternal: true, openExternal: true,
title: 'Request Feature', title: 'Request Feature',
url: `${homepage}/issues/new/choose`, url: `${GITHUB_REPO_URL}/issues/new/choose`,
}, },
]; ];
@ -56,17 +62,23 @@ export const Help = [
icon: <Icon icon={Github} size="small" />, icon: <Icon icon={Github} size="small" />,
openExternal: true, openExternal: true,
title: 'GitHub', title: 'GitHub',
url: homepage, url: GITHUB_REPO_URL,
}, },
{ {
icon: <Icon icon={FileClock} size="small" />, icon: <Icon icon={FileClock} size="small" />,
openExternal: true, openExternal: true,
title: 'Changelog', title: 'Changelog',
url: `${homepage}/blob/main/CHANGELOG.md`, url: `${GITHUB_REPO_URL}/blob/main/CHANGELOG.md`,
}, },
]; ];
export const MoreProducts = [ export const MoreProducts = [
{
description: 'Stable Diffusion Extension',
openExternal: true,
title: '🤯 Lobe Theme',
url: 'https://github.com/lobehub/sd-webui-lobe-theme',
},
{ {
description: 'Minifier ExtraNetwrok Covers', description: 'Minifier ExtraNetwrok Covers',
openExternal: true, openExternal: true,
@ -86,9 +98,9 @@ export const MoreProducts = [
url: 'https://ui.lobehub.com', url: 'https://ui.lobehub.com',
}, },
{ {
description: 'AI Commit CLI', description: 'I18n AI Workflow',
openExternal: true, openExternal: true,
title: '💌 Lobe Commit', title: '🌐 Lobe i18n',
url: 'https://github.com/lobehub/lobe-commit', url: 'https://github.com/lobehub/lobe-cli-toolbox',
}, },
]; ];

View File

@ -1,9 +1,9 @@
import { Footer as F } from '@lobehub/ui'; import { Footer as F } from '@lobehub/ui';
import { consola } from 'consola';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { memo, useEffect, useRef } from 'react'; import { memo, useRef } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useInject } from '@/hooks/useInject';
import { selectors, useAppStore } from '@/store'; import { selectors, useAppStore } from '@/store';
import { type DivProps } from '@/types'; import { type DivProps } from '@/types';
@ -16,23 +16,19 @@ const Footer = memo<DivProps>(({ className, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const footerReference = useRef<HTMLDivElement>(null); const footerReference = useRef<HTMLDivElement>(null);
useEffect(() => { useInject(footerReference, '#footer', {
try { debug: '[layout] inject - Footer',
const footer = gradioApp().querySelector('#footer'); onSuccess: (footer) => {
if (footer) footerReference.current?.append(footer); if (!setting.confirmPageUnload) return;
if (setting.confirmPageUnload) { window.addEventListener('beforeunload', (event) => {
window.addEventListener('beforeunload', (event) => { if (footer?.isConnected) {
if (footer?.isConnected) { event.preventDefault();
event.preventDefault(); return (event.returnValue = '');
return (event.returnValue = ''); }
} });
}); },
} });
consola.success('🤯 [layout] inject - Footer');
} catch (error) {
consola.error('🤯 [layout] inject - Footer', error);
}
}, []);
return ( return (
<div className={cx(styles.footer, className)} {...props}> <div className={cx(styles.footer, className)} {...props}>
<F <F

View File

@ -1,79 +1,16 @@
import { Burger, TabsNav, type TabsNavProps } from '@lobehub/ui'; import { Burger, TabsNav } from '@lobehub/ui';
import { useResponsive } from 'antd-style'; import { useResponsive } from 'antd-style';
import { consola } from 'consola'; import { memo, useState } from 'react';
import { startCase } from 'lodash-es';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { selectors, useAppStore } from '@/store'; import { selectors, useAppStore } from '@/store';
const hideOriganlNav = () => { import { useNavBar } from './useNavBar';
(gradioApp().querySelector('#tabs > .tab-nav:first-of-type') as HTMLDivElement).style.display =
'none';
};
const getNavTabs = (): HTMLDivElement[] =>
Array.prototype.slice.call(
gradioApp().querySelectorAll('#tabs > [id^="tab_"]') as NodeListOf<HTMLDivElement>,
);
const getNavButtons = (): HTMLButtonElement[] =>
Array.prototype.slice.call(
gradioApp().querySelectorAll(
'#tabs > .tab-nav:first-of-type button',
) as NodeListOf<HTMLButtonElement>,
);
interface NavItem {
id: string;
index: number;
label: string;
}
const genNavList = (): NavItem[] => {
const navList = getNavTabs();
const buttons = getNavButtons();
consola.debug('🤯 [nav] generate nav list');
return buttons.map((button, index) => {
const id = navList[index].id;
return {
id,
index,
label: startCase(String(button.textContent)),
};
});
};
const Nav = memo(() => { const Nav = memo(() => {
const currentTab = useAppStore(selectors.currentTab); const currentTab = useAppStore(selectors.currentTab);
const { mobile } = useResponsive(); const { mobile } = useResponsive();
const { items, onChange } = useNavBar(mobile);
const [opened, setOpened] = useState(false); const [opened, setOpened] = useState(false);
const [items, setItems] = useState<TabsNavProps['items']>([]);
const navList = useMemo(() => genNavList(), []);
const onChange: TabsNavProps['onChange'] = useCallback(
(id: string) => {
consola.debug('🤯 [nav] onClick', id);
const index = navList.find((nav) => nav.id === id)?.index || 0;
const buttonList = getNavButtons();
buttonList[index].click();
},
[navList],
);
useEffect(() => {
try {
hideOriganlNav();
const list: TabsNavProps['items'] = navList.map((item) => {
return {
key: item.id,
label: mobile ? <div onClick={() => onChange(item.id)}>{item.label}</div> : item.label,
};
});
setItems(list.filter(Boolean));
consola.success('🤯 [layout] inject - Header');
} catch (error) {
consola.error('🤯 [layout] inject - Header', error);
}
}, [mobile]);
if (mobile) return <Burger items={items} opened={opened} setOpened={setOpened} />; if (mobile) return <Burger items={items} opened={opened} setOpened={setOpened} />;

View File

@ -0,0 +1,32 @@
import { consola } from 'consola';
import { startCase } from 'lodash-es';
const getNavTabs = (): HTMLDivElement[] =>
Array.prototype.slice.call(
gradioApp().querySelectorAll('#tabs > [id^="tab_"]') as NodeListOf<HTMLDivElement>,
);
export const getNavButtons = (): HTMLButtonElement[] =>
Array.prototype.slice.call(
gradioApp().querySelectorAll(
'#tabs > .tab-nav:first-of-type button',
) as NodeListOf<HTMLButtonElement>,
);
interface NavItem {
id: string;
index: number;
label: string;
}
export const genNavList = (): NavItem[] => {
const navList = getNavTabs();
const buttons = getNavButtons();
consola.debug('🤯 [nav] generate nav list');
return buttons.map((button, index) => {
const id = navList[index].id;
return {
id,
index,
label: startCase(String(button.textContent)),
};
});
};

View File

@ -3,10 +3,10 @@ import { useTheme } from 'antd-style';
import { memo } from 'react'; import { memo } from 'react';
import { Logo } from '@/components'; import { Logo } from '@/components';
import { GITHUB_REPO_URL } from '@/const/url';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import { type DivProps } from '@/types'; import { type DivProps } from '@/types';
import { homepage, name } from '../../../package.json';
import Actions from './Actions'; import Actions from './Actions';
import Nav from './Nav'; import Nav from './Nav';
@ -23,12 +23,12 @@ const Header = memo<DivProps>(({ children }) => {
actionsStyle={{ flex: 0 }} actionsStyle={{ flex: 0 }}
logo={ logo={
<a <a
href={`${homepage}/releases`} href={`${GITHUB_REPO_URL}/releases`}
rel="noreferrer" rel="noreferrer"
style={{ alignItems: 'center', color: theme.colorText, display: 'flex' }} style={{ alignItems: 'center', color: theme.colorText, display: 'flex' }}
target="_blank" target="_blank"
> >
<Tooltip title={`${name} v${version}`}> <Tooltip title={`LobeTheme v${version}`}>
<Logo /> <Logo />
</Tooltip> </Tooltip>
</a> </a>

View File

@ -0,0 +1,40 @@
import { TabsNavProps } from '@lobehub/ui';
import { consola } from 'consola';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSelectorHide } from '@/hooks/useSelectorHide';
import { genNavList, getNavButtons } from './genNavList';
export const useNavBar = (mobile?: boolean) => {
const [items, setItems] = useState<TabsNavProps['items']>([]);
const navList = useMemo(() => genNavList(), []);
const onChange: TabsNavProps['onChange'] = useCallback(
(id: string) => {
consola.debug('🤯 [nav] onClick', id);
const index = navList.find((nav) => nav.id === id)?.index || 0;
const buttonList = getNavButtons();
buttonList[index].click();
},
[navList],
);
useSelectorHide('#tabs > .tab-nav:first-of-type');
useEffect(() => {
try {
const list: TabsNavProps['items'] = navList.map((item) => {
return {
key: item.id,
label: mobile ? <div onClick={() => onChange(item.id)}>{item.label}</div> : item.label,
};
});
setItems(list.filter(Boolean));
consola.success('🤯 [layout] inject - Header');
} catch (error) {
consola.error('🤯 [layout] inject - Header', error);
}
}, [mobile]);
return {
items,
onChange,
};
};

View File

@ -0,0 +1,12 @@
import { Converter } from '@/scripts/formatPrompt';
export const createButton = (type: 'txt' | 'img') => {
const button = document.createElement('button');
button.id = `${type}2img_formatconvert`;
button.type = 'button';
button.innerHTML = '🪄';
button.title = 'Format prompt~🪄';
button.className = 'lg secondary gradio-button tool svelte-cmf5ev';
button.addEventListener('click', () => Converter.onClickConvert(type));
return button;
};

View File

@ -0,0 +1,11 @@
import { memo } from 'react';
import { useInjectPromptFormator } from '@/features/PromptFormator/useInjectPromptFormator';
const PromptFormator = memo(() => {
useInjectPromptFormator('txt');
useInjectPromptFormator('img');
return null;
});
export default PromptFormator;

View File

@ -0,0 +1,11 @@
import { useRef } from 'react';
import { createButton } from '@/features/PromptFormator/createButton';
import { useInject } from '@/hooks/useInject';
export const useInjectPromptFormator = (type: 'txt' | 'img') => {
const ref = useRef<any>(createButton(type));
useInject(ref, `#${type}2img_tools > div.form`, {
inverse: true,
});
};

View File

@ -1,12 +1,12 @@
import { DraggablePanelBody } from '@lobehub/ui'; import { DraggablePanelBody } from '@lobehub/ui';
import { Segmented } from 'antd'; import { Segmented } from 'antd';
import { useTheme } from 'antd-style'; import { useTheme } from 'antd-style';
import { consola } from 'consola'; import { memo, useRef, useState } from 'react';
import { memo, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit'; import { Flexbox } from 'react-layout-kit';
import { PromptEditor } from '@/components'; import { PromptEditor } from '@/components';
import { useInject } from '@/hooks/useInject';
import { type DivProps } from '@/types'; import { type DivProps } from '@/types';
enum Tabs { enum Tabs {
@ -19,15 +19,10 @@ const Inner = memo<DivProps>(() => {
const [tab, setTab] = useState<Tabs>(Tabs.Setting); const [tab, setTab] = useState<Tabs>(Tabs.Setting);
const sidebarReference = useRef<HTMLDivElement>(null); const sidebarReference = useRef<HTMLDivElement>(null);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
try { useInject(sidebarReference, '#quicksettings', {
const sidebar = gradioApp().querySelector('#quicksettings'); debug: '[layout] inject - QuickSettingSidebar',
if (sidebar) sidebarReference.current?.append(sidebar); });
consola.success('🤯 [layout] inject - QuickSettingSidebar');
} catch (error) {
consola.error('🤯 [layout] inject - QuickSettingSidebar', error);
}
}, []);
return ( return (
<DraggablePanelBody> <DraggablePanelBody>

View File

@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit'; import { Flexbox } from 'react-layout-kit';
import VersionTag from '@/components/VersionTag'; import VersionTag from '@/components/VersionTag';
import { GITHUB_REPO_URL } from '@/const/url';
import { homepage } from '../../../package.json';
import FormAppearance from './Form/Appearance'; import FormAppearance from './Form/Appearance';
import FormExperimental from './Form/Experimental'; import FormExperimental from './Form/Experimental';
import FormLayout from './Form/Layout'; import FormLayout from './Form/Layout';
@ -29,7 +29,7 @@ const Setting = memo<SettingProps>(({ open, onCancel }) => {
title={ title={
<Flexbox align={'center'} gap={4}> <Flexbox align={'center'} gap={4}>
<Flexbox align={'center'} gap={4} horizontal> <Flexbox align={'center'} gap={4} horizontal>
<a href={homepage} rel="noreferrer" target="_blank"> <a href={GITHUB_REPO_URL} rel="noreferrer" target="_blank">
<ActionIcon icon={Book} title="Setting Documents" /> <ActionIcon icon={Book} title="Setting Documents" />
</a> </a>

View File

@ -7,7 +7,7 @@ import { PropsWithChildren, memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Flexbox } from 'react-layout-kit'; import { Flexbox } from 'react-layout-kit';
import pkg from '@/../package.json'; import { GITHUB_REPO_URL } from '@/const/url';
import { useStyles } from './style'; import { useStyles } from './style';
@ -99,7 +99,7 @@ const Preview = memo<PreviewProps>(({ imageType, withBackground, withFooter, chi
{withFooter ? ( {withFooter ? (
<Flexbox align={'center'} className={styles.footer} gap={4}> <Flexbox align={'center'} className={styles.footer} gap={4}>
<Logo extra={'SD'} type={'combine'} /> <Logo extra={'SD'} type={'combine'} />
<div className={styles.url}>{pkg.homepage}</div> <div className={styles.url}>{GITHUB_REPO_URL}</div>
</Flexbox> </Flexbox>
) : ( ) : (
<div /> <div />

View File

@ -1,19 +0,0 @@
const addShareButton = (id: string, onClick: () => void): HTMLButtonElement => {
const button = document.createElement('button');
button.id = id;
button.type = 'button';
button.innerHTML = '💞';
button.title = 'Share';
button.className = 'lg secondary gradio-button tool svelte-cmf5ev';
button.addEventListener('click', onClick);
return button;
};
export default (type: string, onClick: () => void) => {
const id = `lobe_share_${type}`;
const isInit = document.querySelector(`#${id}`);
if (isInit) return;
const container = document.querySelector(`#image_buttons_${type}2img > .form`) as HTMLDivElement;
if (!container) return;
container.append(addShareButton(id, onClick));
};

View File

@ -0,0 +1,10 @@
export const createButton = (type: string, setOpen: (open: boolean) => void): HTMLButtonElement => {
const button = document.createElement('button');
button.id = `lobe_share_${type}`;
button.type = 'button';
button.innerHTML = '💞';
button.title = 'Share';
button.className = 'lg secondary gradio-button tool svelte-cmf5ev';
button.addEventListener('click', () => setOpen(true));
return button;
};

View File

@ -1,25 +1,21 @@
import { Modal } from '@lobehub/ui'; import { Modal } from '@lobehub/ui';
import { consola } from 'consola'; import { memo, useRef, useState } from 'react';
import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useInject } from '@/hooks/useInject';
import Inner from './Inner'; import Inner from './Inner';
import addShareButton from './addShareButton'; import { createButton } from './createButton';
const Share = memo<{ type: 'txt' | 'img' }>(({ type }) => { const Share = memo<{ type: 'txt' | 'img' }>(({ type }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleShare = useCallback(() => { const buttonReference = useRef<any>(createButton(type, setOpen));
setOpen(true);
}, []);
const { t } = useTranslation(); const { t } = useTranslation();
useEffect(() => {
try { useInject(buttonReference, `#image_buttons_${type}2img > .form`, {
addShareButton(type, handleShare); debug: `[layout] inject - Share ${type}`,
consola.success(`🤯 [layout] inject - Share ${type}`); inverse: true,
} catch (error) { });
consola.error(`🤯 [layout] inject - Share ${type}`, error);
}
}, [type]);
return ( return (
<Modal <Modal

55
src/hooks/useInject.ts Normal file
View File

@ -0,0 +1,55 @@
import { consola } from 'consola';
import { RefObject, useEffect, useRef, useState } from 'react';
interface InjectOptions {
debug?: string;
id?: string;
inverse?: boolean;
onError?: (error: Error) => void;
onStart?: (ele: HTMLDivElement) => void;
onSuccess?: (ele: HTMLDivElement) => void;
parent?: string;
}
export const useInject = (
ref: RefObject<HTMLDivElement>,
selectors: string,
{ onSuccess, onError, debug, id, onStart, parent, inverse }: InjectOptions = {},
) => {
const [isLoading, setIsLoading] = useState(true);
const [element, setElement] = useState<HTMLDivElement>();
const isInject = useRef(false);
useEffect(() => {
if (isInject.current) return;
try {
const root = parent ? (gradioApp().querySelector(parent) as HTMLDivElement) : gradioApp();
const ele = root.querySelector(selectors) as HTMLDivElement;
if (ele) {
if (id) ele.id = id;
onStart?.(ele);
if (inverse && ref.current) {
ele.append(ref.current);
} else {
ref.current?.append(ele);
}
setElement(ele);
onSuccess?.(ele);
isInject.current = true;
setIsLoading(false);
if (debug) consola.success(`🤯 ${debug}`);
} else {
if (debug) consola.error(`🤯 ${debug}`, `Element not found for selector: ${selectors}`);
}
} catch (error: any) {
console.error(error);
onError?.(error);
setIsLoading(false);
if (debug) consola.error(`🤯 ${debug}`, error);
}
}, []);
return {
element,
isLoaded: !isLoading,
isLoading,
};
};

View File

@ -0,0 +1,9 @@
import { useEffect } from 'react';
export const useSelectorHide = (selectors: string) => {
useEffect(() => {
const ele = gradioApp().querySelector(selectors) as HTMLDivElement;
if (!ele) return;
ele.style.display = 'none';
}, []);
};

View File

@ -0,0 +1,5 @@
import { RefObject, useRef } from 'react';
export const useSelectorRef = (selectors: string): RefObject<HTMLDivElement> => {
return useRef<HTMLDivElement>(gradioApp().querySelector(selectors) as HTMLDivElement);
};

View File

@ -1,16 +1,13 @@
import { memo, useEffect } from 'react'; import { memo } from 'react';
import { useObserver } from '@/hooks/useObserver'; import { useObserver } from '@/hooks/useObserver';
import { useSelectorHide } from '@/hooks/useSelectorHide';
import InfoBox from './features/InfoBox'; import InfoBox from './features/InfoBox';
const Index = memo<{ parentId: string }>(({ parentId }) => { const Index = memo<{ parentId: string }>(({ parentId }) => {
const value = useObserver(`${parentId} .infotext`, { subSelector: 'p' }); const value = useObserver(`${parentId} .infotext`, { subSelector: 'p' });
useSelectorHide(`${parentId} .infotext`);
useEffect(() => {
const infoContainer = gradioApp().querySelector(`${parentId} .infotext`) as HTMLDivElement;
infoContainer.style.display = 'none';
}, []);
return <InfoBox value={value} />; return <InfoBox value={value} />;
}); });

View File

@ -1,85 +0,0 @@
import { consola } from 'consola';
const MIN_WIDTH = 240;
const addDraggable = (tabId: string) => {
const settings = document.querySelector(`#${tabId}_settings`) as HTMLDivElement;
const checkDraggableLine = document.querySelector(
`#tab_${tabId} .draggable-line`,
) as HTMLDivElement;
if (!settings || checkDraggableLine) return;
settings.style.minWidth = `min(${MIN_WIDTH}px, 100%)`;
const lineWrapper = document.createElement('div');
lineWrapper.classList.add('draggable-line');
settings.after(lineWrapper);
const container: HTMLElement | any = settings.parentElement;
container.classList.add('draggable-container');
let results: HTMLDivElement = document.querySelector(`#${tabId}_results`) as HTMLDivElement;
if (!results) return;
if (tabId === 'extras') results = results.parentElement as HTMLDivElement;
results.style.minWidth = `${MIN_WIDTH}px`;
let linePosition = 50;
settings.style.flexBasis = `${linePosition}%`;
results.style.flexBasis = `${100 - linePosition}%`;
let isDragging = false;
lineWrapper.addEventListener('mousedown', (e) => {
isDragging = true;
e.preventDefault();
});
document.addEventListener('mousemove', (event) => {
if (!isDragging) return;
const tab = document.querySelector(`#tab_${tabId}`) as HTMLDivElement;
if (!tab) return;
let offsetX = tab.offsetLeft;
let parent = tab.offsetParent as HTMLDivElement;
while (parent) {
offsetX += parent.offsetLeft;
parent = parent.offsetParent as HTMLDivElement;
}
const containerWidth = container.offsetWidth;
const mouseX = event.clientX;
const linePosition = ((mouseX - offsetX) / containerWidth) * 100;
if (linePosition <= (MIN_WIDTH / containerWidth) * 100) return;
if (linePosition >= (1 - MIN_WIDTH / containerWidth) * 100) return;
settings.style.flexBasis = `${linePosition}%`;
results.style.flexBasis = `${100 - linePosition}%`;
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
};
export default () => {
try {
addDraggable('txt2img');
addDraggable('img2img');
const extrasSetting = document.querySelector('#extras_results')?.parentElement
?.previousElementSibling as HTMLDivElement;
if (extrasSetting) {
extrasSetting.id = 'extras_settings';
addDraggable('extras');
}
consola.success('🤯 [layout] inject - DraggablePanel');
} catch (error) {
consola.error('🤯 [layout] inject - DraggablePanel', error);
}
};

View File

@ -1,30 +1,4 @@
import { consola } from 'consola';
/**
*
*/
export const Converter = { export const Converter = {
/**
*
* @param type -
*/
addPromptButton(type: string): void {
consola.info('🤯 [formatPrompt] inject', type);
const actionsColumn: HTMLElement | null = gradioApp().querySelector(
`#${type}_tools > div.form`,
);
const formatBtn: HTMLElement | null = gradioApp().querySelector(`#${type}_formatconvert`);
if (!actionsColumn || formatBtn) return;
const convertButton: HTMLElement = Converter.createButton(`${type}_formatconvert`, '🪄', () =>
Converter.onClickConvert(type));
actionsColumn.append(convertButton);
},
/**
*
* @param input
* @returns
*/
convert(input: string): string { convert(input: string): string {
const re_attention = /\{|\[|\}|\]|[^{}[\]]+/gmu; const re_attention = /\{|\[|\}|\]|[^{}[\]]+/gmu;
@ -92,7 +66,7 @@ export const Converter = {
for (const [word, value] of res) { for (const [word, value] of res) {
result += value === 1 ? word : `(${word}:${value.toString()})`; result += value === 1 ? word : `(${word}:${value.toString()})`;
} }
return result; return result.trim().replaceAll(/\s+/g, ' ');
}, },
/** /**
@ -211,24 +185,6 @@ export const Converter = {
}); });
}, },
/**
*
* @param id id
* @param innerHTML
* @param onClick
* @returns
*/
createButton(id: string, innerHTML: string, onClick: () => void): HTMLButtonElement {
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-cmf5ev';
button.addEventListener('click', onClick);
return button;
},
/** /**
* input * input
* @param target * @param target
@ -248,14 +204,14 @@ export const Converter = {
const default_negative = ''; const default_negative = '';
const prompt = gradioApp().querySelector( const prompt = gradioApp().querySelector(
`#${type}_prompt > label > textarea`, `#${type}2img_prompt > label > textarea`,
) as HTMLTextAreaElement; ) as HTMLTextAreaElement;
const result = Converter.convert(prompt.value); const result = Converter.convert(prompt.value);
prompt.value = prompt.value =
result.match(/^masterpiece, best quality,/) === null ? default_prompt + result : result; result.match(/^masterpiece, best quality,/) === null ? default_prompt + result : result;
Converter.dispatchInputEvent(prompt); Converter.dispatchInputEvent(prompt);
const negprompt = gradioApp().querySelector( const negprompt = gradioApp().querySelector(
`#${type}_neg_prompt > label > textarea`, `#${type}2img_neg_prompt > label > textarea`,
) as HTMLTextAreaElement; ) as HTMLTextAreaElement;
const negResult = Converter.convert(negprompt.value); const negResult = Converter.convert(negprompt.value);
negprompt.value = negprompt.value =
@ -276,9 +232,3 @@ export const Converter = {
return Math.round(value * 10_000) / 10_000; return Math.round(value * 10_000) / 10_000;
}, },
}; };
export default () => {
Converter.addPromptButton('txt2img');
Converter.addPromptButton('img2img');
consola.success('🤯 [formatPrompt] inject');
};

98
src/store/action.test.ts Normal file
View File

@ -0,0 +1,98 @@
// import { act, renderHook } from '@testing-library/react';
// import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
//
// import * as api from './api';
// import { useAppStore } from './index';
//
// vi.mock('./api', () => ({
// getLatestVersion: vi.fn(),
// getLocaleOptions: vi.fn(),
// getSetting: vi.fn(),
// getVersion: vi.fn(),
// postSetting: vi.fn(),
// }));
//
// // Mock for localStorage
// const localStorageMock = (function () {
// let store: any = {};
// return {
// clear: () => {
// store = {};
// },
// getItem: vi.fn((key) => store[key] || null),
// setItem: vi.fn((key, value) => {
// store[key] = value.toString();
// }),
// };
// })();
//
// (global as any).localStorage = localStorageMock;
//
// beforeAll(() => {
// // Initialize mocks before all tests
// vi.mocked(api.getSetting).mockResolvedValue(undefined);
// vi.mocked(api.postSetting).mockResolvedValue(undefined);
// vi.mocked(api.getVersion).mockResolvedValue('1.0.0');
// vi.mocked(api.getLatestVersion).mockResolvedValue('1.0.1');
// vi.mocked(api.getLocaleOptions).mockResolvedValue([]);
// });
//
// afterEach(() => {
// // Clear all mocks after each test
// vi.clearAllMocks();
// localStorage.clear();
// });
//
// describe('Store Actions', () => {
// it('onInit should initialize the store correctly', async () => {
// const { result } = renderHook(() => useAppStore());
//
// act(() => {
// result.current.onInit();
// });
//
// expect(result.current.loading).toBe(false);
// expect(result.current.version).toBe('1.0.0');
// expect(result.current.latestVersion).toBe('1.0.1');
// expect(result.current.localeOptions).toEqual([]);
// });
//
// it('onLoadSetting should load settings correctly', async () => {
// const { result } = renderHook(() => useAppStore());
//
// const mockSetting = {
// confirmPageUnload: true,
// enableSidebar: false,
// };
//
// // Simulate local storage having a setting
// localStorage.setItem('SD-LOBE-SETTING', JSON.stringify(mockSetting));
//
// act(() => {
// result.current.onLoadSetting();
// });
//
// expect(result.current.setting).toEqual(expect.objectContaining(mockSetting));
// });
//
// it('onSetSetting should update the setting correctly', async () => {
// const { result } = renderHook(() => useAppStore());
//
// const newSetting = {
// confirmPageUnload: false,
// };
//
// await act(async () => {
// await result.current.onSetSetting(newSetting);
// });
//
// expect(localStorage.setItem).toHaveBeenCalledWith(
// 'SD-LOBE-SETTING',
// JSON.stringify(expect.objectContaining(newSetting)),
// );
// expect(result.current.setting).toEqual(expect.objectContaining(newSetting));
// expect(api.postSetting).toHaveBeenCalledWith(expect.objectContaining(newSetting));
// });
//
// // Add more tests for each action as required...
// });

View File

@ -2,7 +2,8 @@ import type { SelectProps } from 'antd';
import semver from 'semver'; import semver from 'semver';
import defualtLocaleOptions from '@/../locales/options.json'; import defualtLocaleOptions from '@/../locales/options.json';
import { homepage, version } from '@/../package.json'; import { version } from '@/../package.json';
import { GITHUB_REPO_URL } from '@/const/url';
import type { WebuiSetting } from './initialState'; import type { WebuiSetting } from './initialState';
@ -66,7 +67,10 @@ export const getLocaleOptions = async(): Promise<SelectProps['options']> => {
export const getLatestVersion = async(): Promise<string> => { export const getLatestVersion = async(): Promise<string> => {
const res = await fetch( const res = await fetch(
`https://api.github.com/repos/${homepage.replace('https://github.com/', '')}/releases/latest`, `https://api.github.com/repos/${GITHUB_REPO_URL.replace(
'https://github.com/',
'',
)}/releases/latest`,
); );
const data = (await res.json()) as any; const data = (await res.json()) as any;
if (!data || !data.tag_name) return DEFAULT_VERSION; if (!data || !data.tag_name) return DEFAULT_VERSION;

12
tests/setup.ts Normal file
View File

@ -0,0 +1,12 @@
/* eslint-disable import/newline-after-import,import/first */
import '@testing-library/jest-dom';
import { theme } from 'antd';
// mock indexedDB to test with dexie
// refs: https://github.com/dumbmatter/fakeIndexedDB#dexie-and-other-indexeddb-api-wrappers
import React from 'react';
// remove antd hash on test
theme.defaultConfig.hashed = false;
// 将 React 设置为全局变量,这样就不需要在每个测试文件中导入它了
global.React = React;

View File

@ -1,14 +1,18 @@
import { resolve } from 'node:path';
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
import { name } from './package.json';
export default defineConfig({ export default defineConfig({
test: { test: {
alias: { alias: {
'@': './src', '@': resolve(__dirname, './src'),
[name]: './src', },
coverage: {
all: false,
provider: 'v8',
reporter: ['text', 'json', 'lcov', 'text-summary'],
}, },
environment: 'jsdom', environment: 'jsdom',
globals: true, globals: true,
setupFiles: './tests/setup.ts',
}, },
}); });