Files
cherry-studio/src/renderer/src/pages/settings/DataSettings/DataSettings.tsx
T
beyondkmp be15206234 fix: Data config improvement (#7471)
* fix: update localization files for data migration warnings and path validation messages

* fix: update app data path validation and localization messages for installation path consistency

* fix: enhance app data flushing process by adding connection closure and delay in DataSettings component
2025-06-23 17:18:46 +08:00

757 lines
27 KiB
TypeScript

import {
CloudSyncOutlined,
FileSearchOutlined,
FolderOpenOutlined,
LoadingOutlined,
SaveOutlined,
YuqueOutlined
} from '@ant-design/icons'
import DividerWithText from '@renderer/components/DividerWithText'
import { NutstoreIcon } from '@renderer/components/Icons/NutstoreIcons'
import { HStack } from '@renderer/components/Layout'
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 store, { useAppDispatch } from '@renderer/store'
import { setSkipBackupFile as _setSkipBackupFile } from '@renderer/store/settings'
import { AppInfo } from '@renderer/types'
import { formatFileSize } from '@renderer/utils'
import { Button, Progress, Switch, Typography } from 'antd'
import { FileText, FolderCog, FolderInput, Sparkle } from 'lucide-react'
import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
import {
SettingContainer,
SettingDivider,
SettingGroup,
SettingHelpText,
SettingRow,
SettingRowTitle,
SettingTitle
} from '..'
import AgentsSubscribeUrlSettings from './AgentsSubscribeUrlSettings'
import ExportMenuOptions from './ExportMenuSettings'
import JoplinSettings from './JoplinSettings'
import MarkdownExportSettings from './MarkdownExportSettings'
import NotionSettings from './NotionSettings'
import NutstoreSettings from './NutstoreSettings'
import ObsidianSettings from './ObsidianSettings'
import SiyuanSettings from './SiyuanSettings'
import WebDavSettings from './WebDavSettings'
import YuqueSettings from './YuqueSettings'
const DataSettings: FC = () => {
const { t } = useTranslation()
const [appInfo, setAppInfo] = useState<AppInfo>()
const [cacheSize, setCacheSize] = useState<string>('')
const { size, removeAllFiles } = useKnowledgeFiles()
const { theme } = useTheme()
const [menu, setMenu] = useState<string>('data')
const _skipBackupFile = store.getState().settings.skipBackupFile
const [skipBackupFile, setSkipBackupFile] = useState<boolean>(_skipBackupFile)
const dispatch = useAppDispatch()
//joplin icon needs to be updated into iconfont
const JoplinIcon = () => (
<svg viewBox="0 0 24 24" width="16" height="16" fill="var(--color-icon)" xmlns="http://www.w3.org/2000/svg">
<path d="M20.97 0h-8.9a.15.15 0 00-.16.15v2.83c0 .1.08.17.18.17h1.22c.49 0 .89.38.93.86V17.4l-.01.36-.05.29-.04.13a2.06 2.06 0 01-.38.7l-.02.03a2.08 2.08 0 01-.37.34c-.5.35-1.17.5-1.92.43a4.66 4.66 0 01-2.67-1.22 3.96 3.96 0 01-1.34-2.42c-.1-.78.14-1.47.65-1.93l.07-.05c.37-.31.84-.5 1.39-.55a.09.09 0 00.01 0l.3-.01.35.01h.02a4.39 4.39 0 011.5.44c.15.08.17 0 .18-.06V9.63a.26.26 0 00-.2-.26 7.5 7.5 0 00-6.76 1.61 6.37 6.37 0 00-2.03 5.5 8.18 8.18 0 002.71 5.08A9.35 9.35 0 0011.81 24c1.88 0 3.62-.64 4.9-1.81a6.32 6.32 0 002.06-4.3l.01-10.86V4.08a.95.95 0 01.95-.93h1.22a.17.17 0 00.17-.17V.15a.15.15 0 00-.15-.15z" />
</svg>
)
const SiyuanIcon = () => (
<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2962" width="16" height="16">
<path
d="M309.76 148.16a84.8 84.8 0 0 0-10.88 11.84S288 170.24 288 171.2s-6.72 4.8-6.72 6.72-3.52 1.92-2.88 2.88a12.48 12.48 0 0 0-6.4 6.4 121.28 121.28 0 0 0-20.8 19.2 456.64 456.64 0 0 1-37.76 37.12v2.88c0 2.88 0 0 0 0s-3.52 1.92-6.72 5.12c-8.64 9.28-19.84 20.48-28.16 28.16l-7.04 7.04-2.56 2.88a114.88 114.88 0 0 0-20.16 21.76 2.88 2.88 0 0 1-8 8.64l-1.6 1.6a99.52 99.52 0 0 0-19.52 18.88 21.44 21.44 0 0 0-6.4 5.44c-14.08 14.4-22.4 23.04-22.72 23.04l-9.28 8.96-8.96 8.96V887.04c0 1.28 3.2 2.56 6.72-1.92s3.52-3.84 4.16-3.84 0-1.6 0 0S163.84 800 219.84 744.64l38.4-38.08c16-16.32 29.12-29.76 28.8-30.4s6.72-4.16 5.76-5.76 5.44-3.2 5.44-5.12 23.68-23.04 23.04-26.56 0-115.52 0-252.16V138.56a128 128 0 0 0-11.84 10.88z m373.76 2.24a96 96 0 0 0-13.44 15.04s-33.92 32-76.48 74.56l-42.56 42.88L512 320v504.96s5.76-5.12 5.12-5.76a29.44 29.44 0 0 0 8.32-7.68c3.84-4.16 9.92-10.24 13.76-13.76l21.44-21.76 21.76-21.44c18.56-18.24 32-32 32-32l8.96-9.6a69.76 69.76 0 0 1 10.56-9.6s3.84-1.92 3.84-3.52 6.4-4.48 5.76-5.12 3.2-2.56 2.56-3.2 1.6 0 0 0 11.52-10.24 24-22.72l22.72-22.4v-256-251.84c0-0.96 0-2.24-15.36 11.84z"
fill="#cdcdcd"
p-id="2963"></path>
<path
d="M322.24 136h0c-1.6 0 0-0.64 0 0z m2.88 0v504.64l45.12 44.16c37.44 36.8 93.76 92.8 116.48 114.88l14.4 15.04a64 64 0 0 0 10.24 9.6V320l-4.8-4.48c-2.88-2.24-7.68-7.36-11.52-10.88l-42.24-41.92-20.8-21.12-16-14.4a76.48 76.48 0 0 1-7.36-7.04l-23.36-23.68-42.56-44.16c-15.04-15.04-16-16-17.6-14.72z m376 1.92V640l123.84 123.84c98.24 97.92 124.48 123.52 126.4 123.52h2.56V386.56l-124.8-124.8C760 192 704 136.96 704 136.96a3.52 3.52 0 0 0-1.6 2.56z"
fill="#707070"
p-id="2964"></path>
<path
d="M699.52 136.64V136z m-376.96 249.6V136.96s-0.32 50.56 0 249.28zM512 573.76v-127.04zM667.84 672l-6.72 7.36 7.04-7.04c6.72-6.08 7.68-7.36 6.72-7.36zM184 272.96v1.92l2.56-1.92c2.56-1.92 0-2.24 0-2.24a5.44 5.44 0 0 0-2.56 2.24zM141.76 314.88a2.24 2.24 0 0 0 1.92 0v-1.6z m483.2 399.04a71.36 71.36 0 0 0-8.96 10.24 69.76 69.76 0 0 0 10.56-9.6 56 56 0 0 0 8.96-10.24 73.28 73.28 0 0 0-10.56 9.6z m-448 75.52l-3.2 3.2 3.52-2.88 3.52-3.52s-2.56 0-5.44 3.2z m-97.92 96v1.92l2.88-1.92s1.92-2.24 0-2.24a6.72 6.72 0 0 0-4.48 2.88z"
p-id="2965"></path>
</svg>
)
const menuItems = [
{ key: 'divider_0', isDivider: true, text: t('settings.data.divider.basic') },
{ key: 'data', title: 'settings.data.data.title', icon: <FolderCog size={16} /> },
{ key: 'divider_1', isDivider: true, text: t('settings.data.divider.cloud_storage') },
{ key: 'webdav', title: 'settings.data.webdav.title', icon: <CloudSyncOutlined style={{ fontSize: 16 }} /> },
{ key: 'nutstore', title: 'settings.data.nutstore.title', icon: <NutstoreIcon /> },
{ key: 'divider_2', isDivider: true, text: t('settings.data.divider.export_settings') },
{
key: 'export_menu',
title: 'settings.data.export_menu.title',
icon: <FolderInput size={16} />
},
{
key: 'markdown_export',
title: 'settings.data.markdown_export.title',
icon: <FileText size={16} />
},
{ key: 'divider_3', isDivider: true, text: t('settings.data.divider.third_party') },
{ key: 'notion', title: 'settings.data.notion.title', icon: <i className="iconfont icon-notion" /> },
{
key: 'yuque',
title: 'settings.data.yuque.title',
icon: <YuqueOutlined style={{ fontSize: 16 }} />
},
{
key: 'joplin',
title: 'settings.data.joplin.title',
icon: <JoplinIcon />
},
{
key: 'obsidian',
title: 'settings.data.obsidian.title',
icon: <i className="iconfont icon-obsidian" />
},
{
key: 'siyuan',
title: 'settings.data.siyuan.title',
icon: <SiyuanIcon />
},
{
key: 'agentssubscribe_url',
title: 'agents.settings.title',
icon: <Sparkle size={16} className="icon" />
}
]
useEffect(() => {
window.api.getAppInfo().then(setAppInfo)
window.api.getCacheSize().then(setCacheSize)
}, [])
const handleOpenPath = (path?: string) => {
if (!path) return
if (path?.endsWith('log')) {
const dirPath = path.split(/[/\\]/).slice(0, -1).join('/')
window.api.openPath(dirPath)
} else {
window.api.openPath(path)
}
}
const handleClearCache = () => {
window.modal.confirm({
title: t('settings.data.clear_cache.title'),
content: t('settings.data.clear_cache.confirm'),
okText: t('settings.data.clear_cache.button'),
centered: true,
okButtonProps: {
danger: true
},
onOk: async () => {
try {
await window.api.clearCache()
await window.api.getCacheSize().then(setCacheSize)
window.message.success(t('settings.data.clear_cache.success'))
} catch (error) {
window.message.error(t('settings.data.clear_cache.error'))
}
}
})
}
const handleRemoveAllFiles = () => {
window.modal.confirm({
centered: true,
title: t('settings.data.app_knowledge.remove_all') + ` (${formatFileSize(size)}) `,
content: t('settings.data.app_knowledge.remove_all_confirm'),
onOk: async () => {
await removeAllFiles()
window.message.success(t('settings.data.app_knowledge.remove_all_success'))
},
okText: t('common.delete'),
okButtonProps: {
danger: true
}
})
}
const handleSelectAppDataPath = async () => {
if (!appInfo || !appInfo.appDataPath) {
return
}
const newAppDataPath = await window.api.select({
properties: ['openDirectory', 'createDirectory'],
title: t('settings.data.app_data.select_title')
})
if (!newAppDataPath) {
return
}
// check new app data path is root path
// if is root path, show error
const pathParts = newAppDataPath.split(/[/\\]/).filter((part: string) => part !== '')
if (pathParts.length <= 1) {
window.message.error(t('settings.data.app_data.select_error_root_path'))
return
}
// check new app data path is not in old app data path
if (newAppDataPath.startsWith(appInfo.appDataPath)) {
window.message.error(t('settings.data.app_data.select_error_same_path'))
return
}
// check new app data path is not in app install path
if (newAppDataPath.startsWith(appInfo.installPath)) {
window.message.error(t('settings.data.app_data.select_error_in_app_path'))
return
}
// check new app data path has write permission
const hasWritePermission = await window.api.hasWritePermission(newAppDataPath)
if (!hasWritePermission) {
window.message.error(t('settings.data.app_data.select_error_write_permission'))
return
}
const migrationTitle = (
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>{t('settings.data.app_data.migration_title')}</div>
)
const migrationClassName = 'migration-modal'
showMigrationConfirmModal(appInfo.appDataPath, newAppDataPath, migrationTitle, migrationClassName)
}
const doubleConfirmModalBeforeCopyData = (newPath: string) => {
window.modal.confirm({
title: t('settings.data.app_data.select_not_empty_dir'),
content: t('settings.data.app_data.select_not_empty_dir_content'),
centered: true,
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: () => {
window.message.info({
content: t('settings.data.app_data.restart_notice'),
duration: 2
})
setTimeout(() => {
window.api.relaunchApp({
args: ['--new-data-path=' + newPath]
})
}, 500)
}
})
}
// 显示确认迁移的对话框
const showMigrationConfirmModal = async (
originalPath: string,
newPath: string,
title: React.ReactNode,
className: string
) => {
// 复制数据选项状态
let shouldCopyData = !(await window.api.isNotEmptyDir(newPath))
// 创建路径内容组件
const PathsContent = () => (
<div>
<MigrationPathRow>
<MigrationPathLabel>{t('settings.data.app_data.original_path')}:</MigrationPathLabel>
<MigrationPathValue>{originalPath}</MigrationPathValue>
</MigrationPathRow>
<MigrationPathRow style={{ marginTop: '16px' }}>
<MigrationPathLabel>{t('settings.data.app_data.new_path')}:</MigrationPathLabel>
<MigrationPathValue>{newPath}</MigrationPathValue>
</MigrationPathRow>
</div>
)
const CopyDataContent = () => (
<div>
<MigrationPathRow style={{ marginTop: '20px', flexDirection: 'row', alignItems: 'center' }}>
<Switch
defaultChecked={shouldCopyData}
onChange={(checked) => {
shouldCopyData = checked
}}
style={{ marginRight: '8px' }}
/>
<MigrationPathLabel style={{ fontWeight: 'normal', fontSize: '14px' }}>
{t('settings.data.app_data.copy_data_option')}
</MigrationPathLabel>
</MigrationPathRow>
</div>
)
// 显示确认模态框
window.modal.confirm({
title,
className,
width: 'min(600px, 90vw)',
style: { minHeight: '400px' },
content: (
<MigrationModalContent>
<PathsContent />
<CopyDataContent />
<MigrationNotice>
<p style={{ color: 'var(--color-warning)' }}>{t('settings.data.app_data.restart_notice')}</p>
<p style={{ color: 'var(--color-text-3)', marginTop: '8px' }}>
{t('settings.data.app_data.copy_time_notice')}
</p>
</MigrationNotice>
</MigrationModalContent>
),
centered: true,
okButtonProps: {
danger: true
},
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
try {
if (shouldCopyData) {
if (await window.api.isNotEmptyDir(newPath)) {
doubleConfirmModalBeforeCopyData(newPath)
return
}
window.message.info({
content: t('settings.data.app_data.restart_notice'),
duration: 3
})
setTimeout(() => {
window.api.relaunchApp({
args: ['--new-data-path=' + newPath]
})
}, 500)
return
}
// 如果不复制数据,直接设置新的应用数据路径
await window.api.setAppDataPath(newPath)
window.message.success(t('settings.data.app_data.path_changed_without_copy'))
// 更新应用数据路径
setAppInfo(await window.api.getAppInfo())
// 通知用户并重启应用
setTimeout(() => {
window.message.success(t('settings.data.app_data.select_success'))
window.api.setStopQuitApp(false, '')
window.api.relaunchApp()
}, 500)
} catch (error) {
window.api.setStopQuitApp(false, '')
window.message.error({
content: t('settings.data.app_data.path_change_failed') + ': ' + error,
duration: 5
})
}
}
})
}
useEffect(() => {
const handleDataMigration = async () => {
const newDataPath = await window.api.getDataPathFromArgs()
if (!newDataPath) return
const originalPath = (await window.api.getAppInfo())?.appDataPath
if (!originalPath) return
const title = (
<div style={{ fontSize: '18px', fontWeight: 'bold' }}>{t('settings.data.app_data.migration_title')}</div>
)
const className = 'migration-modal'
const messageKey = 'data-migration'
// Create PathsContent component for this specific migration
const PathsContent = () => (
<div>
<MigrationPathRow>
<MigrationPathLabel>{t('settings.data.app_data.original_path')}:</MigrationPathLabel>
<MigrationPathValue>{originalPath}</MigrationPathValue>
</MigrationPathRow>
<MigrationPathRow style={{ marginTop: '16px' }}>
<MigrationPathLabel>{t('settings.data.app_data.new_path')}:</MigrationPathLabel>
<MigrationPathValue>{newDataPath}</MigrationPathValue>
</MigrationPathRow>
</div>
)
const { loadingModal, progressInterval, updateProgress } = showProgressModal(title, className, PathsContent)
try {
window.api.setStopQuitApp(true, t('settings.data.app_data.stop_quit_app_reason'))
await startMigration(originalPath, newDataPath, progressInterval, updateProgress, loadingModal, messageKey)
// 更新应用数据路径
setAppInfo(await window.api.getAppInfo())
// 通知用户并重启应用
setTimeout(() => {
window.message.success(t('settings.data.app_data.select_success'))
window.api.setStopQuitApp(false, '')
window.api.relaunchApp({
args: ['--user-data-dir=' + newDataPath]
})
}, 1000)
} catch (error) {
window.api.setStopQuitApp(false, '')
window.message.error({
content: t('settings.data.app_data.copy_failed') + ': ' + error,
key: messageKey,
duration: 5
})
} finally {
if (progressInterval) {
clearInterval(progressInterval)
}
loadingModal.destroy()
}
}
handleDataMigration()
}, [])
// 显示进度模态框
const showProgressModal = (title: React.ReactNode, className: string, PathsContent: React.FC) => {
let currentProgress = 0
let progressInterval: NodeJS.Timeout | null = null
// 创建进度更新模态框
const loadingModal = window.modal.info({
title,
className,
width: 'min(600px, 90vw)',
style: { minHeight: '400px' },
icon: <LoadingOutlined style={{ fontSize: 18 }} />,
content: (
<MigrationModalContent>
<PathsContent />
<MigrationNotice>
<p>{t('settings.data.app_data.copying')}</p>
<div style={{ marginTop: '12px' }}>
<Progress percent={currentProgress} status="active" strokeWidth={8} />
</div>
<p style={{ color: 'var(--color-warning)', marginTop: '12px', fontSize: '13px' }}>
{t('settings.data.app_data.copying_warning')}
</p>
</MigrationNotice>
</MigrationModalContent>
),
centered: true,
closable: false,
maskClosable: false,
okButtonProps: { style: { display: 'none' } }
})
// 更新进度的函数
const updateProgress = (progress: number, status: 'active' | 'success' = 'active') => {
loadingModal.update({
title,
content: (
<MigrationModalContent>
<PathsContent />
<MigrationNotice>
<p>{t('settings.data.app_data.copying')}</p>
<div style={{ marginTop: '12px' }}>
<Progress percent={Math.round(progress)} status={status} strokeWidth={8} />
</div>
<p style={{ color: 'var(--color-warning)', marginTop: '12px', fontSize: '13px' }}>
{t('settings.data.app_data.copying_warning')}
</p>
</MigrationNotice>
</MigrationModalContent>
)
})
}
// 开始模拟进度更新
progressInterval = setInterval(() => {
if (currentProgress < 95) {
currentProgress += Math.random() * 5 + 1
if (currentProgress > 95) currentProgress = 95
updateProgress(currentProgress)
}
}, 500)
return { loadingModal, progressInterval, updateProgress }
}
// 开始迁移数据
const startMigration = async (
originalPath: string,
newPath: string,
progressInterval: NodeJS.Timeout | null,
updateProgress: (progress: number, status?: 'active' | 'success') => void,
loadingModal: { destroy: () => void },
messageKey: string
): Promise<void> => {
// flush app data
await window.api.flushAppData()
// wait 2 seconds to flush app data
await new Promise((resolve) => setTimeout(resolve, 2000))
// 开始复制过程
const copyResult = await window.api.copy(originalPath, newPath)
// 停止进度更新
if (progressInterval) {
clearInterval(progressInterval)
}
// 显示100%完成
updateProgress(100, 'success')
if (!copyResult.success) {
// 延迟关闭加载模态框
await new Promise<void>((resolve) => {
setTimeout(() => {
loadingModal.destroy()
window.message.error({
content: t('settings.data.app_data.copy_failed') + ': ' + copyResult.error,
key: messageKey,
duration: 5
})
resolve()
}, 500)
})
throw new Error(copyResult.error || 'Unknown error during copy')
}
// 在复制成功后设置新的AppDataPath
await window.api.setAppDataPath(newPath)
// 短暂延迟以显示100%完成
await new Promise((resolve) => setTimeout(resolve, 500))
// 关闭加载模态框
loadingModal.destroy()
window.message.success({
content: t('settings.data.app_data.copy_success'),
key: messageKey,
duration: 2
})
}
const onSkipBackupFilesChange = (value: boolean) => {
setSkipBackupFile(value)
dispatch(_setSkipBackupFile(value))
}
return (
<Container>
<MenuList>
{menuItems.map((item) =>
item.isDivider ? (
<DividerWithText key={item.key} text={item.text || ''} style={{ margin: '8px 0' }} /> // 动态传递分隔符文字
) : (
<ListItem
key={item.key}
title={t(item.title || '')}
active={menu === item.key}
onClick={() => setMenu(item.key)}
titleStyle={{ fontWeight: 500 }}
icon={item.icon}
/>
)
)}
</MenuList>
<SettingContainer theme={theme} style={{ display: 'flex', flex: 1 }}>
{menu === 'data' && (
<>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
<HStack gap="5px" justifyContent="space-between">
<Button onClick={BackupPopup.show} icon={<SaveOutlined />}>
{t('settings.general.backup.button')}
</Button>
<Button onClick={RestorePopup.show} icon={<FolderOpenOutlined />}>
{t('settings.general.restore.button')}
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
<HStack gap="5px">
<Button onClick={reset} danger>
{t('settings.general.reset.button')}
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
<Switch checked={skipBackupFile} onChange={onSkipBackupFilesChange} />
</SettingRow>
<SettingRow>
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
</SettingRow>
</SettingGroup>
<SettingGroup theme={theme}>
<SettingTitle>{t('settings.data.data.title')}</SettingTitle>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.app_data')}</SettingRowTitle>
<PathRow>
<PathText style={{ color: 'var(--color-text-3)' }}>{appInfo?.appDataPath}</PathText>
<StyledIcon onClick={() => handleOpenPath(appInfo?.appDataPath)} style={{ flexShrink: 0 }} />
<HStack gap="5px" style={{ marginLeft: '8px' }}>
<Button onClick={handleSelectAppDataPath}>{t('settings.data.app_data.select')}</Button>
</HStack>
</PathRow>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.app_logs')}</SettingRowTitle>
<PathRow>
<PathText style={{ color: 'var(--color-text-3)' }}>{appInfo?.logsPath}</PathText>
<StyledIcon onClick={() => handleOpenPath(appInfo?.logsPath)} style={{ flexShrink: 0 }} />
</PathRow>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>{t('settings.data.app_knowledge')}</SettingRowTitle>
<HStack alignItems="center" gap="5px">
<Button onClick={handleRemoveAllFiles} danger>
{t('settings.data.app_knowledge.button.delete')}
</Button>
</HStack>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitle>
{t('settings.data.clear_cache.title')}
{cacheSize && <CacheText>({cacheSize}MB)</CacheText>}
</SettingRowTitle>
<HStack gap="5px">
<Button onClick={handleClearCache} danger>
{t('settings.data.clear_cache.button')}
</Button>
</HStack>
</SettingRow>
</SettingGroup>
</>
)}
{menu === 'webdav' && <WebDavSettings />}
{menu === 'nutstore' && <NutstoreSettings />}
{menu === 'export_menu' && <ExportMenuOptions />}
{menu === 'markdown_export' && <MarkdownExportSettings />}
{menu === 'notion' && <NotionSettings />}
{menu === 'yuque' && <YuqueSettings />}
{menu === 'joplin' && <JoplinSettings />}
{menu === 'obsidian' && <ObsidianSettings />}
{menu === 'siyuan' && <SiyuanSettings />}
{menu === 'agentssubscribe_url' && <AgentsSubscribeUrlSettings />}
</SettingContainer>
</Container>
)
}
const Container = styled(HStack)`
flex: 1;
`
const StyledIcon = styled(FileSearchOutlined)`
color: var(--color-text-2);
cursor: pointer;
transition: color 0.3s;
&:hover {
color: var(--color-text-1);
}
`
const MenuList = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
width: var(--settings-width);
padding: 12px;
border-right: 0.5px solid var(--color-border);
height: 100%;
.iconfont {
color: var(--color-text-2);
line-height: 16px;
}
`
const CacheText = styled(Typography.Text)`
color: var(--color-text-3);
font-size: 12px;
margin-left: 5px;
line-height: 16px;
display: inline-block;
vertical-align: middle;
text-align: left;
`
const PathText = styled(Typography.Text)`
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
display: inline-block;
vertical-align: middle;
text-align: right;
margin-left: 5px;
`
const PathRow = styled(HStack)`
min-width: 0;
flex: 1;
width: 0;
align-items: center;
gap: 5px;
`
// Add styled components for migration modal
const MigrationModalContent = styled.div`
padding: 20px 0 10px;
display: flex;
flex-direction: column;
`
const MigrationNotice = styled.div`
margin-top: 24px;
font-size: 14px;
`
const MigrationPathRow = styled.div`
display: flex;
flex-direction: column;
gap: 5px;
`
const MigrationPathLabel = styled.div`
font-weight: 600;
font-size: 15px;
color: var(--color-text-1);
`
const MigrationPathValue = styled.div`
font-size: 14px;
color: var(--color-text-2);
background-color: var(--color-background-soft);
padding: 8px 12px;
border-radius: 4px;
word-break: break-all;
border: 1px solid var(--color-border);
`
export default DataSettings