feat(i18n): Support i18n and welcome PR contributions

pull/240/head
canisminor1990 2023-06-30 00:48:42 +08:00
parent fe32de2204
commit a61f3b4d30
41 changed files with 1060 additions and 368 deletions

View File

@ -56,6 +56,7 @@
- [x] 🎛️ 高定制侧边栏,左侧为快捷设置侧边栏,右侧为模型侧边栏
- [x] 🖼️ 可调节画板比例,使生成图像始终置顶
- [x] 📱 移动端友好,针对手机屏幕完成部分优化
- [x] 🇨🇳 支持 i18n 并欢迎提交 [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) 贡献
- [ ] 📝 语法高亮的 Prompt 输入框
- [ ] 🆗 i18n 多语言支持

View File

@ -56,6 +56,7 @@ English · [简体中文](./README-zh_CN.md) · [Changelog](./CHANGELOG.md) · [
- [x] 🎛️ Highly customizable sidebar, with a quick settings sidebar on the left and a model sidebar on the right
- [x] 🖼️ Adjustable canvas ratio, ensuring that generated images are always displayed at the top
- [x] 📱 Mobile-friendly, with partial optimization for mobile screens
- [x] 🇨🇳 Support i18n and welcome [PR](https://github.com/canisminor1990/sd-webui-lobe-theme/tree/main/src/i18n/lang) contributions
- [ ] 📝 Syntax highlighting in the prompt input box
- [ ] 🆗 Multilingual support with i18n

View File

@ -83,6 +83,7 @@
"eslint": "^8",
"fast-deep-equal": "^3",
"husky": "^8",
"i18next": "^23",
"lightningcss": "^1",
"lint-staged": "^13",
"lodash-es": "^4",
@ -95,6 +96,7 @@
"react": "^18",
"react-dom": "^18",
"react-helmet": "^6",
"react-i18next": "^13",
"react-layout-kit": "^1",
"react-rnd": "^10",
"react-tag-input": "^6",

View File

@ -1,4 +1,4 @@
import { memo, useEffect, useState } from 'react';
import { StrictMode, Suspense, memo, useEffect, useState } from 'react';
import { Helmet } from 'react-helmet';
import { shallow } from 'zustand/shallow';
@ -7,6 +7,8 @@ import Index from '@/pages';
import Loading from '@/slots/Loading';
import { useAppStore } from '@/store';
import './i18n/config';
const App = memo(() => {
const [loading, setLoading] = useState(true);
const setCurrentTab = useAppStore((st) => st.setCurrentTab, shallow);
@ -22,41 +24,43 @@ const App = memo(() => {
}, []);
return (
<>
<Helmet>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/apple-touch-icon.png"
rel="apple-touch-icon"
sizes="180x180"
/>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/favicon-16x16.png"
rel="icon"
sizes="16x16"
type="image/png"
/>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/site.webmanifest"
rel="manifest"
/>
<link
color="#000000"
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/safari-pinned-tab.svg"
rel="mask-icon"
/>
<meta content="LobeHub" name="apple-mobile-web-app-title" />
<meta content="LobeHub" name="application-name" />
<meta content="#000000" name="msapplication-TileColor" />
<meta content="#000000" name="theme-color" />
</Helmet>
<Layout>{loading ? <Loading /> : <Index />}</Layout>
</>
<StrictMode>
<Suspense fallback={<Loading />}>
<Helmet>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/apple-touch-icon.png"
rel="apple-touch-icon"
sizes="180x180"
/>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/favicon-32x32.png"
rel="icon"
sizes="32x32"
type="image/png"
/>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/favicon-16x16.png"
rel="icon"
sizes="16x16"
type="image/png"
/>
<link
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/site.webmanifest"
rel="manifest"
/>
<link
color="#000000"
href="https://npm.elemecdn.com/@lobehub/assets-favicons/assets/safari-pinned-tab.svg"
rel="mask-icon"
/>
<meta content="LobeHub" name="apple-mobile-web-app-title" />
<meta content="LobeHub" name="application-name" />
<meta content="#000000" name="msapplication-TileColor" />
<meta content="#000000" name="theme-color" />
</Helmet>
<Layout>{loading ? <Loading /> : <Index />}</Layout>
</Suspense>
</StrictMode>
);
});

View File

@ -1,6 +1,6 @@
import { memo } from 'react';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import { useStyles } from './style';

View File

@ -1,6 +1,6 @@
import { memo } from 'react';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import { useStyles } from './style';

View File

@ -1,6 +1,6 @@
import { memo } from 'react';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import { useStyles } from './style';

View File

@ -2,7 +2,7 @@ import { ActionIcon } from '@lobehub/ui';
import { PanelLeft, Pin, PinOff } from 'lucide-react';
import { memo } from 'react';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import { useStyles } from './style';

31
src/i18n/config.ts Normal file
View File

@ -0,0 +1,31 @@
import i18next from 'i18next';
import { initReactI18next } from 'react-i18next';
import en from './lang/en';
import ja_JP from './lang/ja_JP';
import ko_KR from './lang/ko_KR';
import zh_CN from './lang/zh_CN';
import zh_HK from './lang/zh_HK';
i18next.use(initReactI18next).init({
debug: process.env.NODE_ENV === 'development',
fallbackLng: 'en',
lng: 'en',
resources: {
en: {
translation: en,
},
ja_JP: {
translation: ja_JP,
},
ko_KR: {
translation: ko_KR,
},
zh_CN: {
translation: zh_CN,
},
zh_HK: {
translation: zh_HK,
},
},
});

20
src/i18n/index.ts Normal file
View File

@ -0,0 +1,20 @@
import { type SelectProps } from 'antd';
export const i18nOptions: SelectProps['options'] = [
{
label: 'English',
value: 'en',
},
{
label: '简体中文',
value: 'zh_CN',
},
{
label: '繁體中文',
value: 'zh_HK',
},
{
label: '日本語',
value: 'ja_JP',
},
];

85
src/i18n/lang/en.ts Normal file
View File

@ -0,0 +1,85 @@
const translation = {
community: 'Community',
custom: 'Custom',
extraNetwork: 'Extra Network',
feedback: 'Feedback',
fixed: 'Fixed',
float: 'Float',
help: 'Help',
kitchen: 'Kitchen',
loadPrompt: 'Load Prompt',
lobe: 'Lobe',
moreProducts: 'More Products',
negative: 'Negative',
positive: 'Positive',
quickSetting: 'Quick Setting',
resizable: 'Resizable',
resources: 'Resources',
scroll: 'Scroll',
setPrompt: 'Set Prompt',
setting: 'Setting',
settingButtomReset: 'Reset',
settingButtomSubmit: 'Apply and Restart Interface',
settingCustomLogo: 'Custom Logo',
settingCustomLogoDesc: 'Support URL / Base64 / Emoji symbols',
settingCustomTitle: 'Custom Title',
settingCustomTitleDesc: 'Custom Logo Title',
settingExtraNetworkSidebarDefaultCardSize: 'Model Cover Size',
settingExtraNetworkSidebarDefaultCardSizeDesc: 'Default value of model cover size when starting',
settingExtraNetworkSidebarDefaultExpand: 'Default Expand',
settingExtraNetworkSidebarDefaultExpandDesc:
'Whether to expand the sidebar by default when starting',
settingExtraNetworkSidebarDefaultWidth: 'Default Width',
settingExtraNetworkSidebarDefaultWidthDesc: 'Default width of the sidebar when starting',
settingExtraNetworkSidebarDisplayMode: 'Display Mode',
settingExtraNetworkSidebarDisplayModeDesc:
'Fixed as grid mode for constant display, auto-expand when the mouse moves to the side in floating mode',
settingExtraNetworkSidebarEnable: 'Enable',
settingExtraNetworkSidebarEnableDesc: 'Enable the extra network sidebar on the right side',
settingGroupExtraNetworkSidebar: 'Extra Network Sidebar',
settingGroupLayout: 'Layout Settings',
settingGroupPromotTextarea: 'Prompt Textbox',
settingGroupQuickSettingSidebar: 'Quick Setting Sidebar',
settingGroupTheme: 'Theme Settings',
settingHideFooter: 'Hide Footer',
settingHideFooterDesc:
'Hide the theme footer and only display the default footer of stable diffusion webui',
settingLanguage: 'Language',
settingLanguageDesc: 'Lobe Theme language',
settingLogoPreview: 'Preview',
settingLogoType: 'Logo Type',
settingLogoTypeDesc: 'Logo Type',
settingNeutralColor: 'Neutral Color',
settingNeutralColorDesc:
'Customize different shades of gray with different color tendencies, the second one is the original Kitchen neutral color',
settingPrimaryColor: 'Primary Color',
settingPrimaryColorDesc:
'Custom primary color, the second one is the original Kitchen theme color',
settingPromptDisplayMode: 'Display Mode',
settingPromptDisplayModeDesc: 'Fixed height or auto height with draggable resize support',
settingPromptEditor: 'Prompt Editor',
settingPromptEditorDesc: 'Provide a simple prompt editor at the top of the quick setting sidebar',
settingQuickSettingSidebarDefaultExpand: 'Default Expand',
settingQuickSettingSidebarDefaultExpandDesc:
'Whether to expand the sidebar by default when starting',
settingQuickSettingSidebarDefaultWidth: 'Default Width',
settingQuickSettingSidebarDefaultWidthDesc: 'Default width of the sidebar when starting',
settingQuickSettingSidebarDisplayMode: 'Display Mode',
settingQuickSettingSidebarDisplayModeDesc:
'Fixed as grid mode for constant display, auto-expand when the mouse moves to the side in floating mode',
settingQuickSettingSidebarEnable: 'Enable',
settingQuickSettingSidebarEnableDesc: 'Enable the quick setting sidebar on the left side',
settingReduceAnimation: 'Reduce Animation',
settingReduceAnimationDesc:
'Reduce the blur effect and background flow color, which can improve smoothness and save CPU usage',
settingSplitPreviewer: 'Split Previewer',
settingSplitPreviewerDesc:
'Put the prompt input box on the left and the generate button on the right, ensuring that the generated image is always displayed at the top when scrolling (experimental)',
settingSvgIcons: 'Use SVG Icons',
settingSvgIconsDesc: 'Replace all Emoji icons in stable diffusion webui with SVG icons globally',
switchTheme: 'Switch Light/Dark Theme',
themeFeedback: 'Theme Feedback',
themeSetting: 'Theme Settings',
};
export default translation;

84
src/i18n/lang/ja_JP.ts Normal file
View File

@ -0,0 +1,84 @@
import type { Translation } from '@/types';
const translation: Translation = {
community: 'コミュニティ',
custom: 'カスタム',
extraNetwork: '追加ネットワーク',
feedback: 'フィードバック',
fixed: '固定',
float: 'フロート',
help: 'ヘルプ',
kitchen: 'キッチン',
loadPrompt: 'プロンプトをロード',
lobe: 'ローブ',
moreProducts: 'その他の製品',
negative: 'ネガティブなヒント',
positive: 'ポジティブなヒント',
quickSetting: 'クイック設定',
resizable: 'リサイズ可能',
resources: '関連リソース',
scroll: 'スクロール',
setPrompt: 'プロンプトを設定',
setting: '設定',
settingButtomReset: 'リセット',
settingButtomSubmit: '適用して再起動',
settingCustomLogo: 'カスタムロゴ',
settingCustomLogoDesc: 'URL / Base64 / 絵文字をサポート',
settingCustomTitle: 'カスタムタイトル',
settingCustomTitleDesc: 'カスタムロゴのタイトル名',
settingExtraNetworkSidebarDefaultCardSize: 'モデルカバーサイズ',
settingExtraNetworkSidebarDefaultCardSizeDesc: '起動時のモデルカバーサイズのデフォルト値',
settingExtraNetworkSidebarDefaultExpand: 'デフォルトで展開',
settingExtraNetworkSidebarDefaultExpandDesc: '起動時にサイドバーをデフォルトで展開しますか?',
settingExtraNetworkSidebarDefaultWidth: 'デフォルト幅',
settingExtraNetworkSidebarDefaultWidthDesc: '起動時のサイドバーのデフォルト幅',
settingExtraNetworkSidebarDisplayMode: '表示モード',
settingExtraNetworkSidebarDisplayModeDesc:
'グリッドモードで常に表示するか、ホバー時に自動的に展開するフロートモードで表示するか',
settingExtraNetworkSidebarEnable: '有効にする',
settingExtraNetworkSidebarEnableDesc: '右側の追加ネットワークサイドバーを有効にする',
settingGroupExtraNetworkSidebar: '追加ネットワークサイドバー',
settingGroupLayout: 'レイアウト設定',
settingGroupPromotTextarea: 'プロンプトテキストエリア',
settingGroupQuickSettingSidebar: 'クイック設定サイドバー',
settingGroupTheme: 'テーマ設定',
settingHideFooter: 'フッターを非表示にする',
settingHideFooterDesc:
'テーマのフッターを非表示にし、stable diffusion webui のデフォルトフッターのみ表示します',
settingLanguage: '言語',
settingLanguageDesc: 'Lobe Themeの言語',
settingLogoPreview: 'プレビュー',
settingLogoType: 'ロゴタイプ',
settingLogoTypeDesc: 'ロゴタイプ',
settingNeutralColor: '中立色',
settingNeutralColorDesc:
'異なる色相のグレースケールのカスタマイズ。2番目は元のKitchenの中立色です',
settingPrimaryColor: 'プライマリカラー',
settingPrimaryColorDesc: 'カスタムプライマリカラー。2番目は元のKitchenのプライマリカラーです',
settingPromptDisplayMode: '表示モード',
settingPromptDisplayModeDesc: '固定の高さまたはドラッグリサイズをサポートする自動の高さ',
settingPromptEditor: 'プロンプトエディタ',
settingPromptEditorDesc: 'クイック設定サイドバーの上部に簡単なプロンプトエディタを提供します',
settingQuickSettingSidebarDefaultExpand: 'デフォルトで展開',
settingQuickSettingSidebarDefaultExpandDesc: '起動時にサイドバーをデフォルトで展開しますか?',
settingQuickSettingSidebarDefaultWidth: 'デフォルト幅',
settingQuickSettingSidebarDefaultWidthDesc: '起動時のサイドバーのデフォルト幅',
settingQuickSettingSidebarDisplayMode: '表示モード',
settingQuickSettingSidebarDisplayModeDesc:
'グリッドモードで常に表示するか、ホバー時に自動的に展開するフロートモードで表示するか',
settingQuickSettingSidebarEnable: '有効にする',
settingQuickSettingSidebarEnableDesc: '左側のクイック設定サイドバーを有効にする',
settingReduceAnimation: 'アニメーションを削減',
settingReduceAnimationDesc:
'ガラスのエフェクトと背景の流れる色を削減し、スムーズさを向上させ、CPUの使用量を節約できます',
settingSplitPreviewer: '2列モード',
settingSplitPreviewerDesc:
'プロンプト入力ボックスを左側に配置し、生成ボタンを右側に配置し、スクロール時に生成された画像が常にトップに表示されるようにします(実験的)',
settingSvgIcons: 'SVGアイコンを使用',
settingSvgIconsDesc: 'stable diffusion webuiの絵文字アイコンをすべてSVGアイコンに置き換えます',
switchTheme: '明暗テーマを切り替える',
themeFeedback: 'テーマのフィードバック',
themeSetting: 'テーマ設定',
};
export default translation;

84
src/i18n/lang/ko_KR.ts Normal file
View File

@ -0,0 +1,84 @@
import type { Translation } from '@/types';
const translation: Translation = {
community: '커뮤니티',
custom: '사용자 정의',
extraNetwork: '추가 네트워크',
feedback: '피드백',
fixed: '고정',
float: '부유',
help: '도움말',
kitchen: 'Kitchen',
loadPrompt: '로드 프롬프트',
lobe: 'Lobe',
moreProducts: '더 많은 제품',
negative: '부정적인',
positive: '긍정적인',
quickSetting: '빠른 설정',
resizable: '크기 조절 가능',
resources: '관련 자료',
scroll: '스크롤',
setPrompt: '프롬프트 설정',
setting: '설정',
settingButtomReset: '재설정',
settingButtomSubmit: '적용 및 인터페이스 재시작',
settingCustomLogo: '사용자 정의 로고',
settingCustomLogoDesc: 'URL / Base64 / 이모지 표정을 지원합니다.',
settingCustomTitle: '사용자 정의 제목',
settingCustomTitleDesc: '사용자 정의 로고 제목',
settingExtraNetworkSidebarDefaultCardSize: '모델 커버 크기',
settingExtraNetworkSidebarDefaultCardSizeDesc: '시작시 모델 커버 크기 기본값',
settingExtraNetworkSidebarDefaultExpand: '기본 확장',
settingExtraNetworkSidebarDefaultExpandDesc: '시작시 사이드바 기본 확장 여부',
settingExtraNetworkSidebarDefaultWidth: '기본 너비',
settingExtraNetworkSidebarDefaultWidthDesc: '시작시 사이드바 기본 너비',
settingExtraNetworkSidebarDisplayMode: '표시 모드',
settingExtraNetworkSidebarDisplayModeDesc:
'그리드 모드로 고정하여 항상 표시하거나, 부유 모드로 설정하여 사이드바에 마우스를 가져가면 자동으로 확장',
settingExtraNetworkSidebarEnable: '사용',
settingExtraNetworkSidebarEnableDesc: '오른쪽에 추가 네트워크 사이드바 활성화',
settingGroupExtraNetworkSidebar: '추가 네트워크 사이드바',
settingGroupLayout: '레이아웃 설정',
settingGroupPromotTextarea: '프롬프트 텍스트 영역',
settingGroupQuickSettingSidebar: '빠른 설정 사이드바',
settingGroupTheme: '테마 설정',
settingHideFooter: '푸터 숨기기',
settingHideFooterDesc: '테마 푸터를 숨기고 stable diffusion webui의 기본 푸터만 표시',
settingLanguage: '언어',
settingLanguageDesc: 'Lobe Theme 테마 언어',
settingLogoPreview: '미리보기',
settingLogoType: '로고 유형',
settingLogoTypeDesc: '로고 유형',
settingNeutralColor: '중립색',
settingNeutralColorDesc:
'다른 색상 경향의 그레이 스케일 사용자 정의, 두 번째는 원래 Kitchen의 중립색',
settingPrimaryColor: '기본 색상',
settingPrimaryColorDesc: '사용자 정의 기본 색상, 두 번째는 원래 Kitchen의 기본 색상',
settingPromptDisplayMode: '표시 모드',
settingPromptDisplayModeDesc: '고정 높이 또는 자동 높이 및 드래그 조절 지원',
settingPromptEditor: '프롬프트 편집기',
settingPromptEditorDesc: '빠른 설정 사이드바 상단에 간단한 프롬프트 편집기 제공',
settingQuickSettingSidebarDefaultExpand: '기본 확장',
settingQuickSettingSidebarDefaultExpandDesc: '시작시 사이드바 기본 확장 여부',
settingQuickSettingSidebarDefaultWidth: '기본 너비',
settingQuickSettingSidebarDefaultWidthDesc: '시작시 사이드바 기본 너비',
settingQuickSettingSidebarDisplayMode: '표시 모드',
settingQuickSettingSidebarDisplayModeDesc:
'그리드 모드로 고정하여 항상 표시하거나, 부유 모드로 설정하여 사이드바에 마우스를 가져가면 자동으로 확장',
settingQuickSettingSidebarEnable: '사용',
settingQuickSettingSidebarEnableDesc: '왼쪽에 빠른 설정 사이드바 활성화',
settingReduceAnimation: '애니메이션 줄이기',
settingReduceAnimationDesc:
'유리 효과와 배경 흐름 색상을 줄여서 부드러움을 향상시키고 CPU 사용량을 줄일 수 있습니다.',
settingSplitPreviewer: '이중 열 모드',
settingSplitPreviewerDesc:
'프롬프트 입력 상자를 왼쪽에 배치하고, 우측에 생성 버튼을 두어 스크롤 시 생성된 이미지가 항상 위에 표시되도록 합니다 (실험적)',
settingSvgIcons: 'SVG 아이콘 사용',
settingSvgIconsDesc:
'stable diffusion webui의 이모지 아이콘을 전역적으로 SVG 아이콘으로 교체합니다.',
switchTheme: '밝기 테마 전환',
themeFeedback: '테마 피드백',
themeSetting: '테마 설정',
};
export default translation;

81
src/i18n/lang/zh_CN.ts Normal file
View File

@ -0,0 +1,81 @@
import type { Translation } from '@/types';
const translation: Translation = {
community: '社区',
custom: '自定义',
extraNetwork: '附加网络',
feedback: '反馈',
fixed: '固定',
float: '悬浮',
help: '帮助',
kitchen: 'Kitchen',
loadPrompt: '加载提示',
lobe: 'Lobe',
moreProducts: '更多产品',
negative: '反向提示词',
positive: '正面提示词',
quickSetting: '快捷设置',
resizable: '缩放',
resources: '相关资源',
scroll: '滚动',
setPrompt: '发送提示词',
setting: '设置',
settingButtomReset: '重置',
settingButtomSubmit: '应用并重启界面',
settingCustomLogo: '自定义 Logo',
settingCustomLogoDesc: '支持 URL / Base64 / Emoji 表情符号',
settingCustomTitle: '自定义标题',
settingCustomTitleDesc: '自定义 Logo 标题名称',
settingExtraNetworkSidebarDefaultCardSize: '模型封面尺寸',
settingExtraNetworkSidebarDefaultCardSizeDesc: '启动时模型封面尺寸默认值',
settingExtraNetworkSidebarDefaultExpand: '默认展开',
settingExtraNetworkSidebarDefaultExpandDesc: '是否在启动时将侧边栏默认展开',
settingExtraNetworkSidebarDefaultWidth: '默认宽度',
settingExtraNetworkSidebarDefaultWidthDesc: '侧边栏在启动时的默认宽度',
settingExtraNetworkSidebarDisplayMode: '显示模式',
settingExtraNetworkSidebarDisplayModeDesc:
'固定为栅格模式常驻显示,悬浮模式时当鼠标移到侧边时自动展开',
settingExtraNetworkSidebarEnable: '启用',
settingExtraNetworkSidebarEnableDesc: '启用位于右侧的附加网络侧边栏',
settingGroupExtraNetworkSidebar: '附加网络侧边栏',
settingGroupLayout: '布局设置',
settingGroupPromotTextarea: '提示词文本框',
settingGroupQuickSettingSidebar: '快捷设置侧边栏',
settingGroupTheme: '主题设置',
settingHideFooter: '隐藏页脚',
settingHideFooterDesc: '隐藏主题页脚,只显示 stable diffusion webui 默认页脚',
settingLanguage: '语言',
settingLanguageDesc: 'Lobe Theme 主题语言',
settingLogoPreview: '预览',
settingLogoType: 'Logo 类型',
settingLogoTypeDesc: 'Logo 类型',
settingNeutralColor: '中性色',
settingNeutralColorDesc: '不同色彩倾向的灰阶自定义,第二个为原始 Kitchen 中性色',
settingPrimaryColor: '主题色',
settingPrimaryColorDesc: '自定义主题色,第二个为原始 Kitchen 主题色',
settingPromptDisplayMode: '显示模式',
settingPromptDisplayModeDesc: '固定高度或自动高度并支持拖拽拉伸',
settingPromptEditor: '提示词编辑器',
settingPromptEditorDesc: '提供简易的提示词编辑器位于快捷设置侧边栏顶部',
settingQuickSettingSidebarDefaultExpand: '默认展开',
settingQuickSettingSidebarDefaultExpandDesc: '是否在启动时将侧边栏默认展开',
settingQuickSettingSidebarDefaultWidth: '默认宽度',
settingQuickSettingSidebarDefaultWidthDesc: '侧边栏在启动时的默认宽度',
settingQuickSettingSidebarDisplayMode: '显示模式',
settingQuickSettingSidebarDisplayModeDesc:
'固定为栅格模式常驻显示,悬浮模式时当鼠标移到侧边时自动展开',
settingQuickSettingSidebarEnable: '启用',
settingQuickSettingSidebarEnableDesc: '启用位于左侧的快捷设置侧边栏',
settingReduceAnimation: '减少动画效果',
settingReduceAnimationDesc: '减少毛玻璃效果和背景流动色,可以提升流畅度并节省 CPU 使用',
settingSplitPreviewer: '双列模式',
settingSplitPreviewerDesc:
'将提示词输入框放在左侧,生成按钮于右侧,确保在滚动时生成的图像始终显示在顶部 (实验性)',
settingSvgIcons: '使用 SVG 图标',
settingSvgIconsDesc: '将 stable diffusion webui 中的 Emoji 图标全局替换为 SVG 图标',
switchTheme: '切换亮暗色主题',
themeFeedback: '主题反馈',
themeSetting: '主题设置',
};
export default translation;

81
src/i18n/lang/zh_HK.ts Normal file
View File

@ -0,0 +1,81 @@
import type { Translation } from '@/types';
const translation: Translation = {
community: '社區',
custom: '自訂',
extraNetwork: '附加網絡',
feedback: '反饋',
fixed: '固定',
float: '懸浮',
help: '幫助',
kitchen: 'Kitchen',
loadPrompt: '加載提示',
lobe: 'Lobe',
moreProducts: '更多產品',
negative: '反向提示詞',
positive: '正面提示詞',
quickSetting: '快捷設置',
resizable: '縮放',
resources: '相關資源',
scroll: '滾動',
setPrompt: '發送提示詞',
setting: '設置',
settingButtomReset: '重置',
settingButtomSubmit: '應用並重啟界面',
settingCustomLogo: '自定義 Logo',
settingCustomLogoDesc: '支持 URL / Base64 / Emoji 表情符號',
settingCustomTitle: '自定義標題',
settingCustomTitleDesc: '自定義 Logo 標題名稱',
settingExtraNetworkSidebarDefaultCardSize: '模型封面尺寸',
settingExtraNetworkSidebarDefaultCardSizeDesc: '啟動時模型封面尺寸默認值',
settingExtraNetworkSidebarDefaultExpand: '默認展開',
settingExtraNetworkSidebarDefaultExpandDesc: '是否在啟動時將側邊欄默認展開',
settingExtraNetworkSidebarDefaultWidth: '默認寬度',
settingExtraNetworkSidebarDefaultWidthDesc: '側邊欄在啟動時的默認寬度',
settingExtraNetworkSidebarDisplayMode: '顯示模式',
settingExtraNetworkSidebarDisplayModeDesc:
'固定為格模式常駐顯示,懸浮模式時當鼠標移到側邊時自動展開',
settingExtraNetworkSidebarEnable: '啟用',
settingExtraNetworkSidebarEnableDesc: '啟用位於右側的附加網絡側邊欄',
settingGroupExtraNetworkSidebar: '附加網絡側邊欄',
settingGroupLayout: '佈局設置',
settingGroupPromotTextarea: '提示詞文本框',
settingGroupQuickSettingSidebar: '快捷設置側邊欄',
settingGroupTheme: '主題設置',
settingHideFooter: '隱藏頁腳',
settingHideFooterDesc: '隱藏主題頁腳,只顯示 stable diffusion webui 默認頁腳',
settingLanguage: '語言',
settingLanguageDesc: 'Lobe Theme 主題語言',
settingLogoPreview: '預覽',
settingLogoType: 'Logo 類型',
settingLogoTypeDesc: 'Logo 類型',
settingNeutralColor: '中性色',
settingNeutralColorDesc: '不同色彩傾向的灰階自定義,第二個為原始 Kitchen 中性色',
settingPrimaryColor: '主題色',
settingPrimaryColorDesc: '自定義主題色,第二個為原始 Kitchen 主題色',
settingPromptDisplayMode: '顯示模式',
settingPromptDisplayModeDesc: '固定高度或自動高度並支持拖拽拉伸',
settingPromptEditor: '提示詞編輯器',
settingPromptEditorDesc: '提供簡易的提示詞編輯器位於快捷設置側邊欄頂部',
settingQuickSettingSidebarDefaultExpand: '默認展開',
settingQuickSettingSidebarDefaultExpandDesc: '是否在啟動時將側邊欄默認展開',
settingQuickSettingSidebarDefaultWidth: '默認寬度',
settingQuickSettingSidebarDefaultWidthDesc: '側邊欄在啟動時的默認寬度',
settingQuickSettingSidebarDisplayMode: '顯示模式',
settingQuickSettingSidebarDisplayModeDesc:
'固定為格模式常駐顯示,懸浮模式時當鼠標移到側邊時自動展開',
settingQuickSettingSidebarEnable: '啟用',
settingQuickSettingSidebarEnableDesc: '啟用位於左側的快捷設置側邊欄',
settingReduceAnimation: '減少動畫效果',
settingReduceAnimationDesc: '減少毛玻璃效果和背景流動色,可以提升流暢度並節省 CPU 使用',
settingSplitPreviewer: '雙列模式',
settingSplitPreviewerDesc:
'將提示詞輸入框放在左側,生成按鈕於右側,確保在滾動時生成的圖像始終顯示在頂部 (實驗性)',
settingSvgIcons: '使用 SVG 圖標',
settingSvgIconsDesc: '將 stable diffusion webui 中的 Emoji 圖標全局替換為 SVG 圖標',
switchTheme: '切換亮暗色主題',
themeFeedback: '主題反饋',
themeSetting: '主題設置',
};
export default translation;

View File

@ -1,4 +1,4 @@
import { DivProps, ThemeProvider, colors } from '@lobehub/ui';
import { type DivProps, ThemeProvider, colors } from '@lobehub/ui';
import {
generateColorNeutralPalette,
generateColorPalette,
@ -6,6 +6,7 @@ import {
import isEqual from 'fast-deep-equal';
import qs from 'query-string';
import { memo, useCallback, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { useIsDarkMode } from '@/hooks/useIsDarkMode';
@ -21,10 +22,14 @@ const Layout = memo<DivProps>(({ children }) => {
);
const setting = useAppStore((st) => st.setting, isEqual);
const isDarkMode = useIsDarkMode();
const { i18n } = useTranslation();
useEffect(() => {
onInit();
}, []);
useEffect(() => {
i18n.changeLanguage(setting.i18n);
}, [setting.i18n]);
useEffect(() => {
const queryTheme: any = String(qs.parseUrl(window.location.href).query.__theme || '');
if (queryTheme) {

View File

@ -5,7 +5,7 @@ import { memo, useEffect, useRef } from 'react';
import draggablePanel from '@/script/draggablePanel';
import formatPrompt from '@/script/formatPrompt';
import { useAppStore } from '@/store';
import { DivProps } from '@/types';
import { type DivProps } from '@/types';
import { useStyles as usePreviewStyles } from '../Preview/style';
import { useStyles } from './style';

View File

@ -17,6 +17,7 @@ const Inner = memo(() => {
const [setting, currentTab] = useAppStore((st) => [st.setting, st.currentTab], shallow);
const [size, setSize] = useState<number>(setting?.extraNetworkCardSize || 86);
const { styles } = useStyles({ size });
useEffect(() => {
console.time('🤯 [layout] inject - ExtraNetworkSidebar');
if (setting.enableExtraNetworkSidebar) {

View File

@ -2,10 +2,11 @@ import { DraggablePanel, LayoutSidebarInner } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SidebarContainer, SidebarHeader } from '@/components';
import { useAppStore } from '@/store';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import Inner from './Inner';
import { useStyles } from './style';
@ -20,6 +21,7 @@ const ExtraNetworkSidebar = memo<ExtraNetworkSidebarProps>(({ headerHeight }) =>
const [expand, setExpand] = useState<boolean>(mobile ? false : setting.extraNetworkSidebarExpand);
const [pin, setPin] = useState<boolean>(setting.extraNetworkFixedMode === 'fixed');
const { styles, theme } = useStyles({ headerHeight });
const { t } = useTranslation();
useEffect(() => {
const index2indexExtraNetworkButton = gradioApp().querySelector(
@ -63,7 +65,7 @@ const ExtraNetworkSidebar = memo<ExtraNetworkSidebarProps>(({ headerHeight }) =>
position="right"
setExpand={setExpand}
setPin={setPin}
title="ExraNetworks"
title={t('extraNetwork')}
/>
<Inner />
</SidebarContainer>

View File

@ -1,100 +1,88 @@
import { Icon } from '@lobehub/ui';
import { FooterProps as FProps } from '@lobehub/ui/es/Footer';
import { Bug, FileClock, GitFork, Github } from 'lucide-react';
import { homepage } from '@/../package.json';
export const columns: FProps['columns'] = [
export const Resources = [
{
items: [
{
description: 'AUTOMATIC111',
openExternal: true,
title: 'Stable Diffusion Webui',
url: 'https://github.com/AUTOMATIC1111/stable-diffusion-webui',
},
{
description: 'WebUI extension',
openExternal: true,
title: 'Controlnet',
url: 'https://github.com/Mikubill/sd-webui-controlnet',
},
{
description: 'Art models',
openExternal: true,
title: 'Civitai',
url: 'https://civitai.com/',
},
{
description: 'Artist Inspired Styles',
openExternal: true,
title: 'Cheat Sheet',
url: 'https://supagruen.github.io/StableDiffusion-CheatSheet',
},
{
description: 'Image Resizing',
openExternal: true,
title: 'Birme',
url: 'https://www.birme.net/?target_width=512&target_height=512',
},
],
title: 'Resources',
description: 'AUTOMATIC111',
openExternal: true,
title: 'Stable Diffusion Webui',
url: 'https://github.com/AUTOMATIC1111/stable-diffusion-webui',
},
{
items: [
{
icon: <Icon icon={Bug} size="small" />,
openExternal: true,
title: 'Report Bug',
url: `${homepage}/issues/new/choose`,
},
{
icon: <Icon icon={GitFork} size="small" />,
openExternal: true,
title: 'Request Feature',
url: `${homepage}/issues/new/choose`,
},
],
title: 'Community',
description: 'WebUI extension',
openExternal: true,
title: 'Controlnet',
url: 'https://github.com/Mikubill/sd-webui-controlnet',
},
{
items: [
{
icon: <Icon icon={Github} size="small" />,
openExternal: true,
title: 'GitHub',
url: homepage,
},
{
icon: <Icon icon={FileClock} size="small" />,
openExternal: true,
title: 'Changelog',
url: `${homepage}/blob/main/CHANGELOG.md`,
},
],
title: 'Help',
description: 'Art models',
openExternal: true,
title: 'Civitai',
url: 'https://civitai.com/',
},
{
items: [
{
description: 'Minifier ExtraNetwrok Covers',
openExternal: true,
title: '✂️ Cover Minifier',
url: 'https://github.com/canisminor1990/sd-webui-cover-minifier',
},
{
description: 'AIGC Components',
openExternal: true,
title: '🤯 Lobe UI',
url: 'https://github.com/lobehub/lobe-ui',
},
{
description: 'AI Commit CLI',
openExternal: true,
title: '💌 Lobe Commit',
url: 'https://github.com/lobehub/lobe-commit',
},
],
title: 'More Products',
description: 'Artist Inspired Styles',
openExternal: true,
title: 'Cheat Sheet',
url: 'https://supagruen.github.io/StableDiffusion-CheatSheet',
},
{
description: 'Image Resizing',
openExternal: true,
title: 'Birme',
url: 'https://www.birme.net/?target_width=512&target_height=512',
},
];
export const Community = [
{
icon: <Icon icon={Bug} size="small" />,
openExternal: true,
title: 'Report Bug',
url: `${homepage}/issues/new/choose`,
},
{
icon: <Icon icon={GitFork} size="small" />,
openExternal: true,
title: 'Request Feature',
url: `${homepage}/issues/new/choose`,
},
];
export const Help = [
{
icon: <Icon icon={Github} size="small" />,
openExternal: true,
title: 'GitHub',
url: homepage,
},
{
icon: <Icon icon={FileClock} size="small" />,
openExternal: true,
title: 'Changelog',
url: `${homepage}/blob/main/CHANGELOG.md`,
},
];
export const MoreProducts = [
{
description: 'Minifier ExtraNetwrok Covers',
openExternal: true,
title: '✂️ Cover Minifier',
url: 'https://github.com/canisminor1990/sd-webui-cover-minifier',
},
{
description: 'AIGC Components',
openExternal: true,
title: '🤯 Lobe UI',
url: 'https://github.com/lobehub/lobe-ui',
},
{
description: 'AI Commit CLI',
openExternal: true,
title: '💌 Lobe Commit',
url: 'https://github.com/lobehub/lobe-commit',
},
];

View File

@ -1,16 +1,18 @@
import { Footer as F } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useRef } from 'react';
import { useTranslation } from 'react-i18next';
import { useAppStore } from '@/store';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import { columns } from './data';
import { Community, Help, MoreProducts, Resources } from './data';
import { useStyles } from './style';
const Footer = memo<DivProps>(({ className, ...props }) => {
const setting = useAppStore((st) => st.setting, isEqual);
const { cx, styles } = useStyles();
const { t } = useTranslation();
const footerReference = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -24,7 +26,27 @@ const Footer = memo<DivProps>(({ className, ...props }) => {
{setting.layoutHideFooter ? (
<div ref={footerReference} />
) : (
<F bottom={<div ref={footerReference} />} columns={columns} />
<F
bottom={<div ref={footerReference} />}
columns={[
{
items: Resources,
title: t('resources'),
},
{
items: Community,
title: t('community'),
},
{
items: Help,
title: t('help'),
},
{
items: MoreProducts,
title: t('moreProducts'),
},
]}
/>
)}
</div>
);

View File

@ -4,6 +4,7 @@ import { useResponsive } from 'antd-style';
import { Github, LucideIcon, Moon, Settings, Sun } from 'lucide-react';
import qs from 'query-string';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import Giscus from '@/slots/Giscus';
@ -25,6 +26,7 @@ const Actions = memo<ActionsProps>(() => {
const [isModalOpen, setIsModalOpen] = useState(false);
const themeMode = useAppStore((st) => st.themeMode, shallow);
const { mobile } = useResponsive();
const { t } = useTranslation();
const handleSetTheme = useCallback(() => {
const theme = themeMode === 'light' ? 'dark' : 'light';
@ -37,7 +39,7 @@ const Actions = memo<ActionsProps>(() => {
<ActionIcon
icon={themeMode === 'light' ? Sun : Moon}
onClick={handleSetTheme}
title="Switch Theme"
title={t('switchTheme')}
/>
);
@ -49,9 +51,9 @@ const Actions = memo<ActionsProps>(() => {
<a href="https://civitai.com/" rel="noreferrer" target="_blank">
<ActionIcon icon={CivitaiLogo} title="Civitai" />
</a>
<ActionIcon icon={Github} onClick={() => setIsModalOpen(true)} title="Feedback" />
<ActionIcon icon={Github} onClick={() => setIsModalOpen(true)} title={t('feedback')} />
{themeSwitch}
<ActionIcon icon={Settings} onClick={() => setIsSettingOpen(true)} title="Setting" />
<ActionIcon icon={Settings} onClick={() => setIsSettingOpen(true)} title={t('setting')} />
</Space.Compact>
<Setting onCancel={() => setIsSettingOpen(false)} open={isSettingOpen} />
<Giscus onCancel={() => setIsModalOpen(false)} open={isModalOpen} themeMode={themeMode} />

View File

@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
import { homepage, name, version } from '@/../package.json';
import { Logo } from '@/components';
import { useAppStore } from '@/store';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import Actions from './Actions';
import Nav from './Nav';

View File

@ -4,7 +4,7 @@ import { memo, useEffect, useRef } from 'react';
import { shallow } from 'zustand/shallow';
import { useAppStore } from '@/store';
import { DivProps } from '@/types/index';
import { type DivProps } from '@/types';
import { useStyles } from './style';

View File

@ -1,4 +1,4 @@
import { DivProps } from '@lobehub/ui';
import { type DivProps } from '@lobehub/ui';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useRef } from 'react';

View File

@ -1,7 +1,8 @@
import { DivProps, DraggablePanel, LayoutSidebarInner } from '@lobehub/ui';
import { type DivProps, DraggablePanel, LayoutSidebarInner } from '@lobehub/ui';
import { useResponsive } from 'antd-style';
import isEqual from 'fast-deep-equal';
import { memo, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { SidebarContainer, SidebarHeader } from '@/components';
import { useAppStore } from '@/store';
@ -19,6 +20,7 @@ const QuickSettingSidebar = memo<QuickSettingSidebarProps>(({ headerHeight }) =>
const [expand, setExpand] = useState<boolean>(mobile ? false : setting.sidebarExpand);
const [pin, setPin] = useState<boolean>(setting.sidebarFixedMode === 'fixed');
const { styles, theme } = useStyles({ headerHeight });
const { t } = useTranslation();
useEffect(() => {
if (mobile) setExpand(false);
@ -50,7 +52,7 @@ const QuickSettingSidebar = memo<QuickSettingSidebarProps>(({ headerHeight }) =>
position="left"
setExpand={setExpand}
setPin={setPin}
title="Quick Settings"
title={t('quickSetting')}
/>
<Inner />
</SidebarContainer>

View File

@ -37,14 +37,16 @@ const Index = memo(() => {
</LayoutHeader>
<LayoutMain>
{!setting.liteAnimation && <div className={styles.background} />}
<LayoutSidebar
className={styles.sidebar}
headerHeight={HEADER_HEIGHT}
style={{ flex: 0, zIndex: 50 }}
>
<QuickSettingSidebar headerHeight={HEADER_HEIGHT} />
</LayoutSidebar>
<Content />
{setting.enableSidebar && (
<LayoutSidebar
className={styles.sidebar}
headerHeight={HEADER_HEIGHT}
style={{ flex: 0, zIndex: 50 }}
>
<QuickSettingSidebar headerHeight={HEADER_HEIGHT} />
</LayoutSidebar>
)}
<Content className={cx(!setting.enableSidebar && styles.quicksettings)} />
{setting.layoutSplitPreview && mobile === false && (
<LayoutSidebar
className={cx(styles.sidebar, styles.panel)}

View File

@ -39,6 +39,11 @@ export const useStyles = createStyles(
border-style: dashed;
}
`,
quicksettings: css`
#quicksettings {
padding: 16px !important;
}
`,
sidebar: css`
height: calc(100vh - ${headerHeight}px);
`,

View File

@ -2,6 +2,7 @@ import GiscusComponent from '@giscus/react';
import { ActionIcon } from '@lobehub/ui';
import { Github } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { homepage, name } from '@/../package.json';
import { Modal, type ModalProps } from '@/components';
@ -12,6 +13,7 @@ interface GiscusProps {
themeMode: 'light' | 'dark';
}
const Giscus = memo<GiscusProps>(({ themeMode, open, onCancel }) => {
const { t } = useTranslation();
return (
<Modal
onCancel={onCancel}
@ -21,7 +23,7 @@ const Giscus = memo<GiscusProps>(({ themeMode, open, onCancel }) => {
<a href={homepage} rel="noreferrer" target="_blank">
<ActionIcon icon={Github} title={`canisminor1990/${name}`} />
</a>
Theme Feedback
{t('themeFeedback')}
</>
}
>

View File

@ -1,4 +1,5 @@
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import TagList, { PromptType, TagItem } from './TagList';
import { useStyles } from './style';
@ -11,6 +12,7 @@ interface PromptProps {
const Prompt = memo<PromptProps>(({ type }) => {
const [tags, setTags] = useState<TagItem[]>([]);
const { styles } = useStyles();
const { t } = useTranslation();
const id =
type === 'positive' ? "[id$='2img_prompt'] textarea" : "[id$='2img_neg_prompt'] textarea";
@ -51,7 +53,7 @@ const Prompt = memo<PromptProps>(({ type }) => {
<button
className="lg secondary gradio-button tool svelte-1ipelgc"
onClick={getValue}
title="Load Prompt"
title={t('loadPrompt')}
type="button"
>
🔄
@ -59,7 +61,7 @@ const Prompt = memo<PromptProps>(({ type }) => {
<button
className="lg secondary gradio-button tool svelte-1ipelgc"
onClick={setValue}
title="Set Prompt"
title={t('setPrompt')}
type="button"
>

View File

@ -1,4 +1,5 @@
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { useStyles } from '@/slots/PromptEditor/style';
@ -6,11 +7,12 @@ import Prompt from './Prompt';
const PromptEditor = memo(() => {
const { styles } = useStyles();
const { t } = useTranslation();
return (
<div className={styles.view}>
<span style={{ marginBottom: -10 }}>Positive</span>
<span style={{ marginBottom: -10 }}>{t('positive')}</span>
<Prompt type="positive" />
<span style={{ marginBottom: -10 }}>Negative</span>
<span style={{ marginBottom: -10 }}>{t('negative')}</span>
<Prompt type="negative" />
</div>
);

View File

@ -0,0 +1,3 @@
import { Divider } from 'antd';
export default () => <Divider style={{ margin: 0 }} />;

View File

@ -0,0 +1,37 @@
import { Icon, type IconProps } from '@lobehub/ui';
import { Collapse, type CollapseProps } from 'antd';
import { type ReactNode, memo } from 'react';
import { useStyles } from './style';
export interface FormGroupProps extends CollapseProps {
children: ReactNode;
icon: IconProps['icon'];
title: string;
}
const FormGroup = memo<FormGroupProps>(({ icon, title, children, ...props }) => {
const { styles } = useStyles();
return (
<Collapse
className={styles.group}
defaultActiveKey={[1]}
items={[
{
children,
key: 1,
label: (
<div className={styles.title}>
<Icon icon={icon} />
{title}
</div>
),
},
]}
key={1}
{...props}
/>
);
});
export default FormGroup;

View File

@ -0,0 +1,32 @@
import { FormItemProps as AntdFormItemProps, Form } from 'antd';
import { memo } from 'react';
import FormDivider from '@/slots/Setting/FormDivider';
import FormTitle from '@/slots/Setting/FormTitle';
import { useStyles } from './style';
const { Item } = Form;
export interface FormItemProps extends AntdFormItemProps {
desc?: string;
divider?: boolean;
}
const FormItem = memo<FormItemProps>(({ desc, label, children, divider, ...props }) => {
const { styles } = useStyles();
return (
<>
{divider && <FormDivider />}
<Item
className={styles.item}
label={desc ? <FormTitle desc={desc} title={String(label)} /> : label}
{...props}
>
{children}
</Item>
</>
);
});
export default FormItem;

View File

@ -1,32 +1,23 @@
import { Icon, Swatches } from '@lobehub/ui';
import { Button, Divider, Form, Input, InputNumber, Segmented, Switch } from 'antd';
import { Swatches } from '@lobehub/ui';
import { Button, Form, Input, InputNumber, Segmented, Select, Switch } from 'antd';
import isEqual from 'fast-deep-equal';
import { Layout, Palette, PanelLeftClose, PanelRightClose, TextCursorInput } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { memo, useCallback, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { shallow } from 'zustand/shallow';
import { CustomLogo } from '@/components';
import { i18nOptions } from '@/i18n';
import { NeutralColor, PrimaryColor, WebuiSetting, defaultSetting, useAppStore } from '@/store';
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
import { neutralColorScales } from '@/styles/neutralColors';
import FormTitle from './FormTitle';
import FormGroup from './FormGroup';
import FormItem from './FormItem';
import { colors, findKey, neutralColors, primaryColors } from './data';
import { useStyles } from './style';
const { Item } = Form;
const findKey = (object: { [key in string]: string }, value: string): any => {
const res: { [key: string]: PrimaryColor } = {};
for (const key of Object.keys(object)) {
// @ts-ignore
res[object[key]] = key;
}
return res[value];
};
const SettingForm = memo(() => {
const setting = useAppStore((st) => st.setting, isEqual);
const { onSetSetting, themeMode } = useAppStore(
const { onSetSetting } = useAppStore(
(st) => ({ onSetSetting: st.onSetSetting, themeMode: st.themeMode }),
shallow,
);
@ -37,39 +28,8 @@ const SettingForm = memo(() => {
const [neutralColor, setNeutralColor] = useState<NeutralColor | undefined>(
setting.neutralColor || undefined,
);
const { styles, theme } = useStyles();
const colors = useMemo(
() => ({
blue: theme.blue,
cyan: theme.cyan,
geekblue: theme.geekblue,
gold: theme.gold,
green: theme.green,
kitchen: kitchenPrimary.light.colorPrimary,
lime: theme.lime,
magenta: theme.magenta,
orange: theme.orange,
purple: theme.purple,
red: theme.red,
volcano: theme.volcano,
yellow: theme.yellow,
}),
[theme],
);
const neutralColors = useMemo(
() => ({
kitchen: kitchenNeutral[themeMode].colorNeutral,
mauve: neutralColorScales.mauve[themeMode][9],
olive: neutralColorScales.olive[themeMode][9],
sage: neutralColorScales.sage[themeMode][9],
sand: neutralColorScales.sand[themeMode][9],
slate: neutralColorScales.slate[themeMode][9],
}),
[themeMode],
);
const { styles } = useStyles();
const { t } = useTranslation();
const onReset = useCallback(() => {
onSetSetting(defaultSetting);
@ -93,186 +53,252 @@ const SettingForm = memo(() => {
onFinish={onFinish}
onValuesChange={(_, v) => setRawSetting(v)}
>
<div className={styles.group}>
<div className={styles.title}>
<Icon icon={TextCursorInput} />
Promot Textarea
</div>
<Item
className={styles.item}
label={<FormTitle desc="Fixed height / Auto height" title="Display mode" />}
name="promotTextarea"
>
<Segmented options={['scroll', 'resizable']} />
</Item>
<Divider style={{ margin: 0 }} />
<Item
className={styles.item}
label={<FormTitle desc="Top in left sidebar" title="Prompt editor" />}
name="promptEditor"
valuePropName="checked"
>
<Switch />
</Item>
</div>
<div className={styles.group}>
<div className={styles.title}>
<Icon icon={Layout} />
Layout
</div>
<Item
className={styles.item}
label="Split Previewer"
name="layoutSplitPreview"
valuePropName="checked"
>
<Switch />
</Item>
<Divider style={{ margin: 0 }} />
<Item
className={styles.item}
label="Hide Footer"
name="layoutHideFooter"
valuePropName="checked"
>
<Switch />
</Item>
</div>
<div className={styles.group}>
<div className={styles.title}>
<Icon icon={PanelLeftClose} />
QuickSetting Sidebar
</div>
<Item
className={styles.item}
label="Default expand"
name="sidebarExpand"
valuePropName="checked"
>
<Switch />
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Display mode" name="sidebarFixedMode">
<Segmented options={['fixed', 'float']} />
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Default width" name="sidebarWidth">
<InputNumber />
</Item>
</div>
<div className={styles.group}>
<div className={styles.title}>
<Icon icon={PanelRightClose} />
ExtraNetwork Sidebar
</div>
<Item
className={styles.item}
label="Enable"
name="enableExtraNetworkSidebar"
valuePropName="checked"
>
<Switch />
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Display mode" name="extraNetworkFixedMode">
<Segmented options={['fixed', 'float']} />
</Item>
<Divider style={{ margin: 0 }} />
<Item
className={styles.item}
label="Default expand"
name="extraNetworkSidebarExpand"
valuePropName="checked"
>
<Switch />
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Default width" name="extraNetworkSidebarWidth">
<InputNumber />
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Default card size" name="extraNetworkCardSize">
<InputNumber />
</Item>
</div>
<div className={styles.group}>
<div className={styles.title}>
<Icon icon={Palette} />
Theme
</div>
<Item
className={styles.item}
label={<FormTitle desc="Save cpu usage" title="Remove animation" />}
<FormGroup icon={Palette} title={t('settingGroupTheme')}>
<FormItem desc={t('settingLanguageDesc')} label={t('settingLanguage')} name="i18n">
<Select options={i18nOptions} />
</FormItem>
<FormItem
desc={t('settingReduceAnimationDesc')}
divider
label={t('settingReduceAnimation')}
name="liteAnimation"
valuePropName="checked"
>
<Switch />
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Primary color">
</FormItem>
<FormItem desc={t('settingPrimaryColorDesc')} divider label={t('settingPrimaryColor')}>
<Swatches
activeColor={primaryColor ? colors[primaryColor] : undefined}
colors={[
kitchenPrimary.light.colorPrimary,
theme.red,
theme.orange,
theme.gold,
theme.yellow,
theme.lime,
theme.green,
theme.cyan,
theme.blue,
theme.geekblue,
theme.purple,
theme.magenta,
theme.volcano,
]}
colors={primaryColors}
onSelect={(c) => setPrimaryColor(c ? findKey(colors, c) : undefined)}
/>
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Neutral Color">
</FormItem>
<FormItem desc={t('settingNeutralColorDesc')} divider label={t('settingNeutralColor')}>
<Swatches
activeColor={neutralColor ? neutralColors[neutralColor] : undefined}
colors={Object.values(neutralColors)}
onSelect={(c) => setNeutralColor(c ? findKey(neutralColors, c) : undefined)}
/>
</Item>
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Logo type" name="logoType">
<Segmented options={['lobe', 'kitchen', 'custom']} />
</Item>
</FormItem>
<FormItem
desc={t('settingLogoTypeDesc')}
divider
label={t('settingLogoType')}
name="logoType"
>
<Segmented
options={[
{
label: t('lobe'),
value: 'lobe',
},
{
label: t('kitchen'),
value: 'kitchen',
},
{
label: t('custom'),
value: 'custom',
},
]}
/>
</FormItem>
{rawSetting.logoType === 'custom' && (
<>
<Item
className={styles.item}
label="Logo ( Url / Base64 / Emoji )"
<FormItem
desc={t('settingCustomLogoDesc')}
label={t('settingCustomLogo')}
name="logoCustomUrl"
>
<Input />
</Item>
<Item className={styles.item} label="Title" name="logoCustomTitle">
</FormItem>
<FormItem
desc={t('settingCustomTitleDesc')}
label={t('settingCustomTitle')}
name="logoCustomTitle"
>
<Input />
</Item>
<Item className={styles.item} label="Preview">
</FormItem>
<FormItem label={t('settingLogoPreview')}>
<CustomLogo
logoCustomTitle={rawSetting.logoCustomTitle}
logoCustomUrl={rawSetting.logoCustomUrl}
/>
</Item>
</FormItem>
</>
)}
<Divider style={{ margin: 0 }} />
<Item className={styles.item} label="Use svg icons" name="svgIcon" valuePropName="checked">
<FormItem
desc={t('settingSvgIconsDesc')}
divider
label={t('settingSvgIcons')}
name="svgIcon"
valuePropName="checked"
>
<Switch />
</Item>
</div>
</FormItem>
</FormGroup>
<FormGroup icon={TextCursorInput} title={t('settingGroupPromotTextarea')}>
<FormItem
desc={t('settingPromptDisplayModeDesc')}
label={t('settingPromptDisplayMode')}
name="promotTextarea"
>
<Segmented
options={[
{
label: t('scroll'),
value: 'scroll',
},
{
label: t('resizable'),
value: 'resizable',
},
]}
/>
</FormItem>
<FormItem
desc={t('settingPromptEditorDesc')}
divider
label={t('settingPromptEditor')}
name="promptEditor"
valuePropName="checked"
>
<Switch />
</FormItem>
</FormGroup>
<FormGroup icon={Layout} title={t('settingGroupLayout')}>
<FormItem
desc={t('settingSplitPreviewerDesc')}
label={t('settingSplitPreviewer')}
name="layoutSplitPreview"
valuePropName="checked"
>
<Switch />
</FormItem>
<FormItem
desc={t('settingHideFooterDesc')}
divider
label={t('settingHideFooter')}
name="layoutHideFooter"
valuePropName="checked"
>
<Switch />
</FormItem>
</FormGroup>
<FormGroup icon={PanelLeftClose} title={t('settingGroupQuickSettingSidebar')}>
<FormItem
desc={t('settingQuickSettingSidebarEnableDesc')}
label={t('settingQuickSettingSidebarEnable')}
name="enableSidebar"
valuePropName="checked"
>
<Switch />
</FormItem>
{rawSetting.enableSidebar && (
<>
<FormItem
desc={t('settingQuickSettingSidebarDefaultExpandDesc')}
divider
label={t('settingQuickSettingSidebarDefaultExpand')}
name="sidebarExpand"
valuePropName="checked"
>
<Switch />
</FormItem>
<FormItem
desc={t('settingQuickSettingSidebarDisplayModeDesc')}
divider
label={t('settingQuickSettingSidebarDisplayMode')}
name="sidebarFixedMode"
>
<Segmented
options={[
{
label: t('fixed'),
value: 'fixed',
},
{
label: t('float'),
value: 'float',
},
]}
/>
</FormItem>
<FormItem
desc={t('settingQuickSettingSidebarDefaultWidthDesc')}
divider
label={t('settingQuickSettingSidebarDefaultWidth')}
name="sidebarWidth"
>
<InputNumber />
</FormItem>
</>
)}
</FormGroup>
<FormGroup icon={PanelRightClose} title={t('settingGroupExtraNetworkSidebar')}>
<FormItem
desc={t('settingExtraNetworkSidebarEnableDesc')}
label={t('settingExtraNetworkSidebarEnable')}
name="enableExtraNetworkSidebar"
valuePropName="checked"
>
<Switch />
</FormItem>
{rawSetting.enableExtraNetworkSidebar && (
<>
<FormItem
desc={t('settingExtraNetworkSidebarDisplayModeDesc')}
divider
label={t('settingExtraNetworkSidebarDisplayMode')}
name="extraNetworkFixedMode"
>
<Segmented
options={[
{
label: t('fixed'),
value: 'fixed',
},
{
label: t('float'),
value: 'float',
},
]}
/>
</FormItem>
<FormItem
desc={t('settingExtraNetworkSidebarDefaultExpandDesc')}
divider
label={t('settingExtraNetworkSidebarDefaultExpand')}
name="extraNetworkSidebarExpand"
valuePropName="checked"
>
<Switch />
</FormItem>
<FormItem
desc={t('settingExtraNetworkSidebarDefaultWidthDesc')}
divider
label={t('settingExtraNetworkSidebarDefaultWidth')}
name="extraNetworkSidebarWidth"
>
<InputNumber />
</FormItem>
<FormItem
desc={t('settingExtraNetworkSidebarDefaultCardSizeDesc')}
divider
label={t('settingExtraNetworkSidebarDefaultCardSize')}
name="extraNetworkCardSize"
>
<InputNumber />
</FormItem>
</>
)}
</FormGroup>
<div className={styles.footer}>
<Button htmlType="button" onClick={onReset} style={{ borderRadius: 4 }}>
Reset
{t('settingButtomReset')}
</Button>
<Button htmlType="submit" style={{ borderRadius: 4 }} type="primary">
Apply and restart UI
{t('settingButtomSubmit')}
</Button>
</div>
</Form>

55
src/slots/Setting/data.ts Normal file
View File

@ -0,0 +1,55 @@
import { colors as lobeColor } from '@lobehub/ui';
import { PrimaryColor } from '@/store';
import { kitchenNeutral, kitchenPrimary } from '@/styles/kitchenColors';
import { neutralColorScales } from '@/styles/neutralColors';
export const colors = {
blue: lobeColor.blue.dark[9],
cyan: lobeColor.cyan.dark[9],
geekblue: lobeColor.geekblue.dark[9],
gold: lobeColor.gold.dark[9],
green: lobeColor.green.dark[9],
kitchen: kitchenPrimary.dark.colorPrimary,
lime: lobeColor.lime.dark[9],
magenta: lobeColor.magenta.dark[9],
orange: lobeColor.orange.dark[9],
purple: lobeColor.purple.dark[9],
red: lobeColor.red.dark[9],
volcano: lobeColor.volcano.dark[9],
yellow: lobeColor.yellow.dark[9],
};
export const primaryColors = [
kitchenPrimary.dark.colorPrimary,
lobeColor.red.dark[9],
lobeColor.orange.dark[9],
lobeColor.gold.dark[9],
lobeColor.yellow.dark[9],
lobeColor.lime.dark[9],
lobeColor.green.dark[9],
lobeColor.cyan.dark[9],
lobeColor.blue.dark[9],
lobeColor.geekblue.dark[9],
lobeColor.purple.dark[9],
lobeColor.magenta.dark[9],
lobeColor.volcano.dark[9],
];
export const neutralColors = {
kitchen: kitchenNeutral.dark.colorNeutral,
mauve: neutralColorScales.mauve.dark[9],
olive: neutralColorScales.olive.dark[9],
sage: neutralColorScales.sage.dark[9],
sand: neutralColorScales.sand.dark[9],
slate: neutralColorScales.slate.dark[9],
};
export const findKey = (object: { [key in string]: string }, value: string): any => {
const res: { [key: string]: PrimaryColor } = {};
for (const key of Object.keys(object)) {
// @ts-ignore
res[object[key]] = key;
}
return res[value];
};

View File

@ -2,6 +2,7 @@ import { ActionIcon } from '@lobehub/ui';
import { Space, Tag } from 'antd';
import { Book } from 'lucide-react';
import { memo } from 'react';
import { useTranslation } from 'react-i18next';
import { homepage, version } from '@/../package.json';
import { Modal, type ModalProps } from '@/components';
@ -14,9 +15,9 @@ interface SettingProps {
}
const Setting = memo<SettingProps>(({ open, onCancel }) => {
const { t } = useTranslation();
return (
<Modal
okText={'Save and Refresh'}
onCancel={onCancel}
open={open}
title={
@ -25,7 +26,7 @@ const Setting = memo<SettingProps>(({ open, onCancel }) => {
<ActionIcon icon={Book} title="Setting Documents" />
</a>
<Space>
Theme Setting
{t('themeSetting')}
<Tag color="success">v{version}</Tag>
</Space>
</>

View File

@ -29,12 +29,23 @@ export const useStyles = createStyles(({ css, token }) => ({
`,
group: css`
overflow: hidden;
background: ${token.colorFillQuaternary};
border: 1px solid ${token.colorBorder};
border-radius: ${token.borderRadius}px;
.ant-collapse-header {
background: ${token.colorFillTertiary};
border-radius: unset;
}
.ant-collapse-content {
background: transparent;
}
.ant-collapse-content-box {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
`,
item: css`
padding: 8px 16px;
padding: 8px 0;
.ant-row {
justify-content: space-between;
@ -50,15 +61,8 @@ export const useStyles = createStyles(({ css, token }) => ({
gap: 8px;
align-items: center;
margin: 0;
margin-bottom: 4px;
padding: 16px;
font-size: 16px;
font-weight: 600;
line-height: 1;
background: ${token.colorFillTertiary};
.anticon {
color: ${token.colorPrimary};

View File

@ -3,6 +3,8 @@ import { devtools } from 'zustand/middleware';
const SETTING_KEY = 'SD-KITCHEN-SETTING';
export type I18n = 'en' | 'zh-CN';
export type PrimaryColor =
| 'blue'
| 'cyan'
@ -22,10 +24,12 @@ export type NeutralColor = 'mauve' | 'slate' | 'sage' | 'olive' | 'sand' | 'kitc
export interface WebuiSetting {
enableExtraNetworkSidebar: boolean;
enableSidebar: boolean;
extraNetworkCardSize: number;
extraNetworkFixedMode: 'fixed' | 'float';
extraNetworkSidebarExpand: boolean;
extraNetworkSidebarWidth: number;
i18n: I18n;
layoutHideFooter: boolean;
layoutSplitPreview: boolean;
liteAnimation: boolean;
@ -44,10 +48,12 @@ export interface WebuiSetting {
export const defaultSetting: WebuiSetting = {
enableExtraNetworkSidebar: true,
enableSidebar: true,
extraNetworkCardSize: 86,
extraNetworkFixedMode: 'fixed',
extraNetworkSidebarExpand: true,
extraNetworkSidebarWidth: 340,
i18n: 'en',
layoutHideFooter: false,
layoutSplitPreview: false,
liteAnimation: false,

7
src/types/i18next.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
import type { resources } from './index';
declare module 'i18next' {
interface CustomTypeOptions {
resources: typeof resources;
}
}

View File

@ -1,5 +1,17 @@
import { type HTMLAttributes } from 'react';
import translation from '@/i18n/lang/en';
export const resources = {
translation,
} as const;
type TranslationKeys = keyof typeof translation;
export type Translation = {
[key in TranslationKeys]: string;
};
export type DivProps = HTMLAttributes<HTMLDivElement>;
export type SvgProps = HTMLAttributes<SVGSVGElement>;