mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
refactor(settings): settings card barrel + data/integration/websearch/channels/model
Signed-off-by: Siin Xu <31815270+SiinXu@users.noreply.github.com>
This commit is contained in:
@@ -95,10 +95,10 @@ function formatTime(ts: number): string {
|
||||
}
|
||||
|
||||
const LOG_LEVEL_COLORS: Record<string, string> = {
|
||||
error: '#ff4d4f',
|
||||
warn: '#faad14',
|
||||
info: '#1677ff',
|
||||
debug: '#8c8c8c'
|
||||
error: 'var(--color-error-base)',
|
||||
warn: 'var(--color-warning-base)',
|
||||
info: 'var(--color-info-base)',
|
||||
debug: 'var(--color-foreground-muted)'
|
||||
}
|
||||
|
||||
const NO_AGENT_VALUE = '__none'
|
||||
@@ -157,7 +157,7 @@ const ChannelLogModal: FC<{
|
||||
{logs.length > 0 && <CopyButton textToCopy={logsText} size={14} />}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="max-h-100 overflow-y-auto rounded-md bg-background-subtle p-2 font-mono text-[11px] leading-[1.6]">
|
||||
<div className="text-(length:--font-size-body-xs) max-h-100 overflow-y-auto rounded-md bg-background-subtle p-2 font-mono leading-[1.6]">
|
||||
{logs.length === 0 && (
|
||||
<div className="py-8 text-center text-muted-foreground text-xs">
|
||||
{t('agent.cherryClaw.channels.noLogs')}
|
||||
@@ -166,7 +166,8 @@ const ChannelLogModal: FC<{
|
||||
{logs.map((entry, i) => (
|
||||
<div key={i} className="flex gap-2 whitespace-pre-wrap py-px">
|
||||
<span className="shrink-0 text-muted-foreground">{formatTime(entry.timestamp)}</span>
|
||||
<span style={{ color: LOG_LEVEL_COLORS[entry.level] ?? '#8c8c8c', fontWeight: 500 }}>
|
||||
<span
|
||||
style={{ color: LOG_LEVEL_COLORS[entry.level] ?? 'var(--color-foreground-muted)', fontWeight: 500 }}>
|
||||
[{entry.level.toUpperCase()}]
|
||||
</span>
|
||||
<span className="break-all">{entry.message}</span>
|
||||
@@ -349,17 +350,17 @@ const ChannelInstanceRow: FC<{
|
||||
let statusTag: React.ReactNode = null
|
||||
if (channel.isActive) {
|
||||
if (isConnected) {
|
||||
statusColor = 'bg-green-500'
|
||||
statusColor = 'bg-success'
|
||||
statusTag = (
|
||||
<Badge className="border-success/30 bg-success/10 px-1.5 py-0 text-[10px] text-success leading-3.5">
|
||||
<Badge className="text-(length:--font-size-body-2xs) border-success/30 bg-success/10 px-1.5 py-0 text-success leading-3.5">
|
||||
{t('agent.cherryClaw.channels.connected')}
|
||||
</Badge>
|
||||
)
|
||||
} else if (hasError) {
|
||||
statusColor = 'bg-red-500'
|
||||
statusColor = 'bg-destructive'
|
||||
statusTag = (
|
||||
<Tooltip title={hasError}>
|
||||
<Badge className="border-destructive/30 bg-destructive/10 px-1.5 py-0 text-[10px] text-destructive leading-3.5">
|
||||
<Badge className="text-(length:--font-size-body-2xs) border-destructive/30 bg-destructive/10 px-1.5 py-0 text-destructive leading-3.5">
|
||||
{t('agent.cherryClaw.channels.error')}
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
@@ -376,7 +377,7 @@ const ChannelInstanceRow: FC<{
|
||||
{statusTag}
|
||||
</div>
|
||||
<div className="truncate text-foreground-400 text-xs">
|
||||
{agentName && <span className="mr-2 text-blue-400">{agentName}</span>}
|
||||
{agentName && <span className="mr-2 text-info">{agentName}</span>}
|
||||
{summary}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -169,13 +169,11 @@ const ChannelFieldsForm: FC<ChannelFieldsFormProps> = ({
|
||||
/>
|
||||
<span className="mt-1 block text-gray-400 text-xs">{chatIdsConfig.hint}</span>
|
||||
{!chatIds.trim() && idsKey === 'allowed_chat_ids' && (
|
||||
<span className="mt-1 block text-orange-400 text-xs">
|
||||
<span className="mt-1 block text-warning text-xs">
|
||||
{t('agent.cherryClaw.channels.chatIdsAutoTrackHint')}
|
||||
</span>
|
||||
)}
|
||||
{chatIdsConfig.extraHint && (
|
||||
<span className="mt-1 block text-blue-400 text-xs">{chatIdsConfig.extraHint}</span>
|
||||
)}
|
||||
{chatIdsConfig.extraHint && <span className="mt-1 block text-info text-xs">{chatIdsConfig.extraHint}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<ChannelPermissionMode channel={channel} onConfigChange={onConfigChange} />
|
||||
@@ -262,23 +260,23 @@ export const FeishuForm: FC<ChannelFormProps> = ({ channel, onConfigChange }) =>
|
||||
{!hasCredentials && (
|
||||
<div className="flex items-center gap-2">
|
||||
{status === 'pending' && (
|
||||
<span className="text-blue-400 text-xs">{t('agent.cherryClaw.channels.feishu.qrHint')}</span>
|
||||
<span className="text-info text-xs">{t('agent.cherryClaw.channels.feishu.qrHint')}</span>
|
||||
)}
|
||||
{status === 'expired' && (
|
||||
<>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-red-500" />
|
||||
<span className="text-red-500 text-xs">{t('agent.cherryClaw.channels.feishu.qrExpired')}</span>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-destructive" />
|
||||
<span className="text-destructive text-xs">{t('agent.cherryClaw.channels.feishu.qrExpired')}</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'idle' && (
|
||||
<span className="text-blue-400 text-xs">{t('agent.cherryClaw.channels.feishu.loginHint')}</span>
|
||||
<span className="text-info text-xs">{t('agent.cherryClaw.channels.feishu.loginHint')}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasCredentials && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className="text-green-600 text-xs">{t('agent.cherryClaw.channels.feishu.connected')}</span>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-success" />
|
||||
<span className="text-success text-xs">{t('agent.cherryClaw.channels.feishu.connected')}</span>
|
||||
</div>
|
||||
)}
|
||||
<ChannelFieldsForm
|
||||
@@ -440,18 +438,18 @@ export const WeChatForm: FC<ChannelFormProps & { onRemove?: () => void }> = ({ c
|
||||
<div className="flex items-center gap-2">
|
||||
{status === 'confirmed' && (
|
||||
<>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-green-500" />
|
||||
<span className="text-green-600 text-xs">{t('agent.cherryClaw.channels.wechat.connected')}</span>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-success" />
|
||||
<span className="text-success text-xs">{t('agent.cherryClaw.channels.wechat.connected')}</span>
|
||||
</>
|
||||
)}
|
||||
{status === 'disconnected' && (
|
||||
<>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-red-500" />
|
||||
<span className="text-red-500 text-xs">{t('agent.cherryClaw.channels.wechat.disconnected')}</span>
|
||||
<span className="inline-block h-2 w-2 rounded-full bg-destructive" />
|
||||
<span className="text-destructive text-xs">{t('agent.cherryClaw.channels.wechat.disconnected')}</span>
|
||||
</>
|
||||
)}
|
||||
{(status === 'idle' || status === 'pending') && (
|
||||
<span className="text-blue-400 text-xs">{t('agent.cherryClaw.channels.wechat.loginHint')}</span>
|
||||
<span className="text-info text-xs">{t('agent.cherryClaw.channels.wechat.loginHint')}</span>
|
||||
)}
|
||||
</div>
|
||||
{loginUserId && status === 'confirmed' && (
|
||||
|
||||
@@ -13,7 +13,7 @@ import type React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
/**
|
||||
* @deprecated v1 leftover. v2's preboot relocation copies the entire Electron
|
||||
@@ -439,110 +439,116 @@ const BasicDataSettings: React.FC = () => {
|
||||
<>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={() => BackupPopup.show()} variant="outline">
|
||||
<SaveIcon size={14} />
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={RestorePopup.show} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
|
||||
<Switch checked={skipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button onClick={() => BackupPopup.show()} variant="outline">
|
||||
<SaveIcon size={14} strokeWidth={1.6} />
|
||||
{t('settings.general.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={RestorePopup.show} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.general.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.backup.skip_file_data_help')}>
|
||||
{t('settings.data.backup.skip_file_data_title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={skipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.export_to_phone.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_to_phone.lan.title')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={LanTransferPopup.show} variant="outline">
|
||||
<Wifi size={14} />
|
||||
{t('settings.data.export_to_phone.lan.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_to_phone.file.title')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={() => BackupPopup.show('lan-transfer')} variant="outline">
|
||||
<FolderInput size={14} />
|
||||
{t('settings.data.export_to_phone.file.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_to_phone.lan.title')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button onClick={LanTransferPopup.show} variant="outline">
|
||||
<Wifi size={14} strokeWidth={1.6} />
|
||||
{t('settings.data.export_to_phone.lan.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_to_phone.file.title')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button
|
||||
onClick={() => BackupPopup.show('lan-transfer')}
|
||||
variant="outline"
|
||||
className="[&_svg]:[stroke-width:var(--icon-stroke)]">
|
||||
<FolderInput size={14} />
|
||||
{t('settings.data.export_to_phone.file.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.data.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_data.label')}</SettingRowTitle>
|
||||
<PathRow>
|
||||
<PathText
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
onClick={() => handleOpenPath(appInfo?.appDataPath)}>
|
||||
{appInfo?.appDataPath}
|
||||
</PathText>
|
||||
<Tooltip title={t('settings.data.app_data.select')}>
|
||||
<FolderOutput onClick={handleSelectAppDataPath} style={{ cursor: 'pointer' }} size={16} />
|
||||
</Tooltip>
|
||||
<RowFlex className="ml-2 gap-1.25">
|
||||
<Button onClick={() => handleOpenPath(appInfo?.appDataPath)} variant="outline">
|
||||
{t('settings.data.app_data.open')}
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle className="flex-none">{t('settings.data.app_data.label')}</SettingRowTitle>
|
||||
<PathRow>
|
||||
<PathText
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
onClick={() => handleOpenPath(appInfo?.appDataPath)}>
|
||||
{appInfo?.appDataPath}
|
||||
</PathText>
|
||||
<Tooltip title={t('settings.data.app_data.select')}>
|
||||
<FolderOutput
|
||||
onClick={handleSelectAppDataPath}
|
||||
style={{ cursor: 'pointer' }}
|
||||
size={16}
|
||||
strokeWidth={1.6}
|
||||
className="text-foreground-muted"
|
||||
/>
|
||||
</Tooltip>
|
||||
<RowFlex className="ml-2 gap-1.25">
|
||||
<Button onClick={() => handleOpenPath(appInfo?.appDataPath)} variant="outline">
|
||||
{t('settings.data.app_data.open')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</PathRow>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle className="flex-none">{t('settings.data.app_logs.label')}</SettingRowTitle>
|
||||
<PathRow>
|
||||
<PathText
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
onClick={() => handleOpenPath(appInfo?.logsPath)}>
|
||||
{appInfo?.logsPath}
|
||||
</PathText>
|
||||
<RowFlex className="ml-2 gap-1.25">
|
||||
<Button onClick={() => handleOpenPath(appInfo?.logsPath)} variant="outline">
|
||||
{t('settings.data.app_logs.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</PathRow>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.data.clear_cache.title')}
|
||||
{cacheSize && <CacheText>({cacheSize}MB)</CacheText>}
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="gap-1.25">
|
||||
<Button onClick={handleClearCache} variant="outline">
|
||||
{t('settings.data.clear_cache.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</PathRow>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.app_logs.label')}</SettingRowTitle>
|
||||
<PathRow>
|
||||
<PathText
|
||||
style={{ color: 'var(--color-foreground-muted)' }}
|
||||
onClick={() => handleOpenPath(appInfo?.logsPath)}>
|
||||
{appInfo?.logsPath}
|
||||
</PathText>
|
||||
<RowFlex className="ml-2 gap-1.25">
|
||||
<Button onClick={() => handleOpenPath(appInfo?.logsPath)} variant="outline">
|
||||
{t('settings.data.app_logs.button')}
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<RowFlex className="gap-1.25">
|
||||
<Button onClick={reset} variant="destructive">
|
||||
{t('settings.general.reset.title')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</PathRow>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.data.clear_cache.title')}
|
||||
{cacheSize && <CacheText>({cacheSize}MB)</CacheText>}
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="gap-1.25">
|
||||
<Button onClick={handleClearCache} variant="outline">
|
||||
{t('settings.data.clear_cache.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.reset.title')}</SettingRowTitle>
|
||||
<RowFlex className="gap-1.25">
|
||||
<Button onClick={reset} variant="destructive">
|
||||
{t('settings.general.reset.title')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
</>
|
||||
)
|
||||
@@ -604,7 +610,7 @@ const MigrationPathRow = ({ className, ...props }: React.ComponentPropsWithoutRe
|
||||
)
|
||||
|
||||
const MigrationPathLabel = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cn('font-semibold text-[15px] text-foreground', className)} {...props} />
|
||||
<div className={cn('text-(length:--font-size-body-md) font-semibold text-foreground', className)} {...props} />
|
||||
)
|
||||
|
||||
const MigrationPathValue = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useTheme } from '@renderer/hooks/useTheme'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
const ExportMenuOptions: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
@@ -31,87 +31,75 @@ const ExportMenuOptions: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.export_menu.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.image')}</SettingRowTitle>
|
||||
<Switch checked={exportMenuOptions.image} onCheckedChange={(checked) => handleToggleOption('image', checked)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.markdown')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.markdown}
|
||||
onCheckedChange={(checked) => handleToggleOption('markdown', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.markdown_reason')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.markdown_reason}
|
||||
onCheckedChange={(checked) => handleToggleOption('markdown_reason', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.notion')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.notion}
|
||||
onCheckedChange={(checked) => handleToggleOption('notion', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.yuque')}</SettingRowTitle>
|
||||
<Switch checked={exportMenuOptions.yuque} onCheckedChange={(checked) => handleToggleOption('yuque', checked)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.joplin')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.joplin}
|
||||
onCheckedChange={(checked) => handleToggleOption('joplin', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.obsidian')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.obsidian}
|
||||
onCheckedChange={(checked) => handleToggleOption('obsidian', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.siyuan')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.siyuan}
|
||||
onCheckedChange={(checked) => handleToggleOption('siyuan', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle>
|
||||
<Switch checked={exportMenuOptions.docx} onCheckedChange={(checked) => handleToggleOption('docx', checked)} />
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.plain_text')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.plain_text}
|
||||
onCheckedChange={(checked) => handleToggleOption('plain_text', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.image')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.image}
|
||||
onCheckedChange={(checked) => handleToggleOption('image', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.markdown')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.markdown}
|
||||
onCheckedChange={(checked) => handleToggleOption('markdown', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.markdown_reason')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.markdown_reason}
|
||||
onCheckedChange={(checked) => handleToggleOption('markdown_reason', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.notion')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.notion}
|
||||
onCheckedChange={(checked) => handleToggleOption('notion', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.yuque')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.yuque}
|
||||
onCheckedChange={(checked) => handleToggleOption('yuque', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.joplin')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.joplin}
|
||||
onCheckedChange={(checked) => handleToggleOption('joplin', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.obsidian')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.obsidian}
|
||||
onCheckedChange={(checked) => handleToggleOption('obsidian', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.siyuan')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.siyuan}
|
||||
onCheckedChange={(checked) => handleToggleOption('siyuan', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.docx')}</SettingRowTitle>
|
||||
<Switch checked={exportMenuOptions.docx} onCheckedChange={(checked) => handleToggleOption('docx', checked)} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.export_menu.plain_text')}</SettingRowTitle>
|
||||
<Switch
|
||||
checked={exportMenuOptions.plain_text}
|
||||
onCheckedChange={(checked) => handleToggleOption('plain_text', checked)}
|
||||
/>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,25 +4,24 @@ import { useTheme } from '@renderer/hooks/useTheme'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const ImportMenuOptions: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingRow>
|
||||
<SettingTitle>{t('settings.data.import_settings.title')}</SettingTitle>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.import_settings.chatgpt')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={ImportPopup.show} variant="outline">
|
||||
{t('settings.data.import_settings.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingTitle>{t('settings.data.import_settings.title')}</SettingTitle>
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.import_settings.chatgpt')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button onClick={ImportPopup.show} variant="outline">
|
||||
{t('settings.data.import_settings.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { FolderOpen, RefreshCw, Save, Trash2 } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const logger = loggerService.withContext('LocalBackupSettings')
|
||||
|
||||
@@ -181,115 +181,110 @@ const LocalBackupSettings: React.FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.local.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.directory.label')}</SettingRowTitle>
|
||||
<RowFlex className="gap-1.25">
|
||||
<Input
|
||||
value={localBackupDir}
|
||||
onChange={(e) => setLocalBackupDir(e.target.value)}
|
||||
onBlur={(e) => handleLocalBackupDirChange(e.target.value)}
|
||||
placeholder={t('settings.data.local.directory.placeholder')}
|
||||
style={{ minWidth: 200, maxWidth: 400, flex: 1 }}
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.directory.label')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-1 flex-wrap items-center justify-end gap-1.25">
|
||||
<Input
|
||||
value={localBackupDir}
|
||||
onChange={(e) => setLocalBackupDir(e.target.value)}
|
||||
onBlur={(e) => handleLocalBackupDirChange(e.target.value)}
|
||||
placeholder={t('settings.data.local.directory.placeholder')}
|
||||
className="min-w-0 max-w-[400px] flex-1"
|
||||
/>
|
||||
<Button onClick={handleBrowseDirectory} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('common.browse')}
|
||||
</Button>
|
||||
<Button onClick={handleClearDirectory} disabled={!localBackupDir} variant="destructive">
|
||||
<Trash2 size={14} />
|
||||
{t('common.clear')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button onClick={showBackupModal} disabled={!localBackupDir || backuping} variant="outline">
|
||||
<Save size={14} />
|
||||
{t('settings.data.local.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={showBackupManager} disabled={!localBackupDir} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.local.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={localBackupSyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!localBackupDir}
|
||||
options={[
|
||||
{ label: t('settings.data.local.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
<Button onClick={handleBrowseDirectory} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('common.browse')}
|
||||
</Button>
|
||||
<Button onClick={handleClearDirectory} disabled={!localBackupDir} variant="destructive">
|
||||
<Trash2 size={14} />
|
||||
{t('common.clear')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={showBackupModal} disabled={!localBackupDir || backuping} variant="outline">
|
||||
<Save size={14} />
|
||||
{t('settings.data.local.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={showBackupManager} disabled={!localBackupDir} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.local.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={localBackupSyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!localBackupDir}
|
||||
options={[
|
||||
{ label: t('settings.data.local.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.local.minute_interval', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.local.hour_interval', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.maxBackups.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={localBackupMaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!localBackupDir}
|
||||
options={[
|
||||
{ label: t('settings.data.local.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
|
||||
<Switch checked={localBackupSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
{localBackupSync && localBackupSyncInterval > 0 && (
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.maxBackups.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={localBackupMaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!localBackupDir}
|
||||
options={[
|
||||
{ label: t('settings.data.local.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.backup.skip_file_data_help')}>
|
||||
{t('settings.data.backup.skip_file_data_title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={localBackupSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
{localBackupSync && localBackupSyncInterval > 0 && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.local.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<LocalBackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
/>
|
||||
<LocalBackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
/>
|
||||
|
||||
<LocalBackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
localBackupDir={resolvedLocalBackupDir}
|
||||
/>
|
||||
</>
|
||||
<LocalBackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
localBackupDir={resolvedLocalBackupDir}
|
||||
/>
|
||||
</>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import { FolderOpen, Trash2 } from 'lucide-react'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const MarkdownExportSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -75,85 +75,73 @@ const MarkdownExportSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.markdown_export.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.path')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 items-center gap-1.25">
|
||||
<InputGroup className="h-8 w-62.5">
|
||||
<InputGroupInput
|
||||
type="text"
|
||||
value={markdownExportPath || ''}
|
||||
readOnly
|
||||
placeholder={t('settings.data.markdown_export.path_placeholder')}
|
||||
/>
|
||||
{markdownExportPath && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={handleClearPath}
|
||||
size="icon-sm"
|
||||
className="text-destructive hover:text-destructive">
|
||||
<Trash2 size={14} />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
<Button onClick={handleSelectFolder} variant="outline" className="h-8">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.markdown_export.select')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.force_dollar_math.title')}</SettingRowTitle>
|
||||
<Switch checked={forceDollarMathInMarkdown} onCheckedChange={handleToggleForceDollarMath} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.force_dollar_math.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.message_title.use_topic_naming.title')}</SettingRowTitle>
|
||||
<Switch checked={useTopicNamingForMessageTitle} onCheckedChange={handleToggleTopicNaming} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.message_title.use_topic_naming.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.show_model_name.title')}</SettingRowTitle>
|
||||
<Switch checked={showModelNameInExport} onCheckedChange={handleToggleShowModelName} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.show_model_name.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.show_model_provider.title')}</SettingRowTitle>
|
||||
<Switch checked={showModelProviderInMarkdown} onCheckedChange={handleToggleShowModelProvider} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.show_model_provider.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.exclude_citations.title')}</SettingRowTitle>
|
||||
<Switch checked={excludeCitationsInExport} onCheckedChange={handleToggleExcludeCitations} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.exclude_citations.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.markdown_export.standardize_citations.title')}</SettingRowTitle>
|
||||
<Switch checked={standardizeCitationsInExport} onCheckedChange={handleToggleStandardizeCitations} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.markdown_export.standardize_citations.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.markdown_export.help')}>
|
||||
{t('settings.data.markdown_export.path')}
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 max-w-78.75 flex-1 items-center justify-end gap-1.25">
|
||||
<InputGroup className="h-8 min-w-0 flex-1">
|
||||
<InputGroupInput
|
||||
type="text"
|
||||
value={markdownExportPath || ''}
|
||||
readOnly
|
||||
placeholder={t('settings.data.markdown_export.path_placeholder')}
|
||||
/>
|
||||
{markdownExportPath && (
|
||||
<InputGroupAddon align="inline-end">
|
||||
<InputGroupButton
|
||||
onClick={handleClearPath}
|
||||
size="icon-sm"
|
||||
className="text-destructive hover:text-destructive">
|
||||
<Trash2 size={14} />
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>
|
||||
)}
|
||||
</InputGroup>
|
||||
<Button onClick={handleSelectFolder} variant="outline" className="h-8">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.markdown_export.select')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.markdown_export.force_dollar_math.help')}>
|
||||
{t('settings.data.markdown_export.force_dollar_math.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={forceDollarMathInMarkdown} onCheckedChange={handleToggleForceDollarMath} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.message_title.use_topic_naming.help')}>
|
||||
{t('settings.data.message_title.use_topic_naming.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={useTopicNamingForMessageTitle} onCheckedChange={handleToggleTopicNaming} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.markdown_export.show_model_name.help')}>
|
||||
{t('settings.data.markdown_export.show_model_name.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={showModelNameInExport} onCheckedChange={handleToggleShowModelName} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.markdown_export.show_model_provider.help')}>
|
||||
{t('settings.data.markdown_export.show_model_provider.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={showModelProviderInMarkdown} onCheckedChange={handleToggleShowModelProvider} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.markdown_export.exclude_citations.help')}>
|
||||
{t('settings.data.markdown_export.exclude_citations.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={excludeCitationsInExport} onCheckedChange={handleToggleExcludeCitations} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.markdown_export.standardize_citations.help')}>
|
||||
{t('settings.data.markdown_export.standardize_citations.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={standardizeCitationsInExport} onCheckedChange={handleToggleStandardizeCitations} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { type FileStat } from 'webdav'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const NutstoreSettings: FC = () => {
|
||||
const { theme } = useTheme()
|
||||
@@ -206,162 +206,155 @@ const NutstoreSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.nutstore.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{isLogin ? t('settings.data.nutstore.isLogin') : t('settings.data.nutstore.notLogin')}
|
||||
</SettingRowTitle>
|
||||
{isLogin ? (
|
||||
<RowFlex className="items-center justify-between gap-1.25">
|
||||
<Button
|
||||
variant={nsConnected ? 'ghost' : 'outline'}
|
||||
onClick={handleCheckConnection}
|
||||
disabled={checkConnectionLoading}>
|
||||
{checkConnectionLoading ? (
|
||||
<Loader2 className="animate-spin" size={14} />
|
||||
) : nsConnected ? (
|
||||
<Check size={14} />
|
||||
) : (
|
||||
t('settings.data.nutstore.checkConnection.name')
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleLayout}>
|
||||
{t('settings.data.nutstore.logout.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
) : (
|
||||
<Button onClick={handleClickNutstoreSSO} variant="outline">
|
||||
{t('settings.data.nutstore.login.button')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
{isLogin && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.nutstore.username')}</SettingRowTitle>
|
||||
<span className="text-foreground-muted">{nutstoreUsername}</span>
|
||||
</SettingRow>
|
||||
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.nutstore.path.label')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1">
|
||||
<Input
|
||||
placeholder={t('settings.data.nutstore.path.placeholder')}
|
||||
style={{ width: 250 }}
|
||||
value={nutstorePath}
|
||||
onChange={(e) => {
|
||||
void setNutstorePath(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button variant="outline" onClick={handleClickPathChange} size="icon">
|
||||
<FolderOpen size={14} />
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{isLogin ? t('settings.data.nutstore.isLogin') : t('settings.data.nutstore.notLogin')}
|
||||
</SettingRowTitle>
|
||||
{isLogin ? (
|
||||
<RowFlex className="items-center justify-between gap-1.25">
|
||||
<Button
|
||||
variant={nsConnected ? 'ghost' : 'outline'}
|
||||
onClick={handleCheckConnection}
|
||||
disabled={checkConnectionLoading}>
|
||||
{checkConnectionLoading ? (
|
||||
<Loader2 className="animate-spin" size={14} />
|
||||
) : nsConnected ? (
|
||||
<Check size={14} />
|
||||
) : (
|
||||
t('settings.data.nutstore.checkConnection.name')
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleLayout}>
|
||||
{t('settings.data.nutstore.logout.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={showBackupModal} disabled={backuping} variant="outline">
|
||||
{t('settings.data.nutstore.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={showBackupManager} disabled={!nutstoreToken} variant="outline">
|
||||
{t('settings.data.nutstore.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={nutstoreSyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
options={[
|
||||
{ label: t('settings.data.webdav.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
{nutstoreAutoSync && nutstoreSyncInterval > 0 && (
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={handleClickNutstoreSSO} variant="outline">
|
||||
{t('settings.data.nutstore.login.button')}
|
||||
</Button>
|
||||
)}
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={nutstoreMaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!nutstoreToken}
|
||||
options={[
|
||||
{ label: t('settings.data.local.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
|
||||
<Switch checked={nutstoreSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<WebdavBackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
customLabels={{
|
||||
modalTitle: t('settings.data.nutstore.backup.modal.title'),
|
||||
filenamePlaceholder: t('settings.data.nutstore.backup.modal.filename.placeholder')
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
{isLogin && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.nutstore.username')}</SettingRowTitle>
|
||||
<span className="text-foreground-muted">{nutstoreUsername}</span>
|
||||
</SettingRow>
|
||||
|
||||
<WebdavBackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
webdavConfig={{
|
||||
webdavHost: NUTSTORE_HOST,
|
||||
webdavUser: nutstoreUsername,
|
||||
webdavPass: nutstorePass,
|
||||
webdavPath: nutstorePath
|
||||
}}
|
||||
restoreMethod={restoreFromNutstore}
|
||||
customLabels={{
|
||||
restoreConfirmTitle: t('settings.data.nutstore.restore.confirm.title'),
|
||||
restoreConfirmContent: t('settings.data.nutstore.restore.confirm.content'),
|
||||
invalidConfigMessage: t('message.error.invalid.nutstore')
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.nutstore.path.label')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1">
|
||||
<Input
|
||||
placeholder={t('settings.data.nutstore.path.placeholder')}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
value={nutstorePath}
|
||||
onChange={(e) => {
|
||||
void setNutstorePath(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<Button variant="outline" onClick={handleClickPathChange} size="icon">
|
||||
<FolderOpen size={14} />
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button onClick={showBackupModal} disabled={backuping} variant="outline">
|
||||
{t('settings.data.nutstore.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={showBackupManager} disabled={!nutstoreToken} variant="outline">
|
||||
{t('settings.data.nutstore.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={nutstoreSyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
options={[
|
||||
{ label: t('settings.data.webdav.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
{nutstoreAutoSync && nutstoreSyncInterval > 0 && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={nutstoreMaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!nutstoreToken}
|
||||
options={[
|
||||
{ label: t('settings.data.local.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.backup.skip_file_data_help')}>
|
||||
{t('settings.data.backup.skip_file_data_title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={nutstoreSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<WebdavBackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
customLabels={{
|
||||
modalTitle: t('settings.data.nutstore.backup.modal.title'),
|
||||
filenamePlaceholder: t('settings.data.nutstore.backup.modal.filename.placeholder')
|
||||
}}
|
||||
/>
|
||||
|
||||
<WebdavBackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
webdavConfig={{
|
||||
webdavHost: NUTSTORE_HOST,
|
||||
webdavUser: nutstoreUsername,
|
||||
webdavPass: nutstorePass,
|
||||
webdavPath: nutstorePath
|
||||
}}
|
||||
restoreMethod={restoreFromNutstore}
|
||||
customLabels={{
|
||||
restoreConfirmTitle: t('settings.data.nutstore.restore.confirm.title'),
|
||||
restoreConfirmContent: t('settings.data.nutstore.restore.confirm.content'),
|
||||
invalidConfigMessage: t('message.error.invalid.nutstore')
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const S3Settings: FC = () => {
|
||||
const [, setS3AutoSync] = usePreference('data.backup.s3.auto_sync')
|
||||
@@ -114,175 +114,165 @@ const S3Settings: FC = () => {
|
||||
/>
|
||||
</SettingTitle>
|
||||
<SettingHelpText>{t('settings.data.s3.title.help')}</SettingHelpText>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.endpoint.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.endpoint.placeholder')}
|
||||
value={s3Endpoint}
|
||||
onChange={(e) => setS3Endpoint(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
type="url"
|
||||
onBlur={(e) => setS3Endpoint(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.region.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.region.placeholder')}
|
||||
value={s3Region}
|
||||
onChange={(e) => setS3Region(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={(e) => setS3Region(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.bucket.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.bucket.placeholder')}
|
||||
value={s3Bucket}
|
||||
onChange={(e) => setS3Bucket(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={(e) => setS3Bucket(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.accessKeyId.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.accessKeyId.placeholder')}
|
||||
value={s3AccessKeyId}
|
||||
onChange={(e) => setS3AccessKeyId(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={(e) => setS3AccessKeyId(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.secretAccessKey.label')}</SettingRowTitle>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('settings.data.s3.secretAccessKey.placeholder')}
|
||||
value={s3SecretAccessKey}
|
||||
onChange={(e) => setS3SecretAccessKey(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={(e) => setS3SecretAccessKey(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.root.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.root.placeholder')}
|
||||
value={s3Root}
|
||||
onChange={(e) => setS3Root(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={(e) => setS3Root(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.backup.operation')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button
|
||||
onClick={showBackupModal}
|
||||
variant="outline"
|
||||
disabled={backuping || !s3Endpoint || !s3Region || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey}>
|
||||
<Save size={14} />
|
||||
{t('settings.data.s3.backup.button')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={showBackupManager}
|
||||
variant="outline"
|
||||
disabled={!s3Endpoint || !s3Region || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey}>
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.s3.backup.manager.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={s3SyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!s3Endpoint || !s3AccessKeyId || !s3SecretAccessKey}
|
||||
options={[
|
||||
{ label: t('settings.data.s3.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.maxBackups.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={s3MaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!s3Endpoint || !s3AccessKeyId || !s3SecretAccessKey}
|
||||
options={[
|
||||
{ label: t('settings.data.s3.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.skipBackupFile.label')}</SettingRowTitle>
|
||||
<Switch checked={s3SkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.s3.skipBackupFile.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
{s3SyncInterval > 0 && (
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.endpoint.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.endpoint.placeholder')}
|
||||
value={s3Endpoint}
|
||||
onChange={(e) => setS3Endpoint(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
type="url"
|
||||
onBlur={(e) => setS3Endpoint(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.region.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.region.placeholder')}
|
||||
value={s3Region}
|
||||
onChange={(e) => setS3Region(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={(e) => setS3Region(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.bucket.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.bucket.placeholder')}
|
||||
value={s3Bucket}
|
||||
onChange={(e) => setS3Bucket(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={(e) => setS3Bucket(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.accessKeyId.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.accessKeyId.placeholder')}
|
||||
value={s3AccessKeyId}
|
||||
onChange={(e) => setS3AccessKeyId(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={(e) => setS3AccessKeyId(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.secretAccessKey.label')}</SettingRowTitle>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('settings.data.s3.secretAccessKey.placeholder')}
|
||||
value={s3SecretAccessKey}
|
||||
onChange={(e) => setS3SecretAccessKey(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={(e) => setS3SecretAccessKey(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.root.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.s3.root.placeholder')}
|
||||
value={s3Root}
|
||||
onChange={(e) => setS3Root(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={(e) => setS3Root(e.target.value)}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.backup.operation')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button
|
||||
onClick={showBackupModal}
|
||||
variant="outline"
|
||||
disabled={backuping || !s3Endpoint || !s3Region || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey}>
|
||||
<Save size={14} />
|
||||
{t('settings.data.s3.backup.button')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={showBackupManager}
|
||||
variant="outline"
|
||||
disabled={!s3Endpoint || !s3Region || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey}>
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.s3.backup.manager.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={s3SyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!s3Endpoint || !s3AccessKeyId || !s3SecretAccessKey}
|
||||
options={[
|
||||
{ label: t('settings.data.s3.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.s3.autoSync.minute', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.s3.autoSync.hour', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.maxBackups.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={s3MaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!s3Endpoint || !s3AccessKeyId || !s3SecretAccessKey}
|
||||
options={[
|
||||
{ label: t('settings.data.s3.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.s3.skipBackupFile.help')}>
|
||||
{t('settings.data.s3.skipBackupFile.label')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={s3SkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
{s3SyncInterval > 0 && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.syncStatus.label')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.s3.syncStatus.label')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<S3BackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
/>
|
||||
<S3BackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
/>
|
||||
|
||||
<S3BackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
s3Config={{
|
||||
endpoint: s3Endpoint,
|
||||
region: s3Region,
|
||||
bucket: s3Bucket,
|
||||
accessKeyId: s3AccessKeyId,
|
||||
secretAccessKey: s3SecretAccessKey,
|
||||
root: s3Root
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<S3BackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
s3Config={{
|
||||
endpoint: s3Endpoint,
|
||||
region: s3Region,
|
||||
bucket: s3Bucket,
|
||||
accessKeyId: s3AccessKeyId,
|
||||
secretAccessKey: s3SecretAccessKey,
|
||||
root: s3Root
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { FC } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const WebDavSettings: FC = () => {
|
||||
const [, setWebdavAutoSync] = usePreference('data.backup.webdav.auto_sync')
|
||||
@@ -96,154 +96,144 @@ const WebDavSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.webdav.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.host.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.webdav.host.placeholder')}
|
||||
value={webdavHost}
|
||||
onChange={(e) => setWebdavHost(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
type="url"
|
||||
onBlur={() => setWebdavHost(webdavHost || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.user')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.webdav.user')}
|
||||
value={webdavUser}
|
||||
onChange={(e) => setWebdavUser(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={() => setWebdavUser(webdavUser || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.password')}</SettingRowTitle>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('settings.data.webdav.password')}
|
||||
value={webdavPass}
|
||||
onChange={(e) => setWebdavPass(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={() => setWebdavPass(webdavPass || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.path.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.webdav.path.placeholder')}
|
||||
value={webdavPath}
|
||||
onChange={(e) => setWebdavPath(e.target.value)}
|
||||
style={{ width: 250 }}
|
||||
onBlur={() => setWebdavPath(webdavPath || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="justify-between gap-1.25">
|
||||
<Button onClick={showBackupModal} disabled={backuping} variant="outline">
|
||||
<Save size={14} />
|
||||
{t('settings.data.webdav.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={showBackupManager} disabled={!webdavHost} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.webdav.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={webdavSyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!webdavHost}
|
||||
options={[
|
||||
{ label: t('settings.data.webdav.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={webdavMaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!webdavHost}
|
||||
options={[
|
||||
{ label: t('settings.data.local.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.backup.skip_file_data_title')}</SettingRowTitle>
|
||||
<Switch checked={webdavSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.backup.skip_file_data_help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.disableStream.title')}</SettingRowTitle>
|
||||
<Switch checked={webdavDisableStream} onCheckedChange={onDisableStreamChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.webdav.disableStream.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
{webdavSync && webdavSyncInterval > 0 && (
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.host.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.webdav.host.placeholder')}
|
||||
value={webdavHost}
|
||||
onChange={(e) => setWebdavHost(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
type="url"
|
||||
onBlur={() => setWebdavHost(webdavHost || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.user')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.webdav.user')}
|
||||
value={webdavUser}
|
||||
onChange={(e) => setWebdavUser(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={() => setWebdavUser(webdavUser || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.password')}</SettingRowTitle>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder={t('settings.data.webdav.password')}
|
||||
value={webdavPass}
|
||||
onChange={(e) => setWebdavPass(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={() => setWebdavPass(webdavPass || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.path.label')}</SettingRowTitle>
|
||||
<Input
|
||||
placeholder={t('settings.data.webdav.path.placeholder')}
|
||||
value={webdavPath}
|
||||
onChange={(e) => setWebdavPath(e.target.value)}
|
||||
className="min-w-0 max-w-[250px] flex-1"
|
||||
onBlur={() => setWebdavPath(webdavPath || '')}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.general.backup.title')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 flex-wrap justify-end gap-1.25">
|
||||
<Button onClick={showBackupModal} disabled={backuping} variant="outline">
|
||||
<Save size={14} />
|
||||
{t('settings.data.webdav.backup.button')}
|
||||
</Button>
|
||||
<Button onClick={showBackupManager} disabled={!webdavHost} variant="outline">
|
||||
<FolderOpen size={14} />
|
||||
{t('settings.data.webdav.restore.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.autoSync.label')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={webdavSyncInterval}
|
||||
onChange={onSyncIntervalChange}
|
||||
disabled={!webdavHost}
|
||||
options={[
|
||||
{ label: t('settings.data.webdav.autoSync.off'), value: 0 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 1 }), value: 1 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 5 }), value: 5 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 15 }), value: 15 },
|
||||
{ label: t('settings.data.webdav.minute_interval', { count: 30 }), value: 30 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 1 }), value: 60 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 2 }), value: 120 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 6 }), value: 360 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 12 }), value: 720 },
|
||||
{ label: t('settings.data.webdav.hour_interval', { count: 24 }), value: 1440 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.maxBackups')}</SettingRowTitle>
|
||||
<Selector
|
||||
size={14}
|
||||
value={webdavMaxBackups}
|
||||
onChange={onMaxBackupsChange}
|
||||
disabled={!webdavHost}
|
||||
options={[
|
||||
{ label: t('settings.data.local.maxBackups.unlimited'), value: 0 },
|
||||
{ label: '1', value: 1 },
|
||||
{ label: '3', value: 3 },
|
||||
{ label: '5', value: 5 },
|
||||
{ label: '10', value: 10 },
|
||||
{ label: '20', value: 20 },
|
||||
{ label: '50', value: 50 }
|
||||
]}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.backup.skip_file_data_help')}>
|
||||
{t('settings.data.backup.skip_file_data_title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={webdavSkipBackupFile} onCheckedChange={onSkipBackupFilesChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.webdav.disableStream.help')}>
|
||||
{t('settings.data.webdav.disableStream.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={webdavDisableStream} onCheckedChange={onDisableStreamChange} />
|
||||
</SettingRow>
|
||||
{webdavSync && webdavSyncInterval > 0 && (
|
||||
<>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.webdav.syncStatus')}</SettingRowTitle>
|
||||
{renderSyncStatus()}
|
||||
</SettingRow>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<WebdavBackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
/>
|
||||
<WebdavBackupModal
|
||||
isModalVisible={isModalVisible}
|
||||
handleBackup={handleBackup}
|
||||
handleCancel={handleCancel}
|
||||
backuping={backuping}
|
||||
customFileName={customFileName}
|
||||
setCustomFileName={setCustomFileName}
|
||||
/>
|
||||
|
||||
<WebdavBackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
webdavConfig={{
|
||||
webdavHost,
|
||||
webdavUser,
|
||||
webdavPass,
|
||||
webdavPath,
|
||||
webdavDisableStream
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<WebdavBackupManager
|
||||
visible={backupManagerVisible}
|
||||
onClose={closeBackupManager}
|
||||
webdavConfig={{
|
||||
webdavHost,
|
||||
webdavUser,
|
||||
webdavPass,
|
||||
webdavPath,
|
||||
webdavDisableStream
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const logger = loggerService.withContext('JoplinSettings')
|
||||
|
||||
@@ -72,55 +72,53 @@ const JoplinSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.joplin.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.joplin.url')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={joplinUrl || ''}
|
||||
onChange={handleJoplinUrlChange}
|
||||
onBlur={handleJoplinUrlBlur}
|
||||
className="w-78.75 max-w-full"
|
||||
placeholder={t('settings.data.joplin.url_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.joplin.token')}</span>
|
||||
<InfoTooltip
|
||||
content={t('settings.data.joplin.help')}
|
||||
placement="left"
|
||||
iconProps={{ className: 'text-text-2 cursor-pointer ml-1' }}
|
||||
onClick={handleJoplinHelpClick}
|
||||
/>
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.joplin.url')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={joplinToken || ''}
|
||||
onChange={handleJoplinTokenChange}
|
||||
onBlur={handleJoplinTokenChange}
|
||||
placeholder={t('settings.data.joplin.token_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
type="text"
|
||||
value={joplinUrl || ''}
|
||||
onChange={handleJoplinUrlChange}
|
||||
onBlur={handleJoplinUrlBlur}
|
||||
className="w-78.75 max-w-full"
|
||||
placeholder={t('settings.data.joplin.url_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleJoplinConnectionCheck} variant="outline" className="h-9 shrink-0">
|
||||
{t('settings.data.joplin.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.joplin.export_reasoning.title')}</SettingRowTitle>
|
||||
<Switch checked={joplinExportReasoning} onCheckedChange={handleToggleJoplinExportReasoning} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.joplin.export_reasoning.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.joplin.token')}</span>
|
||||
<InfoTooltip
|
||||
content={t('settings.data.joplin.help')}
|
||||
placement="left"
|
||||
iconProps={{ className: 'text-text-2 cursor-pointer ml-1' }}
|
||||
onClick={handleJoplinHelpClick}
|
||||
/>
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={joplinToken || ''}
|
||||
onChange={handleJoplinTokenChange}
|
||||
onBlur={handleJoplinTokenChange}
|
||||
placeholder={t('settings.data.joplin.token_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button onClick={handleJoplinConnectionCheck} variant="outline" className="h-8 shrink-0 rounded-lg">
|
||||
{t('settings.data.joplin.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.joplin.export_reasoning.help')}>
|
||||
{t('settings.data.joplin.export_reasoning.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={joplinExportReasoning} onCheckedChange={handleToggleJoplinExportReasoning} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingHelpText, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const logger = loggerService.withContext('NotionSettings')
|
||||
|
||||
@@ -78,59 +78,56 @@ const NotionSettings: FC = () => {
|
||||
onClick={handleNotionTitleClick}
|
||||
/>
|
||||
</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={notionDatabaseID || ''}
|
||||
onChange={handleNotionDatabaseIdChange}
|
||||
onBlur={handleNotionDatabaseIdChange}
|
||||
placeholder={t('settings.data.notion.database_id_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.page_name_key')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={notionPageNameKey || ''}
|
||||
onChange={handleNotionPageNameKeyChange}
|
||||
onBlur={handleNotionPageNameKeyChange}
|
||||
placeholder={t('settings.data.notion.page_name_key_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.database_id')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={notionApiKey || ''}
|
||||
onChange={handleNotionTokenChange}
|
||||
onBlur={handleNotionTokenChange}
|
||||
placeholder={t('settings.data.notion.api_key_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
type="text"
|
||||
value={notionDatabaseID || ''}
|
||||
onChange={handleNotionDatabaseIdChange}
|
||||
onBlur={handleNotionDatabaseIdChange}
|
||||
placeholder={t('settings.data.notion.database_id_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleNotionConnectionCheck} variant="outline" className="h-9 shrink-0">
|
||||
{t('settings.data.notion.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.export_reasoning.title')}</SettingRowTitle>
|
||||
<Switch checked={notionExportReasoning} onCheckedChange={handleNotionExportReasoningChange} />
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingHelpText>{t('settings.data.notion.export_reasoning.help')}</SettingHelpText>
|
||||
</SettingRow>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.page_name_key')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={notionPageNameKey || ''}
|
||||
onChange={handleNotionPageNameKeyChange}
|
||||
onBlur={handleNotionPageNameKeyChange}
|
||||
placeholder={t('settings.data.notion.page_name_key_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.notion.api_key')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={notionApiKey || ''}
|
||||
onChange={handleNotionTokenChange}
|
||||
onBlur={handleNotionTokenChange}
|
||||
placeholder={t('settings.data.notion.api_key_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button onClick={handleNotionConnectionCheck} variant="outline" className="h-8 shrink-0 rounded-lg">
|
||||
{t('settings.data.notion.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle tip={t('settings.data.notion.export_reasoning.help')}>
|
||||
{t('settings.data.notion.export_reasoning.title')}
|
||||
</SettingRowTitle>
|
||||
<Switch checked={notionExportReasoning} onCheckedChange={handleNotionExportReasoningChange} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const logger = loggerService.withContext('ObsidianSettings')
|
||||
|
||||
@@ -63,34 +63,35 @@ const ObsidianSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup>
|
||||
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.obsidian.default_vault')}</SettingRowTitle>
|
||||
<RowFlex className="gap-1.25">
|
||||
{loading ? (
|
||||
<Spinner text={t('common.loading')} />
|
||||
) : vaults.length > 0 ? (
|
||||
<Select value={defaultObsidianVault || undefined} onValueChange={handleChange}>
|
||||
<SelectTrigger className="w-75 max-w-full">
|
||||
<SelectValue placeholder={t('settings.data.obsidian.default_vault_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vaults.map((vault) => (
|
||||
<SelectItem key={vault.name} value={vault.name}>
|
||||
{vault.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<EmptyState
|
||||
compact
|
||||
preset="no-resource"
|
||||
description={error || t('settings.data.obsidian.default_vault_no_vaults')}
|
||||
/>
|
||||
)}
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.obsidian.default_vault')}</SettingRowTitle>
|
||||
<RowFlex className="min-w-0 max-w-75 flex-1 justify-end gap-1.25">
|
||||
{loading ? (
|
||||
<Spinner text={t('common.loading')} />
|
||||
) : vaults.length > 0 ? (
|
||||
<Select value={defaultObsidianVault || undefined} onValueChange={handleChange}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder={t('settings.data.obsidian.default_vault_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{vaults.map((vault) => (
|
||||
<SelectItem key={vault.name} value={vault.name}>
|
||||
{vault.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<EmptyState
|
||||
compact
|
||||
preset="no-resource"
|
||||
description={error || t('settings.data.obsidian.default_vault_no_vaults')}
|
||||
/>
|
||||
)}
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useTheme } from '@renderer/hooks/useTheme'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const logger = loggerService.withContext('SiyuanSettings')
|
||||
|
||||
@@ -74,69 +74,67 @@ const SiyuanSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.siyuan.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.siyuan.api_url')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={siyuanApiUrl || ''}
|
||||
onChange={handleApiUrlChange}
|
||||
placeholder={t('settings.data.siyuan.api_url_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.siyuan.token.label')}</span>
|
||||
<InfoTooltip
|
||||
content={t('settings.data.siyuan.token.help')}
|
||||
placement="left"
|
||||
iconProps={{ className: 'text-text-2 cursor-pointer ml-1' }}
|
||||
onClick={handleSiyuanHelpClick}
|
||||
/>
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.siyuan.api_url')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={siyuanToken || ''}
|
||||
onChange={handleTokenChange}
|
||||
onBlur={handleTokenChange}
|
||||
placeholder={t('settings.data.siyuan.token_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
type="text"
|
||||
value={siyuanApiUrl || ''}
|
||||
onChange={handleApiUrlChange}
|
||||
placeholder={t('settings.data.siyuan.api_url_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleCheckConnection} variant="outline" className="h-9 shrink-0">
|
||||
{t('settings.data.siyuan.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.siyuan.box_id')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={siyuanBoxId || ''}
|
||||
onChange={handleBoxIdChange}
|
||||
placeholder={t('settings.data.siyuan.box_id_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.siyuan.root_path')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={siyuanRootPath || ''}
|
||||
onChange={handleRootPathChange}
|
||||
placeholder={t('settings.data.siyuan.root_path_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.siyuan.token.label')}</span>
|
||||
<InfoTooltip
|
||||
content={t('settings.data.siyuan.token.help')}
|
||||
placement="left"
|
||||
iconProps={{ className: 'text-text-2 cursor-pointer ml-1' }}
|
||||
onClick={handleSiyuanHelpClick}
|
||||
/>
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={siyuanToken || ''}
|
||||
onChange={handleTokenChange}
|
||||
onBlur={handleTokenChange}
|
||||
placeholder={t('settings.data.siyuan.token_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button onClick={handleCheckConnection} variant="outline" className="h-8 shrink-0 rounded-lg">
|
||||
{t('settings.data.siyuan.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.siyuan.box_id')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={siyuanBoxId || ''}
|
||||
onChange={handleBoxIdChange}
|
||||
placeholder={t('settings.data.siyuan.box_id_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.siyuan.root_path')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={siyuanRootPath || ''}
|
||||
onChange={handleRootPathChange}
|
||||
placeholder={t('settings.data.siyuan.root_path_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { formatErrorMessage } from '@renderer/utils/error'
|
||||
import type { FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const logger = loggerService.withContext('YuqueSettings')
|
||||
|
||||
@@ -84,47 +84,47 @@ const YuqueSettings: FC = () => {
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.data.yuque.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.yuque.repo_url')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="text"
|
||||
value={yuqueUrl || ''}
|
||||
onChange={handleYuqueRepoUrlChange}
|
||||
placeholder={t('settings.data.yuque.repo_url_placeholder')}
|
||||
/>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.data.yuque.token')}
|
||||
<InfoTooltip
|
||||
content={t('settings.data.yuque.help')}
|
||||
placement="left"
|
||||
iconProps={{
|
||||
className: 'text-text-2 cursor-pointer ml-1'
|
||||
}}
|
||||
onClick={handleYuqueHelpClick}
|
||||
/>
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<SettingCard>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.yuque.repo_url')}</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={yuqueToken || ''}
|
||||
onChange={handleYuqueTokenChange}
|
||||
onBlur={handleYuqueTokenChange}
|
||||
placeholder={t('settings.data.yuque.token_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
type="text"
|
||||
value={yuqueUrl || ''}
|
||||
onChange={handleYuqueRepoUrlChange}
|
||||
placeholder={t('settings.data.yuque.repo_url_placeholder')}
|
||||
/>
|
||||
<Button onClick={handleYuqueConnectionCheck} variant="outline" className="h-9 shrink-0">
|
||||
{t('settings.data.yuque.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingRow>
|
||||
<SettingRow>
|
||||
<SettingRowTitle>
|
||||
{t('settings.data.yuque.token')}
|
||||
<InfoTooltip
|
||||
content={t('settings.data.yuque.help')}
|
||||
placement="left"
|
||||
iconProps={{
|
||||
className: 'text-text-2 cursor-pointer ml-1'
|
||||
}}
|
||||
onClick={handleYuqueHelpClick}
|
||||
/>
|
||||
</SettingRowTitle>
|
||||
<RowFlex className="w-78.75 min-w-0 max-w-full items-center gap-1.25">
|
||||
<RowFlex className="w-full min-w-0 items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={yuqueToken || ''}
|
||||
onChange={handleYuqueTokenChange}
|
||||
onBlur={handleYuqueTokenChange}
|
||||
placeholder={t('settings.data.yuque.token_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button onClick={handleYuqueConnectionCheck} variant="outline" className="h-8 shrink-0 rounded-lg">
|
||||
{t('settings.data.yuque.check.button')}
|
||||
</Button>
|
||||
</RowFlex>
|
||||
</RowFlex>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Avatar, AvatarFallback, Button, InfoTooltip, PageSidePanel, Tooltip } from '@cherrystudio/ui'
|
||||
import { Avatar, AvatarFallback, Button, PageSidePanel, Tooltip } from '@cherrystudio/ui'
|
||||
import { resolveIcon } from '@cherrystudio/ui/icons'
|
||||
import { usePreference } from '@data/hooks/usePreference'
|
||||
import { loggerService } from '@logger'
|
||||
@@ -13,20 +13,18 @@ import { TRANSLATE_PROMPT } from '@shared/ai/prompts'
|
||||
import { type Model, parseUniqueModelId } from '@shared/data/types/model'
|
||||
import type { Provider } from '@shared/data/types/provider'
|
||||
import { isNonChatModel } from '@shared/utils/model'
|
||||
import { ChevronDown, Languages, MessageSquareMore, Rocket, RotateCcw, Settings2 } from 'lucide-react'
|
||||
import { ChevronDown, Languages, MessageSquareMore, Package, Rocket, RotateCcw, Settings2 } from 'lucide-react'
|
||||
import type { FC, ReactNode } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
SettingContainer,
|
||||
SettingDescription,
|
||||
SettingDivider,
|
||||
SettingGroup,
|
||||
SettingRow,
|
||||
SettingRowTitle,
|
||||
SettingsContentColumn,
|
||||
SettingTitle
|
||||
SettingsPageHeader
|
||||
} from '..'
|
||||
import { TopicNamingSettings } from './QuickModelPopup'
|
||||
|
||||
@@ -41,21 +39,21 @@ interface ModelSettingsProps {
|
||||
interface ModelSettingRowProps {
|
||||
icon: ReactNode
|
||||
title: ReactNode
|
||||
description?: ReactNode
|
||||
tip?: ReactNode
|
||||
compact?: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const ModelSettingRow: FC<ModelSettingRowProps> = ({ icon, title, description, compact, children }) => (
|
||||
<SettingRow className={cn(compact ? 'flex-col items-stretch gap-3 py-1' : 'items-start gap-6 py-1.5')}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<SettingRowTitle className="gap-2 font-semibold">
|
||||
{icon}
|
||||
{title}
|
||||
</SettingRowTitle>
|
||||
{description && <SettingDescription className="mt-1.5 leading-5">{description}</SettingDescription>}
|
||||
</div>
|
||||
<div className={compact ? 'flex w-full items-center gap-2' : 'flex w-[340px] shrink-0 items-center gap-2'}>
|
||||
const ModelSettingRow: FC<ModelSettingRowProps> = ({ icon, title, tip, compact, children }) => (
|
||||
<SettingRow className={cn('rounded-xl border border-border/60 px-4 py-3', compact && 'flex-col items-stretch gap-3')}>
|
||||
<SettingRowTitle
|
||||
tip={tip}
|
||||
className="text-(length:--font-size-body-sm) min-w-0 flex-1 gap-2 leading-(--line-height-body-sm) [&_svg]:shrink-0">
|
||||
{icon}
|
||||
<span className="min-w-0 truncate">{title}</span>
|
||||
</SettingRowTitle>
|
||||
<div
|
||||
className={compact ? 'flex w-full items-center gap-2' : 'flex min-w-0 max-w-[340px] flex-1 items-center gap-2'}>
|
||||
{children}
|
||||
</div>
|
||||
</SettingRow>
|
||||
@@ -93,9 +91,12 @@ const renderModelSelectorTrigger = ({ model, providers, placeholder, compact }:
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size={compact ? 'lg' : 'default'}
|
||||
className={cn('min-w-0 flex-1 justify-between px-2.5 text-left font-normal', compact ? 'h-9' : 'h-7.5')}>
|
||||
className={cn(
|
||||
'min-w-0 flex-1 justify-between rounded-lg bg-muted/50 px-2.5 text-left font-normal hover:bg-muted data-[state=open]:bg-muted',
|
||||
compact ? 'h-8' : 'h-7'
|
||||
)}>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||
{model && icon ? (
|
||||
<icon.Avatar size={20} />
|
||||
@@ -107,7 +108,7 @@ const renderModelSelectorTrigger = ({ model, providers, placeholder, compact }:
|
||||
<span className="min-w-0 flex-1 truncate">{model?.name ?? placeholder}</span>
|
||||
{providerName && <span className="max-w-[32%] truncate text-muted-foreground text-xs">{providerName}</span>}
|
||||
</span>
|
||||
<ChevronDown size={14} className="shrink-0 text-muted-foreground" />
|
||||
<ChevronDown size={14} className="lucide-custom shrink-0 text-muted-foreground/40" />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -190,89 +191,110 @@ const ModelSettings: FC<ModelSettingsProps> = ({
|
||||
<ContainerComponent theme={theme} {...containerProps}>
|
||||
<SettingGroup theme={theme} style={groupStyle}>
|
||||
{!compact && (
|
||||
<>
|
||||
<SettingTitle>{t('settings.model')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
</>
|
||||
<SettingsPageHeader
|
||||
icon={<Package />}
|
||||
title={t('settings.model')}
|
||||
description={t('settings.models.page_description')}
|
||||
/>
|
||||
)}
|
||||
<ModelSettingRow
|
||||
compact={compact}
|
||||
icon={<MessageSquareMore size={16} className="lucide-custom shrink-0 text-foreground" />}
|
||||
title={t('settings.models.default_assistant_model')}
|
||||
description={showDescription ? t('settings.models.default_assistant_model_description') : undefined}>
|
||||
<DefaultModelSelector
|
||||
model={defaultModel}
|
||||
providers={providers}
|
||||
filter={modelFilter}
|
||||
<div className="mt-4 space-y-4">
|
||||
<ModelSettingRow
|
||||
compact={compact}
|
||||
onSelect={onSelectDefault}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
</ModelSettingRow>
|
||||
<SettingDivider />
|
||||
<ModelSettingRow
|
||||
compact={compact}
|
||||
icon={<Rocket size={16} className="lucide-custom shrink-0 text-foreground" />}
|
||||
title={
|
||||
<>
|
||||
{t('settings.models.quick_model.label')}
|
||||
<InfoTooltip content={t('settings.models.quick_model.tooltip')} />
|
||||
</>
|
||||
}
|
||||
description={showDescription ? t('settings.models.quick_model.description') : undefined}>
|
||||
<DefaultModelSelector
|
||||
model={quickModel}
|
||||
providers={providers}
|
||||
filter={modelFilter}
|
||||
icon={
|
||||
<MessageSquareMore
|
||||
size={16}
|
||||
className="lucide-custom shrink-0 text-foreground [stroke-width:var(--icon-stroke)]"
|
||||
/>
|
||||
}
|
||||
title={t('settings.models.default_assistant_model')}
|
||||
tip={showDescription ? t('settings.models.default_assistant_model_description') : undefined}>
|
||||
<DefaultModelSelector
|
||||
model={defaultModel}
|
||||
providers={providers}
|
||||
filter={modelFilter}
|
||||
compact={compact}
|
||||
onSelect={onSelectDefault}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
</ModelSettingRow>
|
||||
<ModelSettingRow
|
||||
compact={compact}
|
||||
onSelect={onSelectQuick}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
{showSettingsButton && (
|
||||
<Button
|
||||
aria-label={t('settings.models.quick_model.setting_title')}
|
||||
className="shrink-0"
|
||||
onClick={() => setActivePanel('quick-model')}
|
||||
size="icon-sm"
|
||||
variant="outline">
|
||||
<Settings2 size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</ModelSettingRow>
|
||||
<SettingDivider />
|
||||
<ModelSettingRow
|
||||
compact={compact}
|
||||
icon={<Languages size={16} className="lucide-custom shrink-0 text-foreground" />}
|
||||
title={t('settings.models.translate_model')}
|
||||
description={showDescription ? t('settings.models.translate_model_description') : undefined}>
|
||||
<DefaultModelSelector
|
||||
model={translateModel}
|
||||
providers={providers}
|
||||
filter={modelFilter}
|
||||
compact={compact}
|
||||
onSelect={onSelectTranslate}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
{showSettingsButton && (
|
||||
<>
|
||||
icon={
|
||||
<Rocket
|
||||
size={16}
|
||||
className="lucide-custom shrink-0 text-foreground [stroke-width:var(--icon-stroke)]"
|
||||
/>
|
||||
}
|
||||
title={t('settings.models.quick_model.label')}
|
||||
tip={
|
||||
showDescription ? (
|
||||
<>
|
||||
{t('settings.models.quick_model.description')}
|
||||
<br />
|
||||
{t('settings.models.quick_model.tooltip')}
|
||||
</>
|
||||
) : (
|
||||
t('settings.models.quick_model.tooltip')
|
||||
)
|
||||
}>
|
||||
<DefaultModelSelector
|
||||
model={quickModel}
|
||||
providers={providers}
|
||||
filter={modelFilter}
|
||||
compact={compact}
|
||||
onSelect={onSelectQuick}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
{showSettingsButton && (
|
||||
<Button
|
||||
aria-label={t('settings.translate.title')}
|
||||
className="shrink-0"
|
||||
onClick={() => setActivePanel('translate')}
|
||||
aria-label={t('settings.models.quick_model.setting_title')}
|
||||
className="shrink-0 rounded-lg border-[color:var(--color-border-fg-muted)] hover:bg-(--color-surface-fg-subtle-solid) [&_svg]:size-3.5! [&_svg]:[stroke-width:var(--icon-stroke)]"
|
||||
onClick={() => setActivePanel('quick-model')}
|
||||
size="icon-sm"
|
||||
variant="outline">
|
||||
<Settings2 size={16} />
|
||||
<Settings2 />
|
||||
</Button>
|
||||
{translateModelPrompt !== TRANSLATE_PROMPT && (
|
||||
<Tooltip content={t('common.reset')}>
|
||||
<Button className="shrink-0" onClick={onResetTranslatePrompt} size="icon-sm" variant="outline">
|
||||
<RotateCcw size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModelSettingRow>
|
||||
)}
|
||||
</ModelSettingRow>
|
||||
<ModelSettingRow
|
||||
compact={compact}
|
||||
icon={
|
||||
<Languages
|
||||
size={16}
|
||||
className="lucide-custom shrink-0 text-foreground [stroke-width:var(--icon-stroke)]"
|
||||
/>
|
||||
}
|
||||
title={t('settings.models.translate_model')}
|
||||
tip={showDescription ? t('settings.models.translate_model_description') : undefined}>
|
||||
<DefaultModelSelector
|
||||
model={translateModel}
|
||||
providers={providers}
|
||||
filter={modelFilter}
|
||||
compact={compact}
|
||||
onSelect={onSelectTranslate}
|
||||
placeholder={t('settings.models.empty')}
|
||||
/>
|
||||
{showSettingsButton && (
|
||||
<>
|
||||
<Button
|
||||
aria-label={t('settings.translate.title')}
|
||||
className="shrink-0 rounded-lg border-[color:var(--color-border-fg-muted)] hover:bg-(--color-surface-fg-subtle-solid) [&_svg]:size-3.5! [&_svg]:[stroke-width:var(--icon-stroke)]"
|
||||
onClick={() => setActivePanel('translate')}
|
||||
size="icon-sm"
|
||||
variant="outline">
|
||||
<Settings2 />
|
||||
</Button>
|
||||
{translateModelPrompt !== TRANSLATE_PROMPT && (
|
||||
<Tooltip content={t('common.reset')}>
|
||||
<Button className="shrink-0" onClick={onResetTranslatePrompt} size="icon-sm" variant="outline">
|
||||
<RotateCcw size={16} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</ModelSettingRow>
|
||||
</div>
|
||||
</SettingGroup>
|
||||
</ContainerComponent>
|
||||
{showSettingsButton && (
|
||||
|
||||
@@ -104,10 +104,7 @@ const PopupContainer: React.FC<Props> = ({ resolve }) => {
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(next) => !next && closePopup()}>
|
||||
<DialogContent
|
||||
closeOnOverlayClick={false}
|
||||
className="p-6"
|
||||
onPointerDownOutside={(event) => event.preventDefault()}>
|
||||
<DialogContent className="p-6" onPointerDownOutside={(event) => event.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('settings.models.quick_model.setting_title')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -17,7 +17,7 @@ import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '../..'
|
||||
import { SettingCard, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '../..'
|
||||
import { useWebSearchPersist } from '../hooks/useWebSearchPersist'
|
||||
import { useWebSearchProviderLists } from '../hooks/useWebSearchProviderLists'
|
||||
import CompressionSettings from './CompressionSettings'
|
||||
@@ -25,7 +25,7 @@ import { WebSearchProviderOption } from './WebSearchProviderOption'
|
||||
|
||||
const settingRowClassName = 'items-center justify-between gap-6 py-1'
|
||||
const settingLabelClassName = 'min-w-0 flex-1'
|
||||
const selectTriggerClassName = 'h-8 w-56 text-sm'
|
||||
const selectTriggerClassName = 'h-8 min-w-0 max-w-56 flex-1 text-sm'
|
||||
const DEFAULT_MAX_RESULTS = 5
|
||||
|
||||
const BasicSettings: FC = () => {
|
||||
@@ -96,96 +96,98 @@ const BasicSettings: FC = () => {
|
||||
<>
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.tool.websearch.search_provider')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow className={settingRowClassName}>
|
||||
<SettingRowTitle className={settingLabelClassName}>
|
||||
{t('settings.tool.websearch.default_provider')}
|
||||
</SettingRowTitle>
|
||||
<Select
|
||||
value={defaultProvider?.id}
|
||||
onValueChange={(providerId) =>
|
||||
updateSelectedWebSearchProvider(providerId, setDefaultSearchKeywordsProvider)
|
||||
}>
|
||||
<SelectTrigger size="sm" className={selectTriggerClassName}>
|
||||
<SelectValue placeholder={t('settings.tool.websearch.search_provider_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{keywordProviders.map((provider) => (
|
||||
<SelectItem key={provider.id} value={provider.id}>
|
||||
<WebSearchProviderOption provider={provider} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingRow className={settingRowClassName}>
|
||||
<SettingRowTitle className={settingLabelClassName}>
|
||||
{t('settings.tool.websearch.fetch_urls_provider')}
|
||||
</SettingRowTitle>
|
||||
<Select
|
||||
value={defaultFetchUrlsProvider?.id}
|
||||
onValueChange={(providerId) => updateSelectedWebSearchProvider(providerId, setDefaultFetchUrlsProvider)}>
|
||||
<SelectTrigger size="sm" className={selectTriggerClassName}>
|
||||
<SelectValue placeholder={t('settings.tool.websearch.search_provider_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fetchUrlsProviders.map((provider) => (
|
||||
<SelectItem key={provider.id} value={provider.id}>
|
||||
<WebSearchProviderOption provider={provider} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingCard>
|
||||
<SettingRow className={settingRowClassName}>
|
||||
<SettingRowTitle className={settingLabelClassName}>
|
||||
{t('settings.tool.websearch.default_provider')}
|
||||
</SettingRowTitle>
|
||||
<Select
|
||||
value={defaultProvider?.id}
|
||||
onValueChange={(providerId) =>
|
||||
updateSelectedWebSearchProvider(providerId, setDefaultSearchKeywordsProvider)
|
||||
}>
|
||||
<SelectTrigger size="sm" className={selectTriggerClassName}>
|
||||
<SelectValue placeholder={t('settings.tool.websearch.search_provider_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{keywordProviders.map((provider) => (
|
||||
<SelectItem key={provider.id} value={provider.id}>
|
||||
<WebSearchProviderOption provider={provider} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
<SettingRow className={settingRowClassName}>
|
||||
<SettingRowTitle className={settingLabelClassName}>
|
||||
{t('settings.tool.websearch.fetch_urls_provider')}
|
||||
</SettingRowTitle>
|
||||
<Select
|
||||
value={defaultFetchUrlsProvider?.id}
|
||||
onValueChange={(providerId) => updateSelectedWebSearchProvider(providerId, setDefaultFetchUrlsProvider)}>
|
||||
<SelectTrigger size="sm" className={selectTriggerClassName}>
|
||||
<SelectValue placeholder={t('settings.tool.websearch.search_provider_placeholder')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fetchUrlsProviders.map((provider) => (
|
||||
<SelectItem key={provider.id} value={provider.id}>
|
||||
<WebSearchProviderOption provider={provider} />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
|
||||
<SettingGroup theme={theme} style={{ paddingBottom: 8 }}>
|
||||
<SettingTitle>{t('settings.general.label')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow className={settingRowClassName}>
|
||||
<SettingRowTitle className={settingLabelClassName}>
|
||||
{t('settings.tool.websearch.search_max_result.label')}
|
||||
{maxResults > 20 && compressionConfig?.method === 'none' && (
|
||||
<InfoTooltip
|
||||
content={t('settings.tool.websearch.search_max_result.tooltip')}
|
||||
iconProps={{ size: 16, color: 'var(--color-icon)', className: 'ml-1 cursor-pointer' }}
|
||||
<SettingCard>
|
||||
<SettingRow className={settingRowClassName}>
|
||||
<SettingRowTitle className={settingLabelClassName}>
|
||||
{t('settings.tool.websearch.search_max_result.label')}
|
||||
{maxResults > 20 && compressionConfig?.method === 'none' && (
|
||||
<InfoTooltip
|
||||
content={t('settings.tool.websearch.search_max_result.tooltip')}
|
||||
iconProps={{ size: 16, color: 'var(--color-icon)', className: 'ml-1 cursor-pointer' }}
|
||||
/>
|
||||
)}
|
||||
</SettingRowTitle>
|
||||
<div className="flex min-w-0 max-w-56 flex-1 items-center justify-end gap-2">
|
||||
{!isMaxResultsDefault && (
|
||||
<Tooltip content={t('common.reset')}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-icon hover:text-foreground"
|
||||
aria-label={t('common.reset')}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={resetMaxResults}>
|
||||
<ResetIcon size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Input
|
||||
aria-label={t('settings.tool.websearch.search_max_result.label')}
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={draftMaxResultsInput}
|
||||
className="h-8 w-20 text-center text-sm"
|
||||
onChange={(e) => setDraftMaxResultsInput(e.target.value)}
|
||||
onBlur={commitMaxResultsDraft}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</SettingRowTitle>
|
||||
<div className="flex w-56 shrink-0 items-center justify-end gap-2">
|
||||
{!isMaxResultsDefault && (
|
||||
<Tooltip content={t('common.reset')}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-icon hover:text-foreground"
|
||||
aria-label={t('common.reset')}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={resetMaxResults}>
|
||||
<ResetIcon size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Input
|
||||
aria-label={t('settings.tool.websearch.search_max_result.label')}
|
||||
type="number"
|
||||
min={1}
|
||||
max={100}
|
||||
step={1}
|
||||
value={draftMaxResultsInput}
|
||||
className="h-8 w-20 text-center text-sm"
|
||||
onChange={(e) => setDraftMaxResultsInput(e.target.value)}
|
||||
onBlur={commitMaxResultsDraft}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.currentTarget.blur()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingRow>
|
||||
<CompressionSettings />
|
||||
</div>
|
||||
</SettingRow>
|
||||
<CompressionSettings />
|
||||
</SettingCard>
|
||||
</SettingGroup>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Alert, Button, Textarea } from '@cherrystudio/ui'
|
||||
import { Alert, Button, InfoTooltip, Textarea } from '@cherrystudio/ui'
|
||||
import { useTheme } from '@renderer/hooks/useTheme'
|
||||
import { useWebSearchSettings } from '@renderer/hooks/useWebSearch'
|
||||
import { Info } from 'lucide-react'
|
||||
@@ -6,7 +6,7 @@ import type { FC } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingTitle } from '../..'
|
||||
import { SettingGroup, SettingTitle } from '../..'
|
||||
import { useWebSearchPersist } from '../hooks/useWebSearchPersist'
|
||||
import { parseWebSearchBlacklistInput } from '../utils/webSearchBlacklist'
|
||||
|
||||
@@ -50,44 +50,51 @@ const BlacklistSettings: FC = () => {
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingTitle>{t('settings.tool.websearch.blacklist')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<div className="space-y-2 py-2.5">
|
||||
<div className="flex items-center gap-2 text-foreground-muted text-sm leading-5">
|
||||
<span>{t('settings.tool.websearch.blacklist_description')}</span>
|
||||
<span className="rounded-md bg-muted px-1.5 py-px font-medium text-foreground-muted text-xs leading-tight">
|
||||
{excludeDomains.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Textarea.Input
|
||||
value={blacklistInput}
|
||||
onChange={(e) => setBlacklistInput(e.target.value)}
|
||||
placeholder={t('settings.tool.websearch.blacklist_tooltip')}
|
||||
className="max-h-40 min-h-28 rounded-lg pr-20 text-sm leading-5 shadow-none"
|
||||
rows={4}
|
||||
<SettingTitle>
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
{t('settings.tool.websearch.blacklist')}
|
||||
<InfoTooltip
|
||||
content={t('settings.tool.websearch.blacklist_description')}
|
||||
placement="right"
|
||||
iconProps={{ size: 13, className: 'shrink-0 cursor-pointer text-foreground-muted' }}
|
||||
/>
|
||||
{blacklistDirty && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute right-2 bottom-2 h-7 px-2.5"
|
||||
onClick={() => void updateManualBlacklist(blacklistInput)}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)}
|
||||
</span>
|
||||
<span className="shrink-0 rounded-md bg-muted px-1.5 py-px font-medium text-foreground-muted text-xs leading-tight">
|
||||
{excludeDomains.length}
|
||||
</span>
|
||||
</SettingTitle>
|
||||
<div className="mt-3">
|
||||
<div className="space-y-2">
|
||||
<div className="relative">
|
||||
<Textarea.Input
|
||||
value={blacklistInput}
|
||||
onChange={(e) => setBlacklistInput(e.target.value)}
|
||||
placeholder={t('settings.tool.websearch.blacklist_tooltip')}
|
||||
className="max-h-40 min-h-28 rounded-lg pr-20 text-sm leading-5 shadow-none"
|
||||
rows={4}
|
||||
/>
|
||||
{blacklistDirty && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="absolute right-2 bottom-2 h-7 px-2.5"
|
||||
onClick={() => void updateManualBlacklist(blacklistInput)}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{invalidEntries.length > 0 && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
message={t('settings.tool.websearch.blacklist_invalid_entries', {
|
||||
entries: invalidEntries.join(', ')
|
||||
})}
|
||||
type="error"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{invalidEntries.length > 0 && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
message={t('settings.tool.websearch.blacklist_invalid_entries', {
|
||||
entries: invalidEntries.join(', ')
|
||||
})}
|
||||
type="error"
|
||||
/>
|
||||
)}
|
||||
</SettingGroup>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button, ButtonGroup, Flex, InfoTooltip, Input, Label, Tooltip } from '@cherrystudio/ui'
|
||||
import { Button, Flex, InfoTooltip, Input, Label, Tooltip } from '@cherrystudio/ui'
|
||||
import { useTheme } from '@renderer/hooks/useTheme'
|
||||
import type { WebSearchBasicAuthPatch } from '@renderer/hooks/useWebSearch'
|
||||
import { formatApiKeys, splitApiKeyString, withoutTrailingSlash } from '@renderer/utils/api'
|
||||
@@ -23,7 +23,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import {
|
||||
SettingDivider,
|
||||
SettingCard,
|
||||
SettingGroup,
|
||||
SettingHelpLink,
|
||||
SettingHelpText,
|
||||
@@ -296,7 +296,9 @@ export const WebSearchProviderSetting: FC<Props> = ({
|
||||
<div className="min-w-0">
|
||||
<SettingTitle>
|
||||
<Flex className="min-w-0 items-center gap-2.5">
|
||||
<span className="truncate font-semibold text-[17px] text-foreground">{provider.name}</span>
|
||||
<span className="text-(length:--font-size-body-md) truncate font-semibold text-foreground">
|
||||
{provider.name}
|
||||
</span>
|
||||
{officialWebsite && (
|
||||
<SettingTitleExternalLink href={officialWebsite}>
|
||||
<ExternalLink size={13} />
|
||||
@@ -320,135 +322,146 @@ export const WebSearchProviderSetting: FC<Props> = ({
|
||||
{isDefault ? t('settings.tool.websearch.is_default') : t('settings.tool.websearch.set_as_default')}
|
||||
</Button>
|
||||
</div>
|
||||
<SettingDivider style={{ width: '100%', margin: '8px 0 12px' }} />
|
||||
|
||||
<div className={providerFormClassName}>
|
||||
{usesLlmProviderApiKey && (
|
||||
<div className={providerFieldClassName}>
|
||||
<SettingSubtitle>{t('settings.provider.api_key.label')}</SettingSubtitle>
|
||||
<Button variant="outline" size="sm" className="w-fit" onClick={openLlmProviderSettings}>
|
||||
<ExternalLink size={14} />
|
||||
{t('navigate.provider_settings')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showApiKeySettings && !usesLlmProviderApiKey && (
|
||||
<div className={providerFieldClassName}>
|
||||
<div className={providerFieldHeaderClassName}>
|
||||
<SettingSubtitle>{t('settings.provider.api_key.label')}</SettingSubtitle>
|
||||
<Tooltip content={t('settings.provider.api.key.list.open')} delay={500}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-icon hover:text-foreground"
|
||||
aria-label={t('settings.provider.api.key.list.open')}
|
||||
onClick={openApiKeyList}>
|
||||
<List size={14} />
|
||||
{(usesLlmProviderApiKey || showApiKeySettings || showApiHostSetting || supportsBasicAuth) && (
|
||||
<SettingCard>
|
||||
<div className={providerFormClassName}>
|
||||
{usesLlmProviderApiKey && (
|
||||
<div className={providerFieldClassName}>
|
||||
<SettingSubtitle>{t('settings.provider.api_key.label')}</SettingSubtitle>
|
||||
<Button variant="outline" size="sm" className="w-fit" onClick={openLlmProviderSettings}>
|
||||
<ExternalLink size={14} />
|
||||
{t('navigate.provider_settings')}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<ButtonGroup className="w-full">
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKeysInput}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setApiKeysInput(e.target.value)}
|
||||
onBlur={() => void persist(commitApiKeysDraft, 'Failed to save web search API keys')}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.apiKeys.length === 0}
|
||||
className="min-w-0 flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-9 shrink-0 px-3 shadow-none"
|
||||
disabled={providerCheck.checking}
|
||||
onClick={() => void checkProvider()}>
|
||||
{t('settings.tool.websearch.check')}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow className={providerHelpRowClassName}>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
</SettingHelpTextRow>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showApiKeySettings && !usesLlmProviderApiKey && (
|
||||
<div className={providerFieldClassName}>
|
||||
<div className={providerFieldHeaderClassName}>
|
||||
<SettingSubtitle>{t('settings.provider.api_key.label')}</SettingSubtitle>
|
||||
<Tooltip content={t('settings.provider.api.key.list.open')} delay={500}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="text-foreground/80 hover:text-foreground [&_svg]:[stroke-width:1.6]"
|
||||
aria-label={t('settings.provider.api.key.list.open')}
|
||||
onClick={openApiKeyList}>
|
||||
<List size={14} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex w-full min-w-0 items-center gap-1.25">
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKeysInput}
|
||||
placeholder={t('settings.provider.api_key.label')}
|
||||
onChange={(e) => setApiKeysInput(e.target.value)}
|
||||
onBlur={() => void persist(commitApiKeysDraft, 'Failed to save web search API keys')}
|
||||
spellCheck={false}
|
||||
autoFocus={provider.apiKeys.length === 0}
|
||||
className="min-w-0 flex-1"
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 shrink-0 rounded-lg"
|
||||
disabled={providerCheck.checking}
|
||||
onClick={() => void checkProvider()}>
|
||||
{t('settings.tool.websearch.check')}
|
||||
</Button>
|
||||
</div>
|
||||
{apiKeyWebsite && (
|
||||
<SettingHelpTextRow className={providerHelpRowClassName}>
|
||||
<SettingHelpLink target="_blank" href={apiKeyWebsite}>
|
||||
{t('settings.provider.get_api_key')}
|
||||
</SettingHelpLink>
|
||||
</SettingHelpTextRow>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showApiHostSetting && (
|
||||
<div className={providerFieldClassName}>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<div className="flex w-full min-w-0 items-center gap-1.25">
|
||||
<Input
|
||||
value={apiHostInput}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHostInput(e.target.value)}
|
||||
onBlur={() => void persist(commitApiHostDraft, 'Failed to save web search API host')}
|
||||
className="min-w-0 flex-1"
|
||||
/>
|
||||
{showApiHostCheckButton && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-8 shrink-0 rounded-lg"
|
||||
disabled={providerCheck.checking}
|
||||
onClick={() => void checkProvider()}>
|
||||
{t('settings.tool.websearch.check')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supportsBasicAuth && (
|
||||
<>
|
||||
<SettingSubtitle
|
||||
style={{
|
||||
marginTop: 5,
|
||||
marginBottom: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center'
|
||||
}}>
|
||||
{t('settings.provider.basic_auth.label')}
|
||||
<InfoTooltip
|
||||
placement="right"
|
||||
content={t('settings.provider.basic_auth.tip')}
|
||||
iconProps={{
|
||||
size: 16,
|
||||
color: 'var(--color-icon)',
|
||||
className: 'ml-1 cursor-pointer'
|
||||
}}
|
||||
/>
|
||||
</SettingSubtitle>
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="websearch-basic-auth-username">
|
||||
{t('settings.provider.basic_auth.user_name.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="websearch-basic-auth-username"
|
||||
value={basicAuthUsernameInput}
|
||||
placeholder={t('settings.provider.basic_auth.user_name.tip')}
|
||||
onChange={(e) => setBasicAuthUsernameDraft(e.target.value)}
|
||||
onBlur={() =>
|
||||
void persist(commitBasicAuthDraft, 'Failed to save web search basic auth username')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
{basicAuthUsernameInput && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="websearch-basic-auth-password">
|
||||
{t('settings.provider.basic_auth.password.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="websearch-basic-auth-password"
|
||||
type="password"
|
||||
value={basicAuthPasswordInput}
|
||||
placeholder={t('settings.provider.basic_auth.password.tip')}
|
||||
onChange={(e) => setBasicAuthPasswordInput(e.target.value)}
|
||||
onBlur={() =>
|
||||
void persist(commitBasicAuthDraft, 'Failed to save web search basic auth password')
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showApiHostSetting && (
|
||||
<div className={providerFieldClassName}>
|
||||
<SettingSubtitle>{t('settings.provider.api_host')}</SettingSubtitle>
|
||||
<ButtonGroup className="w-full">
|
||||
<Input
|
||||
value={apiHostInput}
|
||||
placeholder={t('settings.provider.api_host')}
|
||||
onChange={(e) => setApiHostInput(e.target.value)}
|
||||
onBlur={() => void persist(commitApiHostDraft, 'Failed to save web search API host')}
|
||||
className="min-w-0 flex-1"
|
||||
/>
|
||||
{showApiHostCheckButton && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-9 shrink-0 px-3 shadow-none"
|
||||
disabled={providerCheck.checking}
|
||||
onClick={() => void checkProvider()}>
|
||||
{t('settings.tool.websearch.check')}
|
||||
</Button>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{supportsBasicAuth && (
|
||||
<>
|
||||
<SettingDivider style={{ marginTop: 0, marginBottom: 0 }} />
|
||||
<SettingSubtitle
|
||||
style={{ marginTop: 5, marginBottom: 10, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
|
||||
{t('settings.provider.basic_auth.label')}
|
||||
<InfoTooltip
|
||||
placement="right"
|
||||
content={t('settings.provider.basic_auth.tip')}
|
||||
iconProps={{
|
||||
size: 16,
|
||||
color: 'var(--color-icon)',
|
||||
className: 'ml-1 cursor-pointer'
|
||||
}}
|
||||
/>
|
||||
</SettingSubtitle>
|
||||
<div className="flex w-full flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="websearch-basic-auth-username">
|
||||
{t('settings.provider.basic_auth.user_name.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="websearch-basic-auth-username"
|
||||
value={basicAuthUsernameInput}
|
||||
placeholder={t('settings.provider.basic_auth.user_name.tip')}
|
||||
onChange={(e) => setBasicAuthUsernameDraft(e.target.value)}
|
||||
onBlur={() => void persist(commitBasicAuthDraft, 'Failed to save web search basic auth username')}
|
||||
/>
|
||||
</div>
|
||||
{basicAuthUsernameInput && (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Label htmlFor="websearch-basic-auth-password">
|
||||
{t('settings.provider.basic_auth.password.label')}
|
||||
</Label>
|
||||
<Input
|
||||
id="websearch-basic-auth-password"
|
||||
type="password"
|
||||
value={basicAuthPasswordInput}
|
||||
placeholder={t('settings.provider.basic_auth.password.tip')}
|
||||
onChange={(e) => setBasicAuthPasswordInput(e.target.value)}
|
||||
onBlur={() => void persist(commitBasicAuthDraft, 'Failed to save web search basic auth password')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SettingCard>
|
||||
)}
|
||||
</SettingGroup>
|
||||
</SettingsContentColumn>
|
||||
)
|
||||
|
||||
@@ -92,7 +92,7 @@ const WebSearchSettings: FC = () => {
|
||||
labelClassName={settingsSubmenuItemLabelClassName}
|
||||
suffix={
|
||||
isDefault ? (
|
||||
<Badge className="mr-0 ml-auto rounded-full border border-green-500/30 bg-green-500/10 px-2.5 py-0.5 font-medium text-green-600 text-xs dark:text-green-400">
|
||||
<Badge className="mr-0 ml-auto rounded-full border border-success/30 bg-success/10 px-2.5 py-0.5 font-medium text-success text-xs">
|
||||
{t('common.default')}
|
||||
</Badge>
|
||||
) : undefined
|
||||
|
||||
@@ -1,41 +1,299 @@
|
||||
export {
|
||||
SettingContainer,
|
||||
SettingDescription,
|
||||
SettingDivider,
|
||||
SettingGroup,
|
||||
SettingHelpLink,
|
||||
SettingHelpText,
|
||||
SettingHelpTextRow,
|
||||
SettingRow,
|
||||
SettingRowTitle,
|
||||
SettingsContentBody,
|
||||
SettingsContentColumn,
|
||||
SettingSubtitle,
|
||||
SettingTitle,
|
||||
SettingTitleExternalLink
|
||||
} from '@renderer/components/SettingsPrimitives'
|
||||
import { InfoTooltip, NormalTooltip } from '@cherrystudio/ui'
|
||||
import { cn } from '@renderer/utils/style'
|
||||
import type { ThemeMode } from '@shared/data/preference/preferenceTypes'
|
||||
import React from 'react'
|
||||
|
||||
// Flatten a label node to plain text so an overflow tooltip can show the full string
|
||||
// even when the label mixes text with an inline icon (e.g. a trailing InfoTooltip).
|
||||
const getNodeText = (node: React.ReactNode): string => {
|
||||
if (node === null || node === undefined || typeof node === 'boolean') return ''
|
||||
if (typeof node === 'string' || typeof node === 'number') return String(node)
|
||||
if (Array.isArray(node)) return node.map(getNodeText).join('')
|
||||
if (React.isValidElement(node)) return getNodeText((node.props as { children?: React.ReactNode }).children)
|
||||
return ''
|
||||
}
|
||||
|
||||
// Horizontal divider between setting rows — re-exported from the shared Divider primitive.
|
||||
export { Divider as SettingDivider } from '@cherrystudio/ui'
|
||||
|
||||
// Legacy scrollable settings shell with uniform padding — kept for pages that don't use SettingsContentColumn.
|
||||
export const SettingContainer = ({
|
||||
className,
|
||||
theme,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'div'> & { theme?: ThemeMode }) => (
|
||||
<div
|
||||
data-theme-mode={theme}
|
||||
className={cn('flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto p-4 [&::-webkit-scrollbar]:hidden', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
// Canonical settings page container — mirrors the model service (Provider Settings) detail column:
|
||||
// outer px-6 py-4 + inner mx-auto max-w-3xl. Use for "simple right-content" settings pages.
|
||||
// Pages with their own internal split layout (Data / Integration / MCP / WebSearch / FileProcessing / Channels)
|
||||
// keep SettingContainer instead. See DESIGN.md §4 "Settings Page Content Container".
|
||||
export const SettingsContentColumn = ({
|
||||
className,
|
||||
innerClassName,
|
||||
theme,
|
||||
children,
|
||||
...rest
|
||||
}: React.ComponentPropsWithoutRef<'div'> & { theme?: ThemeMode; innerClassName?: string }) => (
|
||||
<div
|
||||
data-theme-mode={theme}
|
||||
className={cn(
|
||||
'flex min-h-0 flex-1 flex-col overflow-y-auto px-6 py-4 pt-3 [&::-webkit-scrollbar]:hidden',
|
||||
className
|
||||
)}
|
||||
{...rest}>
|
||||
<div className={cn('mx-auto w-full max-w-3xl', innerClassName)}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Body variant for pages that handle their own Scrollbar (e.g. CommonSettings, ShortcutSettings).
|
||||
// Renders the same two-layer structure (outer px-6 py-4, inner mx-auto max-w-3xl) without owning the scroll.
|
||||
export const SettingsContentBody = ({
|
||||
className,
|
||||
innerClassName,
|
||||
children,
|
||||
...rest
|
||||
}: React.ComponentPropsWithoutRef<'div'> & { innerClassName?: string }) => (
|
||||
<div className={cn('flex min-h-full w-full flex-col px-6 py-4 pt-3', className)} {...rest}>
|
||||
<div className={cn('mx-auto w-full max-w-3xl', innerClassName)}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
// Group / section title within a page (14px medium — bold-looking but lighter than semibold; a real CJK weight).
|
||||
// Sits above each SettingCard or SettingGroup body.
|
||||
export const SettingTitle = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cn('flex select-none items-center justify-between font-medium text-sm', className)} {...props} />
|
||||
)
|
||||
|
||||
// Canonical 2-column page header: leading icon + 16px semibold title + optional description / action.
|
||||
// Renders an <h1> for accessibility; pages should use this at the top, then SettingTitle for group titles below.
|
||||
export const SettingsPageHeader = ({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
className,
|
||||
...rest
|
||||
}: Omit<React.ComponentPropsWithoutRef<'div'>, 'title'> & {
|
||||
icon?: React.ReactNode
|
||||
title: React.ReactNode
|
||||
description?: React.ReactNode
|
||||
action?: React.ReactNode
|
||||
}) => (
|
||||
<div className={cn('flex items-start justify-between gap-3', className)} {...rest}>
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-foreground">
|
||||
{icon ? <span className="inline-flex shrink-0 [&_svg]:size-5 [&_svg]:text-foreground">{icon}</span> : null}
|
||||
<h1 className="m-0 select-none font-[550] text-lg leading-6">{title}</h1>
|
||||
</div>
|
||||
{description ? <p className="m-0 mt-1.5 text-foreground-muted text-xs">{description}</p> : null}
|
||||
</div>
|
||||
{action}
|
||||
</div>
|
||||
)
|
||||
|
||||
// Subtitle inside a SettingCard for nested subsections (14px medium, foreground).
|
||||
export const SettingSubtitle = ({
|
||||
ref,
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement> & { ref?: React.RefObject<HTMLDivElement | null> }) => (
|
||||
<div ref={ref} className={cn('select-none font-medium text-foreground text-sm', className)} {...props} />
|
||||
)
|
||||
|
||||
// Caption-sized helper text under a SettingRow (muted, 12px).
|
||||
export const SettingDescription = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cn('mt-2.5 text-foreground-muted text-xs', className)} {...props} />
|
||||
)
|
||||
|
||||
// One horizontal setting row: label + control(s), gap-x-4, min-h-8.
|
||||
export const SettingRow = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cn('flex min-h-8 flex-wrap items-center justify-between gap-x-4 gap-y-2', className)} {...props} />
|
||||
)
|
||||
|
||||
// Row label (13px, normal weight to match span-reset text via the global `* { font-weight: normal }` base rule).
|
||||
// Supports an optional `tip` tooltip rendered as a help-icon next to the text.
|
||||
export const SettingRowTitle = ({
|
||||
className,
|
||||
tip,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'div'> & { tip?: React.ReactNode }) => {
|
||||
const labelRef = React.useRef<HTMLSpanElement>(null)
|
||||
const [isTruncated, setIsTruncated] = React.useState(false)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
// Only plain-string labels get the truncate-span + overflow tooltip. Mixed children
|
||||
// (leading icon + label, or a trailing InfoTooltip) render as direct flex children so the
|
||||
// row's gap spacing is preserved — wrapping them in a single span would collapse the gap.
|
||||
const isPlainText = typeof children === 'string' || typeof children === 'number'
|
||||
const labelText = getNodeText(children)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isPlainText) return
|
||||
const el = labelRef.current
|
||||
if (!el) return
|
||||
const measure = () => setIsTruncated(el.scrollWidth > el.clientWidth + 1)
|
||||
measure()
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [isPlainText, labelText])
|
||||
|
||||
const baseClassName = cn(
|
||||
'text-(length:--font-size-body-xs) flex min-w-0 flex-1 items-center font-normal text-foreground leading-4.5',
|
||||
className
|
||||
)
|
||||
|
||||
const helpIcon = tip ? (
|
||||
<InfoTooltip
|
||||
content={tip}
|
||||
iconProps={{ size: 14, className: 'ml-1.5 shrink-0 cursor-pointer text-foreground-muted' }}
|
||||
/>
|
||||
) : null
|
||||
|
||||
if (!isPlainText) {
|
||||
return (
|
||||
<div className={baseClassName} {...props}>
|
||||
{children}
|
||||
{helpIcon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const canShowTooltip = isTruncated && labelText !== ''
|
||||
|
||||
return (
|
||||
<div className={baseClassName} {...props}>
|
||||
<NormalTooltip
|
||||
content={labelText}
|
||||
side="top"
|
||||
align="start"
|
||||
open={canShowTooltip ? open : false}
|
||||
onOpenChange={setOpen}>
|
||||
<span ref={labelRef} className="min-w-0 truncate">
|
||||
{children}
|
||||
</span>
|
||||
</NormalTooltip>
|
||||
{helpIcon}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Horizontal container for inline help links + help text under a setting field.
|
||||
export const SettingHelpTextRow = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cn('flex items-center py-1.25', className)} {...props} />
|
||||
)
|
||||
|
||||
// Inline hint copy under a field (11px, low-emphasis foreground).
|
||||
// Shows the full text in a tooltip on hover when it is actually truncated.
|
||||
export const SettingHelpText = ({ className, children, ...props }: React.ComponentPropsWithoutRef<'div'>) => {
|
||||
const textRef = React.useRef<HTMLDivElement>(null)
|
||||
const [isTruncated, setIsTruncated] = React.useState(false)
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const text = getNodeText(children)
|
||||
|
||||
React.useEffect(() => {
|
||||
const el = textRef.current
|
||||
if (!el) return
|
||||
const measure = () => setIsTruncated(el.scrollWidth > el.clientWidth + 1)
|
||||
measure()
|
||||
const observer = new ResizeObserver(measure)
|
||||
observer.observe(el)
|
||||
return () => observer.disconnect()
|
||||
}, [text])
|
||||
|
||||
const canShowTooltip = isTruncated && text !== ''
|
||||
|
||||
return (
|
||||
<NormalTooltip content={text} side="top" align="start" open={canShowTooltip ? open : false} onOpenChange={setOpen}>
|
||||
<div
|
||||
ref={textRef}
|
||||
className={cn('text-(length:--font-size-body-xs) min-w-0 truncate text-foreground/40', className)}
|
||||
{...props}>
|
||||
{children}
|
||||
</div>
|
||||
</NormalTooltip>
|
||||
)
|
||||
}
|
||||
|
||||
// Inline help link in caption tier (11px, blue info color).
|
||||
export const SettingHelpLink = ({ className, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a
|
||||
className={cn(
|
||||
'!text-info text-(length:--font-size-body-xs) shrink-0 cursor-pointer whitespace-nowrap hover:underline',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
// External link displayed next to a SettingTitle (inline-flex, blue info color, opens in new tab by default).
|
||||
export const SettingTitleExternalLink = ({
|
||||
className,
|
||||
target = '_blank',
|
||||
rel = 'noreferrer',
|
||||
...props
|
||||
}: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
|
||||
<a
|
||||
target={target}
|
||||
rel={rel}
|
||||
className={cn('!text-info inline-flex items-center hover:underline', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
// Vertical group wrapper around a SettingTitle + body — adds top spacing between consecutive groups.
|
||||
export const SettingGroup = ({
|
||||
className,
|
||||
theme,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<'div'> & { theme?: ThemeMode }) => (
|
||||
<div data-theme-mode={theme} className={cn('mt-6 first:mt-0', className)} {...props} />
|
||||
)
|
||||
|
||||
// Card shell for a group's rows — SettingTitle stays outside, rows go inside.
|
||||
// Direct children get uniform row padding via the `*:` variant.
|
||||
export const SettingCard = ({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) => (
|
||||
<div className={cn('mt-3 rounded-xl border border-border/60 py-1.5 *:px-4 *:py-1.5', className)} {...props} />
|
||||
)
|
||||
|
||||
// Left submenu scroll column — fixed settings width with a hairline right border.
|
||||
export const settingsSubmenuScrollClassName =
|
||||
'h-[calc(100vh-var(--navbar-height))] w-(--settings-width) border-border border-r-[0.5px]'
|
||||
|
||||
export const settingsSubmenuListClassName = 'flex flex-col gap-1 px-2.5 pb-2.5 [box-sizing:border-box]'
|
||||
// Submenu list wrapper — vertical stack of MenuItems with small gaps and side padding.
|
||||
export const settingsSubmenuListClassName = 'flex flex-col gap-0.5 px-2.5 pb-2.5 [box-sizing:border-box]'
|
||||
|
||||
// Submenu MenuItem — idle/hover/active states for a settings nav entry (selected surface + medium weight when active).
|
||||
export const settingsSubmenuItemClassName =
|
||||
'h-8 rounded-[10px] border-transparent px-2.5 font-normal text-foreground text-sm hover:!bg-muted data-[active=true]:!border-transparent data-[active=true]:!bg-muted data-[active=true]:!font-medium data-[active=true]:!text-foreground [&_svg]:size-4 [&_svg]:text-foreground'
|
||||
'h-7.5 gap-2.5 rounded-lg border-transparent px-2.5 font-normal text-foreground/80 text-sm hover:!bg-muted hover:text-foreground data-[active=true]:!border-transparent data-[active=true]:!bg-selected data-[active=true]:!shadow-(--shadow-selected-outline) data-[active=true]:!font-medium data-[active=true]:!text-foreground [&_svg]:size-4 [&_svg]:text-current [&_svg]:[stroke-width:1.6]'
|
||||
|
||||
// Submenu MenuItem label — bumps to medium weight when the item is active.
|
||||
export const settingsSubmenuItemLabelClassName = 'group-data-[active=true]:font-medium'
|
||||
|
||||
// Submenu section heading between groups of nav entries (muted, 12px).
|
||||
export const settingsSubmenuSectionTitleClassName =
|
||||
'px-2.5 pt-1.5 pb-1 font-normal text-foreground-muted text-xs first:pt-0'
|
||||
|
||||
// Submenu group divider — transparent spacer between nav sections.
|
||||
export const settingsSubmenuDividerClassName = 'my-1 bg-transparent'
|
||||
|
||||
// Right content column scroll container — fills remaining width, hides horizontal overflow.
|
||||
export const settingsContentScrollClassName = 'flex-1 min-h-0 min-w-0 overflow-x-hidden'
|
||||
|
||||
// Right content body — same two-layer padding as SettingsContentBody, applied via className.
|
||||
export const settingsContentBodyClassName = 'flex min-h-full w-full flex-col px-6 py-4'
|
||||
|
||||
// Spacing wrapper below a 3-column page's content header.
|
||||
export const settingsContentHeaderClassName = 'mb-5'
|
||||
|
||||
export const settingsContentHeaderTitleClassName = 'font-semibold text-foreground text-[15px]'
|
||||
// 3-column page content-header title (15px, weight 550).
|
||||
export const settingsContentHeaderTitleClassName = 'font-[550] text-foreground text-(length:--font-size-body-md)'
|
||||
|
||||
// 3-column page content-header description (muted, 14px).
|
||||
export const settingsContentHeaderDescriptionClassName = 'mt-1 text-foreground-muted text-sm'
|
||||
|
||||
Reference in New Issue
Block a user