diff --git a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx index 3f0358fa..5ff93e73 100644 --- a/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx @@ -1,364 +1,38 @@ -import { - DeleteOutlined, - FileSearchOutlined, - FolderOpenOutlined, - InfoCircleOutlined, - SaveOutlined -} from '@ant-design/icons' -import { Client } from '@notionhq/client' +import { FileSearchOutlined, FolderOpenOutlined, SaveOutlined } from '@ant-design/icons' import { HStack } from '@renderer/components/Layout' -import MinApp from '@renderer/components/MinApp' +import ListItem from '@renderer/components/ListItem' import BackupPopup from '@renderer/components/Popups/BackupPopup' import RestorePopup from '@renderer/components/Popups/RestorePopup' import { useTheme } from '@renderer/context/ThemeProvider' import { useKnowledgeFiles } from '@renderer/hooks/useKnowledgeFiles' import { reset } from '@renderer/services/BackupService' -import { RootState, useAppDispatch } from '@renderer/store' -import { - setmarkdownExportPath, - setNotionApiKey, - setNotionAutoSplit, - setNotionDatabaseID, - setNotionPageNameKey, - setNotionSplitSize, - setYuqueRepoId, - setYuqueToken, - setYuqueUrl -} from '@renderer/store/settings' import { AppInfo } from '@renderer/types' import { formatFileSize } from '@renderer/utils' -import { Button, InputNumber, Modal, Switch, Tooltip, Typography } from 'antd' -import Input from 'antd/es/input/Input' +import { Button, Modal, Typography } from 'antd' import { FC, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useSelector } from 'react-redux' import styled from 'styled-components' -import { - SettingContainer, - SettingDivider, - SettingGroup, - SettingHelpText, - SettingRow, - SettingRowTitle, - SettingTitle -} from '..' +import { SettingContainer, SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' +import MarkdownExportSettings from './MarkdownExportSettings' +import NotionSettings from './NotionSettings' import WebDavSettings from './WebDavSettings' - -// 新增的 MarkdownExportSettings 组件 -const MarkdownExportSettings: FC = () => { - const { t } = useTranslation() - const { theme } = useTheme() - const dispatch = useAppDispatch() - - const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath) - - const handleSelectFolder = async () => { - const path = await window.api.file.selectFolder() - if (path) { - dispatch(setmarkdownExportPath(path)) - } - } - - const handleClearPath = () => { - dispatch(setmarkdownExportPath(null)) - } - - return ( - - {t('settings.data.markdown_export.title')} - - - {t('settings.data.markdown_export.path')} - - - ) : null - } - /> - - - - - {t('settings.data.markdown_export.help')} - - - ) -} - -// 新增的 NotionSettings 组件 -const NotionSettings: FC = () => { - const { t } = useTranslation() - const { theme } = useTheme() - const dispatch = useAppDispatch() - - const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey) - const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID) - const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey) - const notionAutoSplit = useSelector((state: RootState) => state.settings.notionAutoSplit) - const notionSplitSize = useSelector((state: RootState) => state.settings.notionSplitSize) - - const handleNotionTokenChange = (e: React.ChangeEvent) => { - dispatch(setNotionApiKey(e.target.value)) - } - - const handleNotionDatabaseIdChange = (e: React.ChangeEvent) => { - dispatch(setNotionDatabaseID(e.target.value)) - } - - const handleNotionPageNameKeyChange = (e: React.ChangeEvent) => { - dispatch(setNotionPageNameKey(e.target.value)) - } - - const handleNotionConnectionCheck = () => { - if (notionApiKey === null) { - window.message.error(t('settings.data.notion.check.empty_api_key')) - return - } - if (notionDatabaseID === null) { - window.message.error(t('settings.data.notion.check.empty_database_id')) - return - } - const notion = new Client({ auth: notionApiKey }) - notion.databases - .retrieve({ - database_id: notionDatabaseID - }) - .then((result) => { - if (result) { - window.message.success(t('settings.data.notion.check.success')) - } else { - window.message.error(t('settings.data.notion.check.fail')) - } - }) - .catch(() => { - window.message.error(t('settings.data.notion.check.error')) - }) - } - - const handleNotionTitleClick = () => { - MinApp.start({ - id: 'notion-help', - name: 'Notion Help', - url: 'https://docs.cherry-ai.com/advanced-basic/notion' - }) - } - - const handleNotionAutoSplitChange = (checked: boolean) => { - dispatch(setNotionAutoSplit(checked)) - } - - const handleNotionSplitSizeChange = (value: number | null) => { - if (value !== null) { - dispatch(setNotionSplitSize(value)) - } - } - - return ( - - - {t('settings.data.notion.title')} - - - - - - - {t('settings.data.notion.database_id')} - - - - - - - {t('settings.data.notion.page_name_key')} - - - - - - - {t('settings.data.notion.api_key')} - - - - - - {/* 添加分割线 */} - - - - - {t('settings.data.notion.auto_split')} - - - - - - - {notionAutoSplit && ( - <> - - - {t('settings.data.notion.split_size')} - - - - {t('settings.data.notion.split_size_help')} - - - )} - - ) -} - -const YuqueSettings: FC = () => { - const { t } = useTranslation() - const { theme } = useTheme() - const dispatch = useAppDispatch() - - const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken) - const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl) - - const handleYuqueTokenChange = (e: React.ChangeEvent) => { - dispatch(setYuqueToken(e.target.value)) - } - - const handleYuqueRepoUrlChange = (e: React.ChangeEvent) => { - dispatch(setYuqueUrl(e.target.value)) - } - - const handleYuqueConnectionCheck = async () => { - if (!yuqueToken) { - window.message.error(t('settings.data.yuque.check.empty_token')) - return - } - if (!yuqueUrl) { - window.message.error(t('settings.data.yuque.check.empty_url')) - return - } - - const response = await fetch('https://www.yuque.com/api/v2/hello', { - headers: { - 'X-Auth-Token': yuqueToken - } - }) - - if (!response.ok) { - window.message.error(t('settings.data.yuque.check.fail')) - return - } - const yuqueSlug = yuqueUrl.replace('https://www.yuque.com/', '') - const repoIDResponse = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueSlug}`, { - headers: { - 'X-Auth-Token': yuqueToken - } - }) - if (!repoIDResponse.ok) { - window.message.error(t('settings.data.yuque.check.fail')) - return - } - const data = await repoIDResponse.json() - dispatch(setYuqueRepoId(data.data.id)) - window.message.success(t('settings.data.yuque.check.success')) - } - - const handleYuqueHelpClick = () => { - MinApp.start({ - id: 'yuque-help', - name: 'Yuque Help', - url: 'https://www.yuque.com/settings/tokens' - }) - } - - return ( - - {t('settings.data.yuque.title')} - - - {t('settings.data.yuque.repo_url')} - - - - - - - - {t('settings.data.yuque.token')} - - - - - - - - - - - ) -} +import YuqueSettings from './YuqueSettings' const DataSettings: FC = () => { const { t } = useTranslation() const [appInfo, setAppInfo] = useState() const { size, removeAllFiles } = useKnowledgeFiles() const { theme } = useTheme() + const [menu, setMenu] = useState('data') + + const menuItems = [ + { key: 'data', title: 'settings.data.data.title' }, + { key: 'webdav', title: 'settings.data.webdav.title' }, + { key: 'markdown_export', title: 'settings.data.markdown_export.title' }, + { key: 'notion', title: 'settings.data.notion.title' }, + { key: 'yuque', title: 'settings.data.yuque.title' } + ] useEffect(() => { window.api.getAppInfo().then(setAppInfo) @@ -411,78 +85,91 @@ const DataSettings: FC = () => { } return ( - - - {t('settings.data.title')} - - - {t('settings.general.backup.title')} - - - - - - - - {t('settings.general.reset.title')} - - - - - - - - - - - - - {t('settings.data.data.title')} - - - {t('settings.data.app_data')} - - {appInfo?.appDataPath} - handleOpenPath(appInfo?.appDataPath)} /> - - - - - {t('settings.data.app_logs')} - - {appInfo?.logsPath} - handleOpenPath(appInfo?.logsPath)} /> - - - - - {t('settings.data.app_knowledge')} - - - - - - - {t('settings.data.clear_cache.title')} - - - - - - + + + {menuItems.map((item) => ( + setMenu(item.key)} /> + ))} + + + {menu === 'data' && ( + <> + + {t('settings.data.title')} + + + {t('settings.general.backup.title')} + + + + + + + + {t('settings.general.reset.title')} + + + + + + + {t('settings.data.data.title')} + + + {t('settings.data.app_data')} + + {appInfo?.appDataPath} + handleOpenPath(appInfo?.appDataPath)} /> + + + + + {t('settings.data.app_logs')} + + {appInfo?.logsPath} + handleOpenPath(appInfo?.logsPath)} /> + + + + + {t('settings.data.app_knowledge')} + + + + + + + {t('settings.data.clear_cache.title')} + + + + + + + )} + {menu === 'webdav' && } + {menu === 'markdown_export' && } + {menu === 'notion' && } + {menu === 'yuque' && } + + ) } +const Container = styled(HStack)` + flex: 1; +` + const StyledIcon = styled(FileSearchOutlined)` color: var(--color-text-2); cursor: pointer; @@ -493,4 +180,14 @@ const StyledIcon = styled(FileSearchOutlined)` } ` +const MenuList = styled.div` + display: flex; + flex-direction: column; + gap: 10px; + width: var(--settings-width); + padding: 12px; + border-right: 0.5px solid var(--color-border); + height: 100%; +` + export default DataSettings diff --git a/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx b/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx new file mode 100644 index 00000000..f2e1cec4 --- /dev/null +++ b/src/renderer/src/pages/settings/DataSettings/MarkdownExportSettings.tsx @@ -0,0 +1,63 @@ +import { DeleteOutlined, FolderOpenOutlined } from '@ant-design/icons' +import { HStack } from '@renderer/components/Layout' +import { useTheme } from '@renderer/context/ThemeProvider' +import { RootState, useAppDispatch } from '@renderer/store' +import { setmarkdownExportPath } from '@renderer/store/settings' +import { Button } from 'antd' +import Input from 'antd/es/input/Input' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + +import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const MarkdownExportSettings: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + + const markdownExportPath = useSelector((state: RootState) => state.settings.markdownExportPath) + + const handleSelectFolder = async () => { + const path = await window.api.file.selectFolder() + if (path) { + dispatch(setmarkdownExportPath(path)) + } + } + + const handleClearPath = () => { + dispatch(setmarkdownExportPath(null)) + } + + return ( + + {t('settings.data.markdown_export.title')} + + + {t('settings.data.markdown_export.path')} + + + ) : null + } + /> + + + + + {t('settings.data.markdown_export.help')} + + + ) +} + +export default MarkdownExportSettings diff --git a/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx new file mode 100644 index 00000000..bb15ce83 --- /dev/null +++ b/src/renderer/src/pages/settings/DataSettings/NotionSettings.tsx @@ -0,0 +1,178 @@ +import { InfoCircleOutlined } from '@ant-design/icons' +import { Client } from '@notionhq/client' +import { HStack } from '@renderer/components/Layout' +import MinApp from '@renderer/components/MinApp' +import { useTheme } from '@renderer/context/ThemeProvider' +import { RootState, useAppDispatch } from '@renderer/store' +import { + setNotionApiKey, + setNotionAutoSplit, + setNotionDatabaseID, + setNotionPageNameKey, + setNotionSplitSize +} from '@renderer/store/settings' +import { Button, InputNumber, Switch, Tooltip } from 'antd' +import Input from 'antd/es/input/Input' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + +import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..' +const NotionSettings: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + + const notionApiKey = useSelector((state: RootState) => state.settings.notionApiKey) + const notionDatabaseID = useSelector((state: RootState) => state.settings.notionDatabaseID) + const notionPageNameKey = useSelector((state: RootState) => state.settings.notionPageNameKey) + const notionAutoSplit = useSelector((state: RootState) => state.settings.notionAutoSplit) + const notionSplitSize = useSelector((state: RootState) => state.settings.notionSplitSize) + + const handleNotionTokenChange = (e: React.ChangeEvent) => { + dispatch(setNotionApiKey(e.target.value)) + } + + const handleNotionDatabaseIdChange = (e: React.ChangeEvent) => { + dispatch(setNotionDatabaseID(e.target.value)) + } + + const handleNotionPageNameKeyChange = (e: React.ChangeEvent) => { + dispatch(setNotionPageNameKey(e.target.value)) + } + + const handleNotionConnectionCheck = () => { + if (notionApiKey === null) { + window.message.error(t('settings.data.notion.check.empty_api_key')) + return + } + if (notionDatabaseID === null) { + window.message.error(t('settings.data.notion.check.empty_database_id')) + return + } + const notion = new Client({ auth: notionApiKey }) + notion.databases + .retrieve({ + database_id: notionDatabaseID + }) + .then((result) => { + if (result) { + window.message.success(t('settings.data.notion.check.success')) + } else { + window.message.error(t('settings.data.notion.check.fail')) + } + }) + .catch(() => { + window.message.error(t('settings.data.notion.check.error')) + }) + } + + const handleNotionTitleClick = () => { + MinApp.start({ + id: 'notion-help', + name: 'Notion Help', + url: 'https://docs.cherry-ai.com/advanced-basic/notion' + }) + } + + const handleNotionAutoSplitChange = (checked: boolean) => { + dispatch(setNotionAutoSplit(checked)) + } + + const handleNotionSplitSizeChange = (value: number | null) => { + if (value !== null) { + dispatch(setNotionSplitSize(value)) + } + } + + return ( + + + {t('settings.data.notion.title')} + + + + + + + {t('settings.data.notion.database_id')} + + + + + + + {t('settings.data.notion.page_name_key')} + + + + + + + {t('settings.data.notion.api_key')} + + + + + + {/* 添加分割线 */} + + + + + {t('settings.data.notion.auto_split')} + + + + + + + {notionAutoSplit && ( + <> + + + {t('settings.data.notion.split_size')} + + + + {t('settings.data.notion.split_size_help')} + + + )} + + ) +} + +export default NotionSettings diff --git a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx index dd9033dc..ba87dbe8 100644 --- a/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx +++ b/src/renderer/src/pages/settings/DataSettings/WebDavSettings.tsx @@ -1,5 +1,6 @@ import { FolderOpenOutlined, SaveOutlined, SyncOutlined } from '@ant-design/icons' import { HStack } from '@renderer/components/Layout' +import { useTheme } from '@renderer/context/ThemeProvider' import { useRuntime } from '@renderer/hooks/useRuntime' import { useSettings } from '@renderer/hooks/useSettings' import { backupToWebdav, restoreFromWebdav, startAutoSync, stopAutoSync } from '@renderer/services/BackupService' @@ -17,7 +18,7 @@ import dayjs from 'dayjs' import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' -import { SettingDivider, SettingRow, SettingRowTitle, SettingTitle } from '..' +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' const WebDavSettings: FC = () => { const { @@ -39,6 +40,7 @@ const WebDavSettings: FC = () => { const [restoring, setRestoring] = useState(false) const dispatch = useAppDispatch() + const { theme } = useTheme() const { t } = useTranslation() @@ -112,7 +114,7 @@ const WebDavSettings: FC = () => { } return ( - <> + {t('settings.data.webdav.title')} @@ -196,7 +198,7 @@ const WebDavSettings: FC = () => { )} - + ) } diff --git a/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx b/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx new file mode 100644 index 00000000..bee9f418 --- /dev/null +++ b/src/renderer/src/pages/settings/DataSettings/YuqueSettings.tsx @@ -0,0 +1,116 @@ +import { InfoCircleOutlined } from '@ant-design/icons' +import { HStack } from '@renderer/components/Layout' +import MinApp from '@renderer/components/MinApp' +import { useTheme } from '@renderer/context/ThemeProvider' +import { RootState, useAppDispatch } from '@renderer/store' +import { setYuqueRepoId, setYuqueToken, setYuqueUrl } from '@renderer/store/settings' +import { Button, Tooltip } from 'antd' +import Input from 'antd/es/input/Input' +import { FC } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' + +import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' + +const YuqueSettings: FC = () => { + const { t } = useTranslation() + const { theme } = useTheme() + const dispatch = useAppDispatch() + + const yuqueToken = useSelector((state: RootState) => state.settings.yuqueToken) + const yuqueUrl = useSelector((state: RootState) => state.settings.yuqueUrl) + + const handleYuqueTokenChange = (e: React.ChangeEvent) => { + dispatch(setYuqueToken(e.target.value)) + } + + const handleYuqueRepoUrlChange = (e: React.ChangeEvent) => { + dispatch(setYuqueUrl(e.target.value)) + } + + const handleYuqueConnectionCheck = async () => { + if (!yuqueToken) { + window.message.error(t('settings.data.yuque.check.empty_token')) + return + } + if (!yuqueUrl) { + window.message.error(t('settings.data.yuque.check.empty_url')) + return + } + + const response = await fetch('https://www.yuque.com/api/v2/hello', { + headers: { + 'X-Auth-Token': yuqueToken + } + }) + + if (!response.ok) { + window.message.error(t('settings.data.yuque.check.fail')) + return + } + const yuqueSlug = yuqueUrl.replace('https://www.yuque.com/', '') + const repoIDResponse = await fetch(`https://www.yuque.com/api/v2/repos/${yuqueSlug}`, { + headers: { + 'X-Auth-Token': yuqueToken + } + }) + if (!repoIDResponse.ok) { + window.message.error(t('settings.data.yuque.check.fail')) + return + } + const data = await repoIDResponse.json() + dispatch(setYuqueRepoId(data.data.id)) + window.message.success(t('settings.data.yuque.check.success')) + } + + const handleYuqueHelpClick = () => { + MinApp.start({ + id: 'yuque-help', + name: 'Yuque Help', + url: 'https://www.yuque.com/settings/tokens' + }) + } + + return ( + + {t('settings.data.yuque.title')} + + + {t('settings.data.yuque.repo_url')} + + + + + + + + {t('settings.data.yuque.token')} + + + + + + + + + + + ) +} + +export default YuqueSettings