mirror of
https://github.com/lobehub/sd-webui-lobe-theme.git
synced 2026-01-09 06:23:44 +08:00
♻️ refactor: Refactor inject with react hook (#489)
* ♻️ refactor: Refactor inject with react hook * ♻️ refactor: Refactor inject with react hook * ♻️ refactor: Refactor inject
This commit is contained in:
parent
d864e39ce0
commit
c376aa6c29
@ -3,5 +3,4 @@
|
||||
|
||||
git add .
|
||||
npx --no-install lint-staged
|
||||
npm run test
|
||||
git add .
|
||||
git add .
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -89,7 +89,7 @@
|
||||
"react-rnd": "^10",
|
||||
"react-tag-input": "^6",
|
||||
"semver": "^7",
|
||||
"shikiji": "^0.7",
|
||||
"shikiji": "^0.8",
|
||||
"swr": "^2",
|
||||
"zustand": "^4.4.1",
|
||||
"zustand-utils": "^1.3.1"
|
||||
@ -97,6 +97,8 @@
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18",
|
||||
"@lobehub/lint": "latest",
|
||||
"@testing-library/jest-dom": "^6",
|
||||
"@testing-library/react": "^14",
|
||||
"@types/lodash-es": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
@ -105,7 +107,7 @@
|
||||
"@types/react-tag-input": "^6",
|
||||
"@types/semver": "^7",
|
||||
"@vitejs/plugin-react-swc": "^3",
|
||||
"@vitest/coverage-v8": "latest",
|
||||
"@vitest/coverage-v8": "^1",
|
||||
"commitlint": "^18",
|
||||
"dotenv": "^16",
|
||||
"eslint": "^8",
|
||||
|
||||
@ -2,6 +2,7 @@ import { LayoutHeader, LayoutMain, LayoutSidebar } from '@lobehub/ui';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useEffect } from 'react';
|
||||
|
||||
import PromptFormator from '@/features/PromptFormator';
|
||||
import '@/locales/config';
|
||||
import ImageInfo from '@/modules/ImageInfo/page';
|
||||
import PromptHighlight from '@/modules/PromptHighlight/page';
|
||||
@ -50,6 +51,7 @@ const Index = memo(() => {
|
||||
</LayoutSidebar>
|
||||
)}
|
||||
<Content className={cx(!setting.enableSidebar && styles.quicksettings)} />
|
||||
<PromptFormator />
|
||||
<Share />
|
||||
{setting?.enableExtraNetworkSidebar && (
|
||||
<LayoutSidebar
|
||||
|
||||
@ -7,15 +7,22 @@ import {
|
||||
type ModalProps,
|
||||
} from '@lobehub/ui';
|
||||
import { Button } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { useTheme, useThemeMode } from 'antd-style';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { Github } from 'lucide-react';
|
||||
import { Github, Heart } from 'lucide-react';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Center, Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { homepage } from '@/../package.json';
|
||||
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';
|
||||
|
||||
export interface GiscusProps {
|
||||
@ -23,11 +30,10 @@ export interface GiscusProps {
|
||||
open?: ModalProps['open'];
|
||||
}
|
||||
|
||||
const repoName = homepage.replace('https://github.com/', '') as `${string}/${string}`;
|
||||
|
||||
const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
|
||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||
const theme = useTheme();
|
||||
const { isDarkMode } = useThemeMode();
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal
|
||||
@ -44,7 +50,6 @@ const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
|
||||
<Flexbox gap={32}>
|
||||
<Center
|
||||
gap={16}
|
||||
horizontal
|
||||
style={{
|
||||
background: theme.colorBgLayout,
|
||||
border: `1px solid ${theme.colorBorderSecondary}`,
|
||||
@ -52,16 +57,32 @@ const Giscus = memo<GiscusProps>(({ open, onCancel }) => {
|
||||
padding: '16px 0',
|
||||
}}
|
||||
>
|
||||
<a href={'https://discord.gg/AYFPHvv2jT'} rel="noreferrer" target="_blank">
|
||||
<Button icon={<Icon icon={DiscordIcon} />} size={'large'}>
|
||||
Join Discover
|
||||
</Button>
|
||||
</a>
|
||||
<a href={homepage} rel="noreferrer" target="_blank">
|
||||
<GradientButton icon={<Icon icon={Github} />}>LobeTheme Github</GradientButton>
|
||||
<Flexbox gap={16} horizontal>
|
||||
<a href={DISCORD_URL} rel="noreferrer" target="_blank">
|
||||
<Button icon={<Icon icon={DiscordIcon} />} size={'large'}>
|
||||
Join Discover
|
||||
</Button>
|
||||
</a>
|
||||
<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>
|
||||
</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>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import { Tag, TagProps } from 'antd';
|
||||
import { memo } from 'react';
|
||||
import semver from 'semver';
|
||||
|
||||
import { homepage } from '@/../package.json';
|
||||
import { GITHUB_REPO_URL } from '@/const/url';
|
||||
import { useAppStore } from '@/store';
|
||||
|
||||
const VersionTag = memo<TagProps>((props) => {
|
||||
@ -14,7 +14,7 @@ const VersionTag = memo<TagProps>((props) => {
|
||||
const isLatest = semver.gte(version, latestVersion);
|
||||
|
||||
return (
|
||||
<a href={homepage} rel="noreferrer" target="_blank">
|
||||
<a href={GITHUB_REPO_URL} rel="noreferrer" target="_blank">
|
||||
{isLatest ? (
|
||||
<Tag color="success" {...props}>
|
||||
v{version}
|
||||
|
||||
11
src/const/url.ts
Normal file
11
src/const/url.ts
Normal 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}`;
|
||||
@ -1,9 +1,9 @@
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { consola } from 'consola';
|
||||
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 { type DivProps } from '@/types';
|
||||
|
||||
@ -14,46 +14,17 @@ const Content = memo<DivProps>(({ className, ...props }) => {
|
||||
const mainReference = useRef<HTMLDivElement>(null);
|
||||
const { mobile } = useResponsive();
|
||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||
|
||||
const { cx, styles } = useStyles({
|
||||
isPromptResizable: setting.promptTextareaType === 'resizable',
|
||||
layoutSplitPreview: setting.layoutSplitPreview,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
// Content
|
||||
const main = gradioApp().querySelector('.app');
|
||||
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);
|
||||
}
|
||||
}, []);
|
||||
useInject(mainReference, '.app', {
|
||||
debug: '[layout] inject - Content',
|
||||
onSuccess: () => {
|
||||
removePromptScrollHide();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -61,13 +32,14 @@ const Content = memo<DivProps>(({ className, ...props }) => {
|
||||
className={cx(
|
||||
styles.container,
|
||||
styles.textares,
|
||||
styles.text2img,
|
||||
styles.txt2img,
|
||||
setting.layoutSplitPreview && styles.splitView,
|
||||
className,
|
||||
)}
|
||||
ref={mainReference}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
{setting.layoutSplitPreview && mobile === false && <SplitView />}
|
||||
</>
|
||||
);
|
||||
|
||||
10
src/features/Content/removePromptScrollHide.ts
Normal file
10
src/features/Content/removePromptScrollHide.ts
Normal 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';
|
||||
}
|
||||
};
|
||||
@ -171,7 +171,96 @@ export const useStyles = createStyles(
|
||||
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'] {
|
||||
height: var(--button-lg-height) !important;
|
||||
min-height: var(--button-lg-height) !important;
|
||||
@ -336,95 +425,6 @@ export const useStyles = createStyles(
|
||||
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`};
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
@ -1,116 +1,31 @@
|
||||
import { ActionIcon, DraggablePanelBody, DraggablePanelFooter } from '@lobehub/ui';
|
||||
import { useTimeout } from 'ahooks';
|
||||
import { Skeleton, Slider } from 'antd';
|
||||
import { consola } from 'consola';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
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 civitaiHelperFix from '@/scripts/civitaiHelperFix';
|
||||
import { useCivitaiHelperFix } from '@/features/ExtraNetworkSidebar/useCivitaiHelperFix';
|
||||
import { useInjectExtraNetwork } from '@/features/ExtraNetworkSidebar/useInjectExtraNetwork';
|
||||
import { selectors, useAppStore } from '@/store';
|
||||
|
||||
const Inner = memo(() => {
|
||||
const txt2imgExtraNetworkSidebarReference = useRef<HTMLDivElement>(null);
|
||||
const img2imgExtraNetworkSidebarReference = useRef<HTMLDivElement>(null);
|
||||
const [extraLoading, setExtraLoading] = useState(true);
|
||||
const txt2imgExtraNetworkSidebarReference = useInjectExtraNetwork('txt');
|
||||
const img2imgExtraNetworkSidebarReference = useInjectExtraNetwork('img');
|
||||
const setting = useAppStore(selectors.currentSetting, isEqual);
|
||||
const currentTab = useAppStore(selectors.currentTab);
|
||||
const [size, setSize] = useState<number>(setting.extraNetworkCardSize || 86);
|
||||
const { styles } = useStyles({ size });
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
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);
|
||||
const { isLoading } = useCivitaiHelperFix({
|
||||
debug: '[layout] inject - ExtraNetworkSidebar',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<DraggablePanelBody className={styles.body}>
|
||||
{extraLoading && <Skeleton active />}
|
||||
<div style={extraLoading ? { display: 'none' } : {}}>
|
||||
{isLoading && <Skeleton active />}
|
||||
<div style={isLoading ? { display: 'none' } : {}}>
|
||||
<div
|
||||
id="txt2img-extra-network-sidebar"
|
||||
ref={txt2imgExtraNetworkSidebarReference}
|
||||
|
||||
6
src/features/ExtraNetworkSidebar/refreshExtraNetwork.ts
Normal file
6
src/features/ExtraNetworkSidebar/refreshExtraNetwork.ts
Normal 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();
|
||||
};
|
||||
@ -11,6 +11,10 @@ export const useStyles = createStyles(
|
||||
#img2img_extra_search {
|
||||
width: 100% !important;
|
||||
max-width: unset !important;
|
||||
|
||||
textarea {
|
||||
height: unset !important;
|
||||
}
|
||||
}
|
||||
|
||||
#txt2img-extra-network-sidebar,
|
||||
|
||||
60
src/features/ExtraNetworkSidebar/useCivitaiHelperFix.ts
Normal file
60
src/features/ExtraNetworkSidebar/useCivitaiHelperFix.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
21
src/features/ExtraNetworkSidebar/useInjectExtraNetwork.ts
Normal file
21
src/features/ExtraNetworkSidebar/useInjectExtraNetwork.ts
Normal 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;
|
||||
};
|
||||
@ -1,7 +1,7 @@
|
||||
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 = [
|
||||
{
|
||||
@ -37,17 +37,23 @@ export const Resources = [
|
||||
];
|
||||
|
||||
export const Community = [
|
||||
{
|
||||
icon: <Icon icon={Heart} size="small" />,
|
||||
openExternal: true,
|
||||
title: 'Sponsor',
|
||||
url: `https://opencollective.com/lobehub`,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={Bug} size="small" />,
|
||||
openExternal: true,
|
||||
title: 'Report Bug',
|
||||
url: `${homepage}/issues/new/choose`,
|
||||
url: `${GITHUB_REPO_URL}/issues/new/choose`,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={GitFork} size="small" />,
|
||||
openExternal: true,
|
||||
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" />,
|
||||
openExternal: true,
|
||||
title: 'GitHub',
|
||||
url: homepage,
|
||||
url: GITHUB_REPO_URL,
|
||||
},
|
||||
{
|
||||
icon: <Icon icon={FileClock} size="small" />,
|
||||
openExternal: true,
|
||||
title: 'Changelog',
|
||||
url: `${homepage}/blob/main/CHANGELOG.md`,
|
||||
url: `${GITHUB_REPO_URL}/blob/main/CHANGELOG.md`,
|
||||
},
|
||||
];
|
||||
|
||||
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',
|
||||
openExternal: true,
|
||||
@ -86,9 +98,9 @@ export const MoreProducts = [
|
||||
url: 'https://ui.lobehub.com',
|
||||
},
|
||||
{
|
||||
description: 'AI Commit CLI',
|
||||
description: 'I18n AI Workflow',
|
||||
openExternal: true,
|
||||
title: '💌 Lobe Commit',
|
||||
url: 'https://github.com/lobehub/lobe-commit',
|
||||
title: '🌐 Lobe i18n',
|
||||
url: 'https://github.com/lobehub/lobe-cli-toolbox',
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { Footer as F } from '@lobehub/ui';
|
||||
import { consola } from 'consola';
|
||||
import isEqual from 'fast-deep-equal';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useInject } from '@/hooks/useInject';
|
||||
import { selectors, useAppStore } from '@/store';
|
||||
import { type DivProps } from '@/types';
|
||||
|
||||
@ -16,23 +16,19 @@ const Footer = memo<DivProps>(({ className, ...props }) => {
|
||||
const { t } = useTranslation();
|
||||
const footerReference = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const footer = gradioApp().querySelector('#footer');
|
||||
if (footer) footerReference.current?.append(footer);
|
||||
if (setting.confirmPageUnload) {
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
if (footer?.isConnected) {
|
||||
event.preventDefault();
|
||||
return (event.returnValue = '');
|
||||
}
|
||||
});
|
||||
}
|
||||
consola.success('🤯 [layout] inject - Footer');
|
||||
} catch (error) {
|
||||
consola.error('🤯 [layout] inject - Footer', error);
|
||||
}
|
||||
}, []);
|
||||
useInject(footerReference, '#footer', {
|
||||
debug: '[layout] inject - Footer',
|
||||
onSuccess: (footer) => {
|
||||
if (!setting.confirmPageUnload) return;
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
if (footer?.isConnected) {
|
||||
event.preventDefault();
|
||||
return (event.returnValue = '');
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={cx(styles.footer, className)} {...props}>
|
||||
<F
|
||||
|
||||
@ -1,79 +1,16 @@
|
||||
import { Burger, TabsNav, type TabsNavProps } from '@lobehub/ui';
|
||||
import { Burger, TabsNav } from '@lobehub/ui';
|
||||
import { useResponsive } from 'antd-style';
|
||||
import { consola } from 'consola';
|
||||
import { startCase } from 'lodash-es';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
|
||||
import { selectors, useAppStore } from '@/store';
|
||||
|
||||
const hideOriganlNav = () => {
|
||||
(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)),
|
||||
};
|
||||
});
|
||||
};
|
||||
import { useNavBar } from './useNavBar';
|
||||
|
||||
const Nav = memo(() => {
|
||||
const currentTab = useAppStore(selectors.currentTab);
|
||||
const { mobile } = useResponsive();
|
||||
const { items, onChange } = useNavBar(mobile);
|
||||
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} />;
|
||||
|
||||
|
||||
32
src/features/Header/genNavList.ts
Normal file
32
src/features/Header/genNavList.ts
Normal 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)),
|
||||
};
|
||||
});
|
||||
};
|
||||
@ -3,10 +3,10 @@ import { useTheme } from 'antd-style';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { Logo } from '@/components';
|
||||
import { GITHUB_REPO_URL } from '@/const/url';
|
||||
import { useAppStore } from '@/store';
|
||||
import { type DivProps } from '@/types';
|
||||
|
||||
import { homepage, name } from '../../../package.json';
|
||||
import Actions from './Actions';
|
||||
import Nav from './Nav';
|
||||
|
||||
@ -23,12 +23,12 @@ const Header = memo<DivProps>(({ children }) => {
|
||||
actionsStyle={{ flex: 0 }}
|
||||
logo={
|
||||
<a
|
||||
href={`${homepage}/releases`}
|
||||
href={`${GITHUB_REPO_URL}/releases`}
|
||||
rel="noreferrer"
|
||||
style={{ alignItems: 'center', color: theme.colorText, display: 'flex' }}
|
||||
target="_blank"
|
||||
>
|
||||
<Tooltip title={`${name} v${version}`}>
|
||||
<Tooltip title={`LobeTheme v${version}`}>
|
||||
<Logo />
|
||||
</Tooltip>
|
||||
</a>
|
||||
|
||||
40
src/features/Header/useNavBar.tsx
Normal file
40
src/features/Header/useNavBar.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
12
src/features/PromptFormator/createButton.ts
Normal file
12
src/features/PromptFormator/createButton.ts
Normal 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;
|
||||
};
|
||||
11
src/features/PromptFormator/index.tsx
Normal file
11
src/features/PromptFormator/index.tsx
Normal 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;
|
||||
11
src/features/PromptFormator/useInjectPromptFormator.ts
Normal file
11
src/features/PromptFormator/useInjectPromptFormator.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
@ -1,12 +1,12 @@
|
||||
import { DraggablePanelBody } from '@lobehub/ui';
|
||||
import { Segmented } from 'antd';
|
||||
import { useTheme } from 'antd-style';
|
||||
import { consola } from 'consola';
|
||||
import { memo, useEffect, useRef, useState } from 'react';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import { PromptEditor } from '@/components';
|
||||
import { useInject } from '@/hooks/useInject';
|
||||
import { type DivProps } from '@/types';
|
||||
|
||||
enum Tabs {
|
||||
@ -19,15 +19,10 @@ const Inner = memo<DivProps>(() => {
|
||||
const [tab, setTab] = useState<Tabs>(Tabs.Setting);
|
||||
const sidebarReference = useRef<HTMLDivElement>(null);
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
try {
|
||||
const sidebar = gradioApp().querySelector('#quicksettings');
|
||||
if (sidebar) sidebarReference.current?.append(sidebar);
|
||||
consola.success('🤯 [layout] inject - QuickSettingSidebar');
|
||||
} catch (error) {
|
||||
consola.error('🤯 [layout] inject - QuickSettingSidebar', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useInject(sidebarReference, '#quicksettings', {
|
||||
debug: '[layout] inject - QuickSettingSidebar',
|
||||
});
|
||||
|
||||
return (
|
||||
<DraggablePanelBody>
|
||||
|
||||
@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import VersionTag from '@/components/VersionTag';
|
||||
import { GITHUB_REPO_URL } from '@/const/url';
|
||||
|
||||
import { homepage } from '../../../package.json';
|
||||
import FormAppearance from './Form/Appearance';
|
||||
import FormExperimental from './Form/Experimental';
|
||||
import FormLayout from './Form/Layout';
|
||||
@ -29,7 +29,7 @@ const Setting = memo<SettingProps>(({ open, onCancel }) => {
|
||||
title={
|
||||
<Flexbox align={'center'} gap={4}>
|
||||
<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" />
|
||||
</a>
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import { PropsWithChildren, memo, useCallback, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Flexbox } from 'react-layout-kit';
|
||||
|
||||
import pkg from '@/../package.json';
|
||||
import { GITHUB_REPO_URL } from '@/const/url';
|
||||
|
||||
import { useStyles } from './style';
|
||||
|
||||
@ -99,7 +99,7 @@ const Preview = memo<PreviewProps>(({ imageType, withBackground, withFooter, chi
|
||||
{withFooter ? (
|
||||
<Flexbox align={'center'} className={styles.footer} gap={4}>
|
||||
<Logo extra={'SD'} type={'combine'} />
|
||||
<div className={styles.url}>{pkg.homepage}</div>
|
||||
<div className={styles.url}>{GITHUB_REPO_URL}</div>
|
||||
</Flexbox>
|
||||
) : (
|
||||
<div />
|
||||
|
||||
@ -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));
|
||||
};
|
||||
10
src/features/Share/createButton.ts
Normal file
10
src/features/Share/createButton.ts
Normal 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;
|
||||
};
|
||||
@ -1,25 +1,21 @@
|
||||
import { Modal } from '@lobehub/ui';
|
||||
import { consola } from 'consola';
|
||||
import { memo, useCallback, useEffect, useState } from 'react';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useInject } from '@/hooks/useInject';
|
||||
|
||||
import Inner from './Inner';
|
||||
import addShareButton from './addShareButton';
|
||||
import { createButton } from './createButton';
|
||||
|
||||
const Share = memo<{ type: 'txt' | 'img' }>(({ type }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const handleShare = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
const buttonReference = useRef<any>(createButton(type, setOpen));
|
||||
const { t } = useTranslation();
|
||||
useEffect(() => {
|
||||
try {
|
||||
addShareButton(type, handleShare);
|
||||
consola.success(`🤯 [layout] inject - Share ${type}`);
|
||||
} catch (error) {
|
||||
consola.error(`🤯 [layout] inject - Share ${type}`, error);
|
||||
}
|
||||
}, [type]);
|
||||
|
||||
useInject(buttonReference, `#image_buttons_${type}2img > .form`, {
|
||||
debug: `[layout] inject - Share ${type}`,
|
||||
inverse: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
55
src/hooks/useInject.ts
Normal file
55
src/hooks/useInject.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
9
src/hooks/useSelectorHide.ts
Normal file
9
src/hooks/useSelectorHide.ts
Normal 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';
|
||||
}, []);
|
||||
};
|
||||
5
src/hooks/useSelectorRef.ts
Normal file
5
src/hooks/useSelectorRef.ts
Normal 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);
|
||||
};
|
||||
@ -1,16 +1,13 @@
|
||||
import { memo, useEffect } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { useObserver } from '@/hooks/useObserver';
|
||||
import { useSelectorHide } from '@/hooks/useSelectorHide';
|
||||
|
||||
import InfoBox from './features/InfoBox';
|
||||
|
||||
const Index = memo<{ parentId: string }>(({ parentId }) => {
|
||||
const value = useObserver(`${parentId} .infotext`, { subSelector: 'p' });
|
||||
|
||||
useEffect(() => {
|
||||
const infoContainer = gradioApp().querySelector(`${parentId} .infotext`) as HTMLDivElement;
|
||||
infoContainer.style.display = 'none';
|
||||
}, []);
|
||||
useSelectorHide(`${parentId} .infotext`);
|
||||
|
||||
return <InfoBox value={value} />;
|
||||
});
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
@ -1,30 +1,4 @@
|
||||
import { consola } from 'consola';
|
||||
|
||||
/**
|
||||
* 转换器工具类
|
||||
*/
|
||||
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 {
|
||||
const re_attention = /\{|\[|\}|\]|[^{}[\]]+/gmu;
|
||||
|
||||
@ -92,7 +66,7 @@ export const Converter = {
|
||||
for (const [word, value] of res) {
|
||||
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 事件
|
||||
* @param target 目标元素
|
||||
@ -248,14 +204,14 @@ export const Converter = {
|
||||
const default_negative = '';
|
||||
|
||||
const prompt = gradioApp().querySelector(
|
||||
`#${type}_prompt > label > textarea`,
|
||||
`#${type}2img_prompt > label > textarea`,
|
||||
) as HTMLTextAreaElement;
|
||||
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`,
|
||||
`#${type}2img_neg_prompt > label > textarea`,
|
||||
) as HTMLTextAreaElement;
|
||||
const negResult = Converter.convert(negprompt.value);
|
||||
negprompt.value =
|
||||
@ -276,9 +232,3 @@ export const Converter = {
|
||||
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
98
src/store/action.test.ts
Normal 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...
|
||||
// });
|
||||
@ -2,7 +2,8 @@ import type { SelectProps } from 'antd';
|
||||
import semver from 'semver';
|
||||
|
||||
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';
|
||||
|
||||
@ -66,7 +67,10 @@ export const getLocaleOptions = async(): Promise<SelectProps['options']> => {
|
||||
|
||||
export const getLatestVersion = async(): Promise<string> => {
|
||||
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;
|
||||
if (!data || !data.tag_name) return DEFAULT_VERSION;
|
||||
|
||||
12
tests/setup.ts
Normal file
12
tests/setup.ts
Normal 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;
|
||||
@ -1,14 +1,18 @@
|
||||
import { resolve } from 'node:path';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
import { name } from './package.json';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
alias: {
|
||||
'@': './src',
|
||||
[name]: './src',
|
||||
'@': resolve(__dirname, './src'),
|
||||
},
|
||||
coverage: {
|
||||
all: false,
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'json', 'lcov', 'text-summary'],
|
||||
},
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: './tests/setup.ts',
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user