feat: 优化导出obsidian,自动选择库路径,不再需要手动配置 (#3854)
* feat: 优化导出obsidian,自动选择库路径,不再需要手动配置 * fix: eslint报错 * feat: 增加预设置默认仓库 * fix: 解决合并冲突
This commit is contained in:
parent
aee0f9ea3f
commit
917943386e
@ -17,6 +17,7 @@ import FileStorage from './services/FileStorage'
|
||||
import { GeminiService } from './services/GeminiService'
|
||||
import KnowledgeService from './services/KnowledgeService'
|
||||
import MCPService from './services/MCPService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
@ -31,6 +32,7 @@ const fileManager = new FileStorage()
|
||||
const backupManager = new BackupManager()
|
||||
const exportService = new ExportService(fileManager)
|
||||
const mcpService = new MCPService()
|
||||
const obsidianVaultService = new ObsidianVaultService()
|
||||
|
||||
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater(mainWindow)
|
||||
@ -300,6 +302,15 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle('copilot:logout', CopilotService.logout)
|
||||
ipcMain.handle('copilot:get-user', CopilotService.getUser)
|
||||
|
||||
// Obsidian service
|
||||
ipcMain.handle('obsidian:get-vaults', () => {
|
||||
return obsidianVaultService.getVaults()
|
||||
})
|
||||
|
||||
ipcMain.handle('obsidian:get-files', (_event, vaultName) => {
|
||||
return obsidianVaultService.getFilesByVaultName(vaultName)
|
||||
})
|
||||
|
||||
// nutstore
|
||||
ipcMain.handle('nutstore:get-sso-url', NutstoreService.getNutstoreSSOUrl)
|
||||
ipcMain.handle('nutstore:decrypt-token', (_, token: string) => NutstoreService.decryptToken(token))
|
||||
|
||||
167
src/main/services/ObsidianVaultService.ts
Normal file
167
src/main/services/ObsidianVaultService.ts
Normal file
@ -0,0 +1,167 @@
|
||||
import { app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
interface VaultInfo {
|
||||
path: string
|
||||
name: string
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string
|
||||
type: 'folder' | 'markdown'
|
||||
name: string
|
||||
}
|
||||
|
||||
class ObsidianVaultService {
|
||||
private obsidianConfigPath: string
|
||||
|
||||
constructor() {
|
||||
// 根据操作系统获取Obsidian配置文件路径
|
||||
if (process.platform === 'win32') {
|
||||
this.obsidianConfigPath = path.join(app.getPath('appData'), 'obsidian', 'obsidian.json')
|
||||
} else if (process.platform === 'darwin') {
|
||||
this.obsidianConfigPath = path.join(
|
||||
app.getPath('home'),
|
||||
'Library',
|
||||
'Application Support',
|
||||
'obsidian',
|
||||
'obsidian.json'
|
||||
)
|
||||
} else {
|
||||
// Linux
|
||||
this.obsidianConfigPath = path.join(app.getPath('home'), '.config', 'obsidian', 'obsidian.json')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有的Obsidian Vault
|
||||
*/
|
||||
getVaults(): VaultInfo[] {
|
||||
try {
|
||||
if (!fs.existsSync(this.obsidianConfigPath)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const configContent = fs.readFileSync(this.obsidianConfigPath, 'utf8')
|
||||
const config = JSON.parse(configContent)
|
||||
|
||||
if (!config.vaults) {
|
||||
return []
|
||||
}
|
||||
|
||||
return Object.entries(config.vaults).map(([, vault]: [string, any]) => ({
|
||||
path: vault.path,
|
||||
name: vault.name || path.basename(vault.path)
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian Vault失败:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Vault中的文件夹和Markdown文件结构
|
||||
*/
|
||||
getVaultStructure(vaultPath: string): FileInfo[] {
|
||||
const results: FileInfo[] = []
|
||||
|
||||
try {
|
||||
// 检查vault路径是否存在
|
||||
if (!fs.existsSync(vaultPath)) {
|
||||
console.error('Vault路径不存在:', vaultPath)
|
||||
return []
|
||||
}
|
||||
|
||||
// 检查是否是目录
|
||||
const stats = fs.statSync(vaultPath)
|
||||
if (!stats.isDirectory()) {
|
||||
console.error('Vault路径不是一个目录:', vaultPath)
|
||||
return []
|
||||
}
|
||||
|
||||
this.traverseDirectory(vaultPath, '', results)
|
||||
} catch (error) {
|
||||
console.error('读取Vault文件夹结构失败:', error)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历目录获取所有文件夹和Markdown文件
|
||||
*/
|
||||
private traverseDirectory(dirPath: string, relativePath: string, results: FileInfo[]) {
|
||||
try {
|
||||
// 首先添加当前文件夹
|
||||
if (relativePath) {
|
||||
results.push({
|
||||
path: relativePath,
|
||||
type: 'folder',
|
||||
name: path.basename(relativePath)
|
||||
})
|
||||
}
|
||||
|
||||
// 确保目录存在且可访问
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
console.error('目录不存在:', dirPath)
|
||||
return
|
||||
}
|
||||
|
||||
let items
|
||||
try {
|
||||
items = fs.readdirSync(dirPath, { withFileTypes: true })
|
||||
} catch (err) {
|
||||
console.error(`无法读取目录 ${dirPath}:`, err)
|
||||
return
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
// 忽略以.开头的隐藏文件夹和文件
|
||||
if (item.name.startsWith('.')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const newRelativePath = relativePath ? `${relativePath}/${item.name}` : item.name
|
||||
const fullPath = path.join(dirPath, item.name)
|
||||
|
||||
if (item.isDirectory()) {
|
||||
this.traverseDirectory(fullPath, newRelativePath, results)
|
||||
} else if (item.isFile() && item.name.endsWith('.md')) {
|
||||
// 收集.md文件
|
||||
results.push({
|
||||
path: newRelativePath,
|
||||
type: 'markdown',
|
||||
name: item.name
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`遍历目录出错 ${dirPath}:`, error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定Vault的文件夹和Markdown文件结构
|
||||
* @param vaultName vault名称
|
||||
*/
|
||||
getFilesByVaultName(vaultName: string): FileInfo[] {
|
||||
try {
|
||||
const vaults = this.getVaults()
|
||||
const vault = vaults.find((v) => v.name === vaultName)
|
||||
|
||||
if (!vault) {
|
||||
console.error('未找到指定名称的Vault:', vaultName)
|
||||
return []
|
||||
}
|
||||
|
||||
console.log('获取Vault文件结构:', vault.name, vault.path)
|
||||
return this.getVaultStructure(vault.path)
|
||||
} catch (error) {
|
||||
console.error('获取Vault文件结构时发生错误:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ObsidianVaultService
|
||||
@ -174,6 +174,11 @@ if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld('electron', electronAPI)
|
||||
contextBridge.exposeInMainWorld('api', api)
|
||||
contextBridge.exposeInMainWorld('obsidian', {
|
||||
getVaults: () => ipcRenderer.invoke('obsidian:get-vaults'),
|
||||
getFolders: (vaultName: string) => ipcRenderer.invoke('obsidian:get-files', vaultName),
|
||||
getFiles: (vaultName: string) => ipcRenderer.invoke('obsidian:get-files', vaultName)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
@ -1,36 +1,223 @@
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||
import { Form, Input, Modal, Select } from 'antd'
|
||||
import React, { useState } from 'react'
|
||||
import { Alert, Empty, Form, Input, Modal, Select, Spin, TreeSelect } from 'antd'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
interface ObsidianExportDialogProps {
|
||||
title: string
|
||||
markdown: string
|
||||
open: boolean // 使用 open 属性替代 visible
|
||||
open: boolean
|
||||
onClose: (success: boolean) => void
|
||||
obsidianTags: string | null
|
||||
processingMethod: string | '3' //默认新增(存在就覆盖)
|
||||
}
|
||||
|
||||
interface FileInfo {
|
||||
path: string
|
||||
type: 'folder' | 'markdown'
|
||||
name: string
|
||||
}
|
||||
|
||||
// 转换文件信息数组为树形结构
|
||||
const convertToTreeData = (files: FileInfo[]) => {
|
||||
const treeData: any[] = [
|
||||
{
|
||||
title: i18n.t('chat.topics.export.obsidian_root_directory'),
|
||||
value: '',
|
||||
isLeaf: false,
|
||||
selectable: true
|
||||
}
|
||||
]
|
||||
|
||||
// 记录已创建的节点路径
|
||||
const pathMap: Record<string, any> = {
|
||||
'': treeData[0]
|
||||
}
|
||||
|
||||
// 先按类型分组,确保先处理文件夹
|
||||
const folders = files.filter((file) => file.type === 'folder')
|
||||
const mdFiles = files.filter((file) => file.type === 'markdown')
|
||||
|
||||
// 按路径排序,确保父文件夹先被创建
|
||||
const sortedFolders = [...folders].sort((a, b) => a.path.split('/').length - b.path.split('/').length)
|
||||
|
||||
// 先处理所有文件夹,构建目录结构
|
||||
for (const folder of sortedFolders) {
|
||||
const parts = folder.path.split('/')
|
||||
let currentPath = ''
|
||||
let parentPath = ''
|
||||
|
||||
// 遍历文件夹路径的每一部分,确保创建完整路径
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i]
|
||||
|
||||
// 构建当前路径
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part
|
||||
|
||||
// 如果这个路径节点还没创建
|
||||
if (!pathMap[currentPath]) {
|
||||
const node = {
|
||||
title: part,
|
||||
value: currentPath,
|
||||
key: currentPath,
|
||||
isLeaf: false,
|
||||
selectable: true,
|
||||
children: []
|
||||
}
|
||||
|
||||
// 获取父节点,将当前节点添加到父节点的children中
|
||||
const parentNode = pathMap[parentPath]
|
||||
if (parentNode) {
|
||||
if (!parentNode.children) {
|
||||
parentNode.children = []
|
||||
}
|
||||
parentNode.children.push(node)
|
||||
}
|
||||
|
||||
pathMap[currentPath] = node
|
||||
}
|
||||
|
||||
// 更新父路径为当前路径,为下一级做准备
|
||||
parentPath = currentPath
|
||||
}
|
||||
}
|
||||
|
||||
// 然后处理md文件
|
||||
for (const file of mdFiles) {
|
||||
const fullPath = file.path
|
||||
const dirPath = fullPath.substring(0, fullPath.lastIndexOf('/'))
|
||||
const fileName = file.name
|
||||
|
||||
// 获取父文件夹节点
|
||||
const parentNode = pathMap[dirPath] || pathMap['']
|
||||
|
||||
// 创建文件节点
|
||||
const fileNode = {
|
||||
title: fileName,
|
||||
value: fullPath,
|
||||
isLeaf: true,
|
||||
selectable: true,
|
||||
icon: <span style={{ marginRight: 4 }}>📄</span>
|
||||
}
|
||||
|
||||
// 添加到父节点
|
||||
if (!parentNode.children) {
|
||||
parentNode.children = []
|
||||
}
|
||||
parentNode.children.push(fileNode)
|
||||
}
|
||||
|
||||
return treeData
|
||||
}
|
||||
|
||||
const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
title,
|
||||
markdown,
|
||||
obsidianTags,
|
||||
processingMethod,
|
||||
open,
|
||||
onClose
|
||||
onClose,
|
||||
obsidianTags,
|
||||
processingMethod
|
||||
}) => {
|
||||
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
|
||||
const [state, setState] = useState({
|
||||
title: title,
|
||||
title,
|
||||
tags: obsidianTags || '',
|
||||
createdAt: new Date().toISOString().split('T')[0],
|
||||
source: 'Cherry Studio',
|
||||
processingMethod: processingMethod
|
||||
processingMethod: processingMethod,
|
||||
folder: ''
|
||||
})
|
||||
|
||||
const [vaults, setVaults] = useState<Array<{ path: string; name: string }>>([])
|
||||
const [files, setFiles] = useState<FileInfo[]>([])
|
||||
const [fileTreeData, setFileTreeData] = useState<any[]>([])
|
||||
const [selectedVault, setSelectedVault] = useState<string>('')
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
// 处理文件数据转为树形结构
|
||||
useEffect(() => {
|
||||
if (files.length > 0) {
|
||||
const treeData = convertToTreeData(files)
|
||||
setFileTreeData(treeData)
|
||||
} else {
|
||||
setFileTreeData([
|
||||
{
|
||||
title: i18n.t('chat.topics.export.obsidian_root_directory'),
|
||||
value: '',
|
||||
isLeaf: false,
|
||||
selectable: true
|
||||
}
|
||||
])
|
||||
}
|
||||
}, [files])
|
||||
|
||||
// 组件加载时获取Vault列表
|
||||
useEffect(() => {
|
||||
const fetchVaults = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const vaultsData = await window.obsidian.getVaults()
|
||||
|
||||
if (vaultsData.length === 0) {
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vaults'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
setVaults(vaultsData)
|
||||
|
||||
// 如果没有选择的vault,使用默认值或第一个
|
||||
const vaultToUse = defaultObsidianVault || vaultsData[0]?.name
|
||||
if (vaultToUse) {
|
||||
setSelectedVault(vaultToUse)
|
||||
|
||||
// 获取选中vault的文件和文件夹
|
||||
const filesData = await window.obsidian.getFiles(vaultToUse)
|
||||
setFiles(filesData)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian Vault失败:', error)
|
||||
setError(i18n.t('chat.topics.export.obsidian_fetch_error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchVaults()
|
||||
}, [defaultObsidianVault])
|
||||
|
||||
// 当选择的vault变化时,获取其文件和文件夹
|
||||
useEffect(() => {
|
||||
if (selectedVault) {
|
||||
const fetchFiles = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const filesData = await window.obsidian.getFiles(selectedVault)
|
||||
setFiles(filesData)
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian文件失败:', error)
|
||||
setError(i18n.t('chat.topics.export.obsidian_fetch_folders_error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
fetchFiles()
|
||||
}
|
||||
}, [selectedVault])
|
||||
|
||||
const handleOk = async () => {
|
||||
if (!selectedVault) {
|
||||
setError(i18n.t('chat.topics.export.obsidian_no_vault_selected'))
|
||||
return
|
||||
}
|
||||
|
||||
//构建content 并复制到粘贴板
|
||||
let content = ''
|
||||
if (state.processingMethod !== '3') {
|
||||
@ -45,10 +232,18 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
}
|
||||
if (content === '') {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||
return
|
||||
}
|
||||
|
||||
await navigator.clipboard.writeText(content)
|
||||
markdown = ''
|
||||
exportMarkdownToObsidian(state)
|
||||
|
||||
// 导出到Obsidian
|
||||
exportMarkdownToObsidian({
|
||||
...state,
|
||||
folder: state.folder,
|
||||
vault: selectedVault
|
||||
})
|
||||
|
||||
onClose(true)
|
||||
}
|
||||
|
||||
@ -60,18 +255,56 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
setState((prevState) => ({ ...prevState, [key]: value }))
|
||||
}
|
||||
|
||||
const handleVaultChange = (value: string) => {
|
||||
setSelectedVault(value)
|
||||
// 文件夹会通过useEffect自动获取
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
folder: ''
|
||||
}))
|
||||
}
|
||||
|
||||
// 处理文件选择
|
||||
const handleFileSelect = (value: string) => {
|
||||
// 更新folder值
|
||||
handleChange('folder', value)
|
||||
|
||||
// 检查是否选中md文件
|
||||
if (value) {
|
||||
const selectedFile = files.find((file) => file.path === value)
|
||||
if (selectedFile) {
|
||||
if (selectedFile.type === 'markdown') {
|
||||
// 如果是md文件,自动设置标题为文件名并设置处理方式为1(追加)
|
||||
const fileName = selectedFile.name
|
||||
const titleWithoutExt = fileName.endsWith('.md') ? fileName.substring(0, fileName.length - 3) : fileName
|
||||
handleChange('title', titleWithoutExt)
|
||||
handleChange('processingMethod', '1')
|
||||
} else {
|
||||
// 如果是文件夹,自动设置标题为话题名并设置处理方式为3(新建)
|
||||
handleChange('processingMethod', '3')
|
||||
handleChange('title', title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||
open={open} // 使用 open 属性
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onCancel={handleCancel}
|
||||
width={600}
|
||||
closable
|
||||
maskClosable
|
||||
centered
|
||||
okButtonProps={{ type: 'primary' }}
|
||||
okButtonProps={{
|
||||
type: 'primary',
|
||||
disabled: vaults.length === 0 || loading || !!error
|
||||
}}
|
||||
okText={i18n.t('chat.topics.export.obsidian_btn')}>
|
||||
{error && <Alert message={error} type="error" showIcon style={{ marginBottom: 16 }} />}
|
||||
|
||||
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
|
||||
<Input
|
||||
@ -80,6 +313,55 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_vault')}>
|
||||
{vaults.length > 0 ? (
|
||||
<Select
|
||||
loading={loading}
|
||||
value={selectedVault}
|
||||
onChange={handleVaultChange}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_vault_placeholder')}
|
||||
style={{ width: '100%' }}>
|
||||
{vaults.map((vault) => (
|
||||
<Option key={vault.name} value={vault.name}>
|
||||
{vault.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Empty
|
||||
description={
|
||||
loading
|
||||
? i18n.t('chat.topics.export.obsidian_loading')
|
||||
: i18n.t('chat.topics.export.obsidian_no_vaults')
|
||||
}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_path')}>
|
||||
<Spin spinning={loading}>
|
||||
{selectedVault ? (
|
||||
<TreeSelect
|
||||
value={state.folder}
|
||||
onChange={handleFileSelect}
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_path_placeholder')}
|
||||
style={{ width: '100%' }}
|
||||
showSearch
|
||||
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
|
||||
treeDefaultExpandAll={false}
|
||||
treeNodeFilterProp="title"
|
||||
treeData={fileTreeData}></TreeSelect>
|
||||
) : (
|
||||
<Empty
|
||||
description={i18n.t('chat.topics.export.obsidian_select_vault_first')}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
|
||||
<Input
|
||||
value={state.tags}
|
||||
@ -101,6 +383,7 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||
placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}>
|
||||
<Select
|
||||
value={state.processingMethod}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
|
||||
import i18n from '@renderer/i18n'
|
||||
import store from '@renderer/store'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
interface ObsidianExportOptions {
|
||||
@ -17,14 +15,6 @@ interface ObsidianExportOptions {
|
||||
* @returns
|
||||
*/
|
||||
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||
const obsidianValut = store.getState().settings.obsidianValut
|
||||
const obsidianFolder = store.getState().settings.obsidianFolder
|
||||
|
||||
if (!obsidianValut || !obsidianFolder) {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||
return false
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
@ -35,12 +25,12 @@ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise
|
||||
document.body.removeChild(div)
|
||||
resolve(success)
|
||||
}
|
||||
const obsidianTags = store.getState().settings.obsidianTages
|
||||
// 不再从store中获取tag配置
|
||||
root.render(
|
||||
<ObsidianExportDialog
|
||||
title={options.title}
|
||||
markdown={options.markdown}
|
||||
obsidianTags={obsidianTags}
|
||||
obsidianTags=""
|
||||
processingMethod={options.processingMethod}
|
||||
open={true}
|
||||
onClose={handleClose}
|
||||
@ -49,8 +39,6 @@ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise
|
||||
})
|
||||
}
|
||||
|
||||
const ObsidianExportPopup = {
|
||||
export default {
|
||||
show: showObsidianExportDialog
|
||||
}
|
||||
|
||||
export default ObsidianExportPopup
|
||||
|
||||
@ -180,13 +180,16 @@
|
||||
"topics.export.md": "Export as markdown",
|
||||
"topics.export.notion": "Export to Notion",
|
||||
"topics.export.obsidian": "Export to Obsidian",
|
||||
"topics.export.obsidian_vault": "Vault",
|
||||
"topics.export.obsidian_vault_placeholder": "Please select the vault name",
|
||||
"topics.export.obsidian_path": "Path",
|
||||
"topics.export.obsidian_path_placeholder": "Please select the path",
|
||||
"topics.export.obsidian_atributes": "Configure Note Attributes",
|
||||
"topics.export.obsidian_btn": "Confirm",
|
||||
"topics.export.obsidian_created": "Creation Time",
|
||||
"topics.export.obsidian_created_placeholder": "Please select the creation time",
|
||||
"topics.export.obsidian_export_failed": "Export failed",
|
||||
"topics.export.obsidian_export_success": "Export success",
|
||||
"topics.export.obsidian_not_configured": "Obsidian not configured",
|
||||
"topics.export.obsidian_operate": "Operation Method",
|
||||
"topics.export.obsidian_operate_append": "Append",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "Create New (Overwrite if it exists)",
|
||||
@ -199,6 +202,13 @@
|
||||
"topics.export.obsidian_title": "Title",
|
||||
"topics.export.obsidian_title_placeholder": "Please enter the title",
|
||||
"topics.export.obsidian_title_required": "The title cannot be empty",
|
||||
"topics.export.obsidian_no_vaults": "No Obsidian vaults found",
|
||||
"topics.export.obsidian_loading": "Loading...",
|
||||
"topics.export.obsidian_fetch_error": "Failed to fetch Obsidian vaults",
|
||||
"topics.export.obsidian_fetch_folders_error": "Failed to fetch folder structure",
|
||||
"topics.export.obsidian_no_vault_selected": "Please select a vault first",
|
||||
"topics.export.obsidian_select_vault_first": "Please select a vault first",
|
||||
"topics.export.obsidian_root_directory": "Root Directory",
|
||||
"topics.export.title": "Export",
|
||||
"topics.export.word": "Export as Word",
|
||||
"topics.export.yuque": "Export to Yuque",
|
||||
@ -796,15 +806,6 @@
|
||||
"notion.split_size_help": "Recommended: 90 for Free plan, 24990 for Plus plan, default is 90",
|
||||
"notion.split_size_placeholder": "Enter block limit per page (default 90)",
|
||||
"notion.title": "Notion Configuration",
|
||||
"obsidian": {
|
||||
"folder": "Folder",
|
||||
"folder_placeholder": "Please enter the folder name",
|
||||
"tags": "Global Tags",
|
||||
"tags_placeholder": "Please enter the tag name, separate multiple tags with commas",
|
||||
"title": "Obsidian Configuration",
|
||||
"vault": "Vault",
|
||||
"vault_placeholder": "Please enter the vault name"
|
||||
},
|
||||
"title": "Data Settings",
|
||||
"webdav": {
|
||||
"autoSync": "Auto Backup",
|
||||
@ -850,6 +851,15 @@
|
||||
"token": "Yuque Token",
|
||||
"token_placeholder": "Please enter the Yuque Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian Configuration",
|
||||
"default_vault": "Default Obsidian Vault",
|
||||
"default_vault_placeholder": "Please select the default Obsidian vault",
|
||||
"default_vault_loading": "Loading Obsidian vault...",
|
||||
"default_vault_no_vaults": "No Obsidian vaults found",
|
||||
"default_vault_fetch_error": "Failed to fetch Obsidian vault",
|
||||
"default_vault_export_failed": "Export failed"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "Siyuan Note Configuration",
|
||||
"api_url": "Siyuan Note API URL",
|
||||
|
||||
@ -180,13 +180,16 @@
|
||||
"topics.export.md": "Markdownとしてエクスポート",
|
||||
"topics.export.notion": "Notion にエクスポート",
|
||||
"topics.export.obsidian": "Obsidian にエクスポート",
|
||||
"topics.export.obsidian_vault": "保管庫",
|
||||
"topics.export.obsidian_vault_placeholder": "保管庫名を選択してください",
|
||||
"topics.export.obsidian_path": "パス",
|
||||
"topics.export.obsidian_path_placeholder": "パスを選択してください",
|
||||
"topics.export.obsidian_atributes": "ノートの属性を設定",
|
||||
"topics.export.obsidian_btn": "確定",
|
||||
"topics.export.obsidian_created": "作成日時",
|
||||
"topics.export.obsidian_created_placeholder": "作成日時を選択してください",
|
||||
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
||||
"topics.export.obsidian_export_success": "エクスポート成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未設定",
|
||||
"topics.export.obsidian_operate": "処理方法",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)",
|
||||
@ -199,6 +202,13 @@
|
||||
"topics.export.obsidian_title": "タイトル",
|
||||
"topics.export.obsidian_title_placeholder": "タイトルを入力してください",
|
||||
"topics.export.obsidian_title_required": "タイトルは空白にできません",
|
||||
"topics.export.obsidian_no_vaults": "Obsidianの保管庫が見つかりません",
|
||||
"topics.export.obsidian_loading": "読み込み中...",
|
||||
"topics.export.obsidian_fetch_error": "Obsidianの保管庫の取得に失敗しました",
|
||||
"topics.export.obsidian_fetch_folders_error": "フォルダ構造の取得に失敗しました",
|
||||
"topics.export.obsidian_no_vault_selected": "保管庫を選択してください",
|
||||
"topics.export.obsidian_select_vault_first": "最初に保管庫を選択してください",
|
||||
"topics.export.obsidian_root_directory": "ルートディレクトリ",
|
||||
"topics.export.title": "エクスポート",
|
||||
"topics.export.word": "Wordとしてエクスポート",
|
||||
"topics.export.yuque": "語雀にエクスポート",
|
||||
@ -796,15 +806,6 @@
|
||||
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
|
||||
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"obsidian": {
|
||||
"folder": "フォルダー",
|
||||
"folder_placeholder": "フォルダーの名前を入力してください",
|
||||
"tags": "グローバルタグ",
|
||||
"tags_placeholder": "タグの名前を入力してください。複数のタグは英語のコンマで区切ってください",
|
||||
"title": "Obsidian の設定",
|
||||
"vault": "ヴォールト(保管庫)",
|
||||
"vault_placeholder": "保管庫の名前を入力してください"
|
||||
},
|
||||
"title": "データ設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動バックアップ",
|
||||
@ -850,6 +851,15 @@
|
||||
"token": "Yuqueトークン",
|
||||
"token_placeholder": "Yuqueトークンを入力してください"
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian 設定",
|
||||
"default_vault": "デフォルトの Obsidian 保管庫",
|
||||
"default_vault_placeholder": "デフォルトの Obsidian 保管庫を選択してください",
|
||||
"default_vault_loading": "Obsidian 保管庫を取得中...",
|
||||
"default_vault_no_vaults": "Obsidian 保管庫が見つかりません",
|
||||
"default_vault_fetch_error": "Obsidian 保管庫の取得に失敗しました",
|
||||
"default_vault_export_failed": "エクスポートに失敗しました"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "思源ノート設定",
|
||||
"api_url": "APIアドレス",
|
||||
|
||||
@ -180,13 +180,16 @@
|
||||
"topics.export.md": "Экспорт как markdown",
|
||||
"topics.export.notion": "Экспорт в Notion",
|
||||
"topics.export.obsidian": "Экспорт в Obsidian",
|
||||
"topics.export.obsidian_vault": "Хранилище",
|
||||
"topics.export.obsidian_vault_placeholder": "Выберите имя хранилища",
|
||||
"topics.export.obsidian_path": "Путь",
|
||||
"topics.export.obsidian_path_placeholder": "Выберите путь",
|
||||
"topics.export.obsidian_atributes": "Настроить атрибуты заметки",
|
||||
"topics.export.obsidian_btn": "Подтвердить",
|
||||
"topics.export.obsidian_created": "Дата создания",
|
||||
"topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания",
|
||||
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
||||
"topics.export.obsidian_export_success": "Экспорт успешно завершен",
|
||||
"topics.export.obsidian_not_configured": "Obsidian не настроен",
|
||||
"topics.export.obsidian_operate": "Метод обработки",
|
||||
"topics.export.obsidian_operate_append": "Добавить в конец",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)",
|
||||
@ -199,6 +202,13 @@
|
||||
"topics.export.obsidian_title": "Заголовок",
|
||||
"topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок",
|
||||
"topics.export.obsidian_title_required": "Заголовок не может быть пустым",
|
||||
"topics.export.obsidian_no_vaults": "Хранилища Obsidian не найдены",
|
||||
"topics.export.obsidian_loading": "Загрузка...",
|
||||
"topics.export.obsidian_fetch_error": "Не удалось получить хранилища Obsidian",
|
||||
"topics.export.obsidian_fetch_folders_error": "Не удалось получить структуру папок",
|
||||
"topics.export.obsidian_no_vault_selected": "Пожалуйста, сначала выберите хранилище",
|
||||
"topics.export.obsidian_select_vault_first": "Пожалуйста, сначала выберите хранилище",
|
||||
"topics.export.obsidian_root_directory": "Корневая директория",
|
||||
"topics.export.title": "Экспорт",
|
||||
"topics.export.word": "Экспорт как Word",
|
||||
"topics.export.yuque": "Экспорт в Yuque",
|
||||
@ -796,15 +806,6 @@
|
||||
"notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90",
|
||||
"notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)",
|
||||
"notion.title": "Настройки Notion",
|
||||
"obsidian": {
|
||||
"folder": "Папка",
|
||||
"folder_placeholder": "Пожалуйста, введите имя папки",
|
||||
"tags": "Глобальные Теги",
|
||||
"tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры.",
|
||||
"title": "Конфигурация Obsidian",
|
||||
"vault": "Хранилище",
|
||||
"vault_placeholder": "Пожалуйста, введите имя хранилища"
|
||||
},
|
||||
"title": "Настройки данных",
|
||||
"webdav": {
|
||||
"autoSync": "Автоматическое резервное копирование",
|
||||
@ -850,6 +851,15 @@
|
||||
"token": "Токен Yuque",
|
||||
"token_placeholder": "Введите токен Yuque"
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Настройки Obsidian",
|
||||
"default_vault": "Хранилище Obsidian по умолчанию",
|
||||
"default_vault_placeholder": "Выберите хранилище Obsidian по умолчанию",
|
||||
"default_vault_loading": "Получение хранилищ Obsidian...",
|
||||
"default_vault_no_vaults": "Хранилища Obsidian не найдены",
|
||||
"default_vault_fetch_error": "Не удалось получить хранилища Obsidian",
|
||||
"default_vault_export_failed": "Ошибка экспорта"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "Конфигурация SiYuan Note",
|
||||
"api_url": "API адрес",
|
||||
|
||||
@ -180,13 +180,16 @@
|
||||
"topics.export.md": "导出为 Markdown",
|
||||
"topics.export.notion": "导出到 Notion",
|
||||
"topics.export.obsidian": "导出到 Obsidian",
|
||||
"topics.export.obsidian_vault": "保管库",
|
||||
"topics.export.obsidian_vault_placeholder": "请选择保管库名称",
|
||||
"topics.export.obsidian_path": "路径",
|
||||
"topics.export.obsidian_path_placeholder": "请选择路径",
|
||||
"topics.export.obsidian_atributes": "配置笔记属性",
|
||||
"topics.export.obsidian_btn": "确定",
|
||||
"topics.export.obsidian_created": "创建时间",
|
||||
"topics.export.obsidian_created_placeholder": "请选择创建时间",
|
||||
"topics.export.obsidian_export_failed": "导出失败",
|
||||
"topics.export.obsidian_export_success": "导出成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_export_failed": "导出到Obsidian失败",
|
||||
"topics.export.obsidian_export_success": "导出到Obsidian成功",
|
||||
"topics.export.obsidian_operate": "处理方式",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)",
|
||||
@ -199,6 +202,13 @@
|
||||
"topics.export.obsidian_title": "标题",
|
||||
"topics.export.obsidian_title_placeholder": "请输入标题",
|
||||
"topics.export.obsidian_title_required": "标题不能为空",
|
||||
"topics.export.obsidian_no_vaults": "未找到Obsidian保管库",
|
||||
"topics.export.obsidian_loading": "加载中...",
|
||||
"topics.export.obsidian_fetch_error": "获取Obsidian保管库失败",
|
||||
"topics.export.obsidian_fetch_folders_error": "获取文件夹结构失败",
|
||||
"topics.export.obsidian_no_vault_selected": "请先选择一个保管库",
|
||||
"topics.export.obsidian_select_vault_first": "请先选择保管库",
|
||||
"topics.export.obsidian_root_directory": "根目录",
|
||||
"topics.export.title": "导出",
|
||||
"topics.export.word": "导出为 Word",
|
||||
"topics.export.yuque": "导出到语雀",
|
||||
@ -796,15 +806,6 @@
|
||||
"notion.split_size_help": "Notion免费版用户建议设置为90,高级版用户建议设置为24990,默认值为90",
|
||||
"notion.split_size_placeholder": "请输入每页块数限制(默认90)",
|
||||
"notion.title": "Notion 配置",
|
||||
"obsidian": {
|
||||
"folder": "文件夹",
|
||||
"folder_placeholder": "请输入文件夹名称",
|
||||
"tags": "全局标签",
|
||||
"tags_placeholder": "请输入标签名称, 多个标签用英文逗号分隔",
|
||||
"title": "Obsidian 配置",
|
||||
"vault": "保管库",
|
||||
"vault_placeholder": "请输入保管库名称"
|
||||
},
|
||||
"title": "数据设置",
|
||||
"webdav": {
|
||||
"autoSync": "自动备份",
|
||||
@ -850,6 +851,15 @@
|
||||
"token": "语雀 Token",
|
||||
"token_placeholder": "请输入语雀Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian 配置",
|
||||
"default_vault": "默认 Obsidian 仓库",
|
||||
"default_vault_placeholder": "请选择默认 Obsidian 仓库",
|
||||
"default_vault_loading": "正在获取 Obsidian 仓库...",
|
||||
"default_vault_no_vaults": "未找到 Obsidian 仓库",
|
||||
"default_vault_fetch_error": "获取 Obsidian 仓库失败",
|
||||
"default_vault_export_failed": "导出失败"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "思源笔记配置",
|
||||
"api_url": "API地址",
|
||||
|
||||
@ -180,13 +180,16 @@
|
||||
"topics.export.md": "匯出為 Markdown",
|
||||
"topics.export.notion": "匯出到 Notion",
|
||||
"topics.export.obsidian": "匯出到 Obsidian",
|
||||
"topics.export.obsidian_vault": "保管庫",
|
||||
"topics.export.obsidian_vault_placeholder": "請選擇保管庫名稱",
|
||||
"topics.export.obsidian_path": "路徑",
|
||||
"topics.export.obsidian_path_placeholder": "請選擇路徑",
|
||||
"topics.export.obsidian_atributes": "配置筆記屬性",
|
||||
"topics.export.obsidian_btn": "確定",
|
||||
"topics.export.obsidian_created": "建立時間",
|
||||
"topics.export.obsidian_created_placeholder": "請選擇建立時間",
|
||||
"topics.export.obsidian_export_failed": "匯出失敗",
|
||||
"topics.export.obsidian_export_success": "匯出成功",
|
||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||
"topics.export.obsidian_operate": "處理方式",
|
||||
"topics.export.obsidian_operate_append": "追加",
|
||||
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)",
|
||||
@ -199,6 +202,13 @@
|
||||
"topics.export.obsidian_title": "標題",
|
||||
"topics.export.obsidian_title_placeholder": "請輸入標題",
|
||||
"topics.export.obsidian_title_required": "標題不能為空",
|
||||
"topics.export.obsidian_no_vaults": "未找到Obsidian保管庫",
|
||||
"topics.export.obsidian_loading": "加載中...",
|
||||
"topics.export.obsidian_fetch_error": "獲取Obsidian保管庫失敗",
|
||||
"topics.export.obsidian_fetch_folders_error": "獲取文件夾結構失敗",
|
||||
"topics.export.obsidian_no_vault_selected": "請先選擇一個保管庫",
|
||||
"topics.export.obsidian_select_vault_first": "請先選擇保管庫",
|
||||
"topics.export.obsidian_root_directory": "根目錄",
|
||||
"topics.export.title": "匯出",
|
||||
"topics.export.word": "匯出為 Word",
|
||||
"topics.export.yuque": "匯出到語雀",
|
||||
@ -796,15 +806,6 @@
|
||||
"notion.split_size_help": "Notion 免費版使用者建議設定為 90,進階版使用者建議設定為 24990,預設值為 90",
|
||||
"notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)",
|
||||
"notion.title": "Notion 設定",
|
||||
"obsidian": {
|
||||
"folder": "資料夾",
|
||||
"folder_placeholder": "請輸入資料夾名稱",
|
||||
"tags": "全域標籤",
|
||||
"tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。",
|
||||
"title": "Obsidian 設定",
|
||||
"vault": "保險庫",
|
||||
"vault_placeholder": "請輸入保險庫名稱"
|
||||
},
|
||||
"title": "資料設定",
|
||||
"webdav": {
|
||||
"autoSync": "自動備份",
|
||||
@ -850,6 +851,15 @@
|
||||
"token": "語雀 Token",
|
||||
"token_placeholder": "請輸入語雀 Token"
|
||||
},
|
||||
"obsidian": {
|
||||
"title": "Obsidian 設定",
|
||||
"default_vault": "預設 Obsidian 倉庫",
|
||||
"default_vault_placeholder": "請選擇預設 Obsidian 倉庫",
|
||||
"default_vault_loading": "正在獲取 Obsidian 倉庫...",
|
||||
"default_vault_no_vaults": "未找到 Obsidian 倉庫",
|
||||
"default_vault_fetch_error": "獲取 Obsidian 倉庫失敗",
|
||||
"default_vault_export_failed": "匯出失敗"
|
||||
},
|
||||
"siyuan": {
|
||||
"title": "思源筆記配置",
|
||||
"api_url": "API地址",
|
||||
|
||||
@ -37,7 +37,7 @@ const DataSettings: FC = () => {
|
||||
const [appInfo, setAppInfo] = useState<AppInfo>()
|
||||
const { size, removeAllFiles } = useKnowledgeFiles()
|
||||
const { theme } = useTheme()
|
||||
const [menu, setMenu] = useState<string>('data')
|
||||
const [menu, setMenu] = useState<string>('common')
|
||||
|
||||
//joplin icon needs to be updated into iconfont
|
||||
const JoplinIcon = () => (
|
||||
@ -77,17 +77,17 @@ const DataSettings: FC = () => {
|
||||
title: 'settings.data.yuque.title',
|
||||
icon: <YuqueOutlined style={{ fontSize: 16 }} />
|
||||
},
|
||||
{
|
||||
key: 'obsidian',
|
||||
title: 'settings.data.obsidian.title',
|
||||
icon: <i className="iconfont icon-obsidian" />
|
||||
},
|
||||
{
|
||||
key: 'joplin',
|
||||
title: 'settings.data.joplin.title',
|
||||
//joplin icon needs to be updated into iconfont
|
||||
icon: <JoplinIcon />
|
||||
},
|
||||
{
|
||||
key: 'obsidian',
|
||||
title: 'settings.data.obsidian.title',
|
||||
icon: <i className="iconfont icon-obsidian" />
|
||||
},
|
||||
{
|
||||
key: 'siyuan',
|
||||
title: 'settings.data.siyuan.title',
|
||||
@ -230,8 +230,8 @@ const DataSettings: FC = () => {
|
||||
{menu === 'markdown_export' && <MarkdownExportSettings />}
|
||||
{menu === 'notion' && <NotionSettings />}
|
||||
{menu === 'yuque' && <YuqueSettings />}
|
||||
{menu === 'obsidian' && <ObsidianSettings />}
|
||||
{menu === 'joplin' && <JoplinSettings />}
|
||||
{menu === 'obsidian' && <ObsidianSettings />}
|
||||
{menu === 'siyuan' && <SiyuanSettings />}
|
||||
</SettingContainer>
|
||||
</Container>
|
||||
|
||||
@ -1,95 +1,90 @@
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import { RootState, useAppDispatch } from '@renderer/store'
|
||||
import { setObsidianFolder, setObsidianTages, setObsidianValut } from '@renderer/store/settings'
|
||||
import Input from 'antd/es/input/Input'
|
||||
import { FC } from 'react'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { setDefaultObsidianVault } from '@renderer/store/settings'
|
||||
import { Empty, Select, Spin } from 'antd'
|
||||
import { FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSelector } from 'react-redux'
|
||||
|
||||
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
|
||||
|
||||
const { Option } = Select
|
||||
|
||||
const ObsidianSettings: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { theme } = useTheme()
|
||||
const { defaultObsidianVault } = useSettings()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
// const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey)
|
||||
// const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl)
|
||||
const [vaults, setVaults] = useState<Array<{ path: string; name: string }>>([])
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const obsidianVault = useSelector((state: RootState) => state.settings.obsidianValut)
|
||||
const obsidianFolder = useSelector((state: RootState) => state.settings.obsidianFolder)
|
||||
const obsidianTags = useSelector((state: RootState) => state.settings.obsidianTages)
|
||||
// 组件加载时获取Vault列表
|
||||
useEffect(() => {
|
||||
const fetchVaults = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const vaultsData = await window.obsidian.getVaults()
|
||||
|
||||
const handleObsidianVaultChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianValut(e.target.value))
|
||||
if (vaultsData.length === 0) {
|
||||
setError(t('settings.data.obsidian.default_vault_no_vaults'))
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
const handleObsidianFolderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianFolder(e.target.value))
|
||||
setVaults(vaultsData)
|
||||
|
||||
// 如果没有设置默认vault,则选择第一个
|
||||
if (!defaultObsidianVault && vaultsData.length > 0) {
|
||||
dispatch(setDefaultObsidianVault(vaultsData[0].name))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取Obsidian Vault失败:', error)
|
||||
setError(t('settings.data.obsidian.default_vault_fetch_error'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleObsidianVaultBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianValut(e.target.value))
|
||||
}
|
||||
fetchVaults()
|
||||
}, [dispatch, defaultObsidianVault, t])
|
||||
|
||||
const handleObsidianFolderBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianFolder(e.target.value))
|
||||
}
|
||||
|
||||
const handleObsidianTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianTages(e.target.value))
|
||||
}
|
||||
|
||||
const handleObsidianTagsBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
dispatch(setObsidianTages(e.target.value))
|
||||
const handleChange = (value: string) => {
|
||||
dispatch(setDefaultObsidianVault(value))
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingGroup theme={theme}>
|
||||
<SettingGroup>
|
||||
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle>{t('settings.data.obsidian.vault')}</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
type="text"
|
||||
value={obsidianVault || ''}
|
||||
onChange={handleObsidianVaultChange}
|
||||
onBlur={handleObsidianVaultBlur}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.obsidian.vault_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.obsidian.folder')}</span>
|
||||
</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
value={obsidianFolder || ''}
|
||||
onChange={handleObsidianFolderChange}
|
||||
onBlur={handleObsidianFolderBlur}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.obsidian.folder_placeholder')}
|
||||
/>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{t('settings.data.obsidian.tags')}</span>
|
||||
</SettingRowTitle>
|
||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||
<Input
|
||||
value={obsidianTags || ''}
|
||||
onChange={handleObsidianTagsChange}
|
||||
onBlur={handleObsidianTagsBlur}
|
||||
style={{ width: 315 }}
|
||||
placeholder={t('settings.data.obsidian.tags_placeholder')}
|
||||
<SettingRowTitle>{t('settings.data.obsidian.default_vault')}</SettingRowTitle>
|
||||
<HStack gap="5px">
|
||||
<Spin spinning={loading} size="small">
|
||||
{vaults.length > 0 ? (
|
||||
<Select
|
||||
value={defaultObsidianVault || undefined}
|
||||
onChange={handleChange}
|
||||
placeholder={t('settings.data.obsidian.default_vault_placeholder')}
|
||||
style={{ width: 300 }}>
|
||||
{vaults.map((vault) => (
|
||||
<Option key={vault.name} value={vault.name}>
|
||||
{vault.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
) : (
|
||||
<Empty
|
||||
description={
|
||||
loading
|
||||
? t('settings.data.obsidian.default_vault_loading')
|
||||
: error || t('settings.data.obsidian.default_vault_no_vaults')
|
||||
}
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</HStack>
|
||||
</SettingRow>
|
||||
</SettingGroup>
|
||||
|
||||
@ -90,12 +90,9 @@ export interface SettingsState {
|
||||
yuqueToken: string | null
|
||||
yuqueUrl: string | null
|
||||
yuqueRepoId: string | null
|
||||
//obsidian settings obsidianVault, obisidanFolder
|
||||
obsidianValut: string | null
|
||||
obsidianFolder: string | null
|
||||
obsidianTages: string | null
|
||||
joplinToken: string | null
|
||||
joplinUrl: string | null
|
||||
defaultObsidianVault: string | null
|
||||
// 思源笔记配置
|
||||
siyuanApiUrl: string | null
|
||||
siyuanToken: string | null
|
||||
@ -172,11 +169,9 @@ const initialState: SettingsState = {
|
||||
yuqueToken: '',
|
||||
yuqueUrl: '',
|
||||
yuqueRepoId: '',
|
||||
obsidianValut: '',
|
||||
obsidianFolder: '',
|
||||
obsidianTages: '',
|
||||
joplinToken: '',
|
||||
joplinUrl: '',
|
||||
defaultObsidianVault: null,
|
||||
// 思源笔记配置初始值
|
||||
siyuanApiUrl: null,
|
||||
siyuanToken: null,
|
||||
@ -386,15 +381,6 @@ const settingsSlice = createSlice({
|
||||
setYuqueUrl: (state, action: PayloadAction<string>) => {
|
||||
state.yuqueUrl = action.payload
|
||||
},
|
||||
setObsidianValut: (state, action: PayloadAction<string>) => {
|
||||
state.obsidianValut = action.payload
|
||||
},
|
||||
setObsidianFolder: (state, action: PayloadAction<string>) => {
|
||||
state.obsidianFolder = action.payload
|
||||
},
|
||||
setObsidianTages: (state, action: PayloadAction<string>) => {
|
||||
state.obsidianTages = action.payload
|
||||
},
|
||||
setJoplinToken: (state, action: PayloadAction<string>) => {
|
||||
state.joplinToken = action.payload
|
||||
},
|
||||
@ -415,6 +401,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
|
||||
state.messageNavigation = action.payload
|
||||
},
|
||||
setDefaultObsidianVault: (state, action: PayloadAction<string>) => {
|
||||
state.defaultObsidianVault = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -484,16 +473,14 @@ export const {
|
||||
setYuqueToken,
|
||||
setYuqueRepoId,
|
||||
setYuqueUrl,
|
||||
setObsidianValut,
|
||||
setObsidianFolder,
|
||||
setObsidianTages,
|
||||
setJoplinToken,
|
||||
setJoplinUrl,
|
||||
setMessageNavigation,
|
||||
setDefaultObsidianVault,
|
||||
setSiyuanApiUrl,
|
||||
setSiyuanToken,
|
||||
setSiyuanBoxId,
|
||||
setSiyuanRootPath,
|
||||
setMessageNavigation
|
||||
setSiyuanRootPath
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
9
src/renderer/src/types/electron.d.ts
vendored
Normal file
9
src/renderer/src/types/electron.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
interface ObsidianAPI {
|
||||
getVaults: () => Promise<Array<{ path: string; name: string }>>
|
||||
getFiles: (vaultName: string) => Promise<Array<{ path: string; type: 'folder' | 'markdown'; name: string }>>
|
||||
getFolders: (vaultName: string) => Promise<Array<{ path: string; type: 'folder' | 'markdown'; name: string }>>
|
||||
}
|
||||
|
||||
interface Window {
|
||||
obsidian: ObsidianAPI
|
||||
}
|
||||
@ -326,31 +326,49 @@ export const exportMarkdownToYuque = async (title: string, content: string) => {
|
||||
* @param attributes.source 来源
|
||||
* @param attributes.tags 标签
|
||||
* @param attributes.processingMethod 处理方式
|
||||
* @param attributes.folder 选择的文件夹路径或文件路径
|
||||
* @param attributes.vault 选择的Vault名称
|
||||
*/
|
||||
export const exportMarkdownToObsidian = async (attributes: any) => {
|
||||
try {
|
||||
const obsidianValut = store.getState().settings.obsidianValut
|
||||
const obsidianFolder = store.getState().settings.obsidianFolder
|
||||
// 从参数获取Vault名称
|
||||
const obsidianValut = attributes.vault
|
||||
let obsidianFolder = attributes.folder || ''
|
||||
let isMarkdownFile = false
|
||||
|
||||
if (!obsidianValut || !obsidianFolder) {
|
||||
if (!obsidianValut) {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||
return
|
||||
}
|
||||
let path = ''
|
||||
|
||||
if (!attributes.title) {
|
||||
window.message.error(i18n.t('chat.topics.export.obsidian_title_required'))
|
||||
return
|
||||
}
|
||||
|
||||
//构建保存路径添加以 / 结尾
|
||||
if (!obsidianFolder.endsWith('/')) {
|
||||
path = obsidianFolder + '/'
|
||||
// 检查是否选择了.md文件
|
||||
if (obsidianFolder && obsidianFolder.endsWith('.md')) {
|
||||
isMarkdownFile = true
|
||||
}
|
||||
|
||||
let filePath = ''
|
||||
|
||||
// 如果是.md文件,直接使用该文件路径
|
||||
if (isMarkdownFile) {
|
||||
filePath = obsidianFolder
|
||||
} else {
|
||||
// 否则构建路径
|
||||
//构建保存路径添加以 / 结尾
|
||||
if (obsidianFolder && !obsidianFolder.endsWith('/')) {
|
||||
obsidianFolder = obsidianFolder + '/'
|
||||
}
|
||||
|
||||
//构建文件名
|
||||
const fileName = transformObsidianFileName(attributes.title)
|
||||
filePath = obsidianFolder + fileName + '.md'
|
||||
}
|
||||
|
||||
let obsidianUrl = `obsidian://new?file=${encodeURIComponent(path + fileName)}&vault=${encodeURIComponent(obsidianValut)}&clipboard`
|
||||
let obsidianUrl = `obsidian://new?file=${encodeURIComponent(filePath)}&vault=${encodeURIComponent(obsidianValut)}&clipboard`
|
||||
|
||||
if (attributes.processingMethod === '3') {
|
||||
obsidianUrl += '&overwrite=true'
|
||||
@ -359,6 +377,7 @@ export const exportMarkdownToObsidian = async (attributes: any) => {
|
||||
} else if (attributes.processingMethod === '1') {
|
||||
obsidianUrl += '&append=true'
|
||||
}
|
||||
|
||||
window.open(obsidianUrl)
|
||||
window.message.success(i18n.t('chat.topics.export.obsidian_export_success'))
|
||||
} catch (error) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user