♻️ 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:
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 .
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-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",

View File

@ -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

View File

@ -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>
);

View File

@ -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
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 { 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 />}
</>
);

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;
}
`,
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`};
}
}
`,
};
},
);

View File

@ -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}

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 {
width: 100% !important;
max-width: unset !important;
textarea {
height: unset !important;
}
}
#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 { 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',
},
];

View File

@ -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

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 { 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} />;

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 { 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>

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 { 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>

View File

@ -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>

View File

@ -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 />

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 { 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
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 { 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} />;
});

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 = {
/**
*
* @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
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 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
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 { 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',
},
});