feat: 优化导出obsidian,自动选择库路径,不再需要手动配置 (#3854)

* feat: 优化导出obsidian,自动选择库路径,不再需要手动配置

* fix: eslint报错

* feat: 增加预设置默认仓库

* fix: 解决合并冲突
This commit is contained in:
africa1207 2025-03-25 21:31:22 +08:00 committed by GitHub
parent aee0f9ea3f
commit 917943386e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 702 additions and 188 deletions

View File

@ -17,6 +17,7 @@ import FileStorage from './services/FileStorage'
import { GeminiService } from './services/GeminiService' import { GeminiService } from './services/GeminiService'
import KnowledgeService from './services/KnowledgeService' import KnowledgeService from './services/KnowledgeService'
import MCPService from './services/MCPService' import MCPService from './services/MCPService'
import ObsidianVaultService from './services/ObsidianVaultService'
import * as NutstoreService from './services/NutstoreService' import * as NutstoreService from './services/NutstoreService'
import { ProxyConfig, proxyManager } from './services/ProxyManager' import { ProxyConfig, proxyManager } from './services/ProxyManager'
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService' import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
@ -31,6 +32,7 @@ const fileManager = new FileStorage()
const backupManager = new BackupManager() const backupManager = new BackupManager()
const exportService = new ExportService(fileManager) const exportService = new ExportService(fileManager)
const mcpService = new MCPService() const mcpService = new MCPService()
const obsidianVaultService = new ObsidianVaultService()
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) { export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const appUpdater = new AppUpdater(mainWindow) 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:logout', CopilotService.logout)
ipcMain.handle('copilot:get-user', CopilotService.getUser) 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 // nutstore
ipcMain.handle('nutstore:get-sso-url', NutstoreService.getNutstoreSSOUrl) ipcMain.handle('nutstore:get-sso-url', NutstoreService.getNutstoreSSOUrl)
ipcMain.handle('nutstore:decrypt-token', (_, token: string) => NutstoreService.decryptToken(token)) ipcMain.handle('nutstore:decrypt-token', (_, token: string) => NutstoreService.decryptToken(token))

View 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

View File

@ -174,6 +174,11 @@ if (process.contextIsolated) {
try { try {
contextBridge.exposeInMainWorld('electron', electronAPI) contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api) 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) { } catch (error) {
console.error(error) console.error(error)
} }

View File

@ -1,36 +1,223 @@
import i18n from '@renderer/i18n' import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { exportMarkdownToObsidian } from '@renderer/utils/export' import { exportMarkdownToObsidian } from '@renderer/utils/export'
import { Form, Input, Modal, Select } from 'antd' import { Alert, Empty, Form, Input, Modal, Select, Spin, TreeSelect } from 'antd'
import React, { useState } from 'react' import React, { useEffect, useState } from 'react'
const { Option } = Select const { Option } = Select
interface ObsidianExportDialogProps { interface ObsidianExportDialogProps {
title: string title: string
markdown: string markdown: string
open: boolean // 使用 open 属性替代 visible open: boolean
onClose: (success: boolean) => void onClose: (success: boolean) => void
obsidianTags: string | null obsidianTags: string | null
processingMethod: string | '3' //默认新增(存在就覆盖) 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> = ({ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
title, title,
markdown, markdown,
obsidianTags,
processingMethod,
open, open,
onClose onClose,
obsidianTags,
processingMethod
}) => { }) => {
const defaultObsidianVault = store.getState().settings.defaultObsidianVault
const [state, setState] = useState({ const [state, setState] = useState({
title: title, title,
tags: obsidianTags || '', tags: obsidianTags || '',
createdAt: new Date().toISOString().split('T')[0], createdAt: new Date().toISOString().split('T')[0],
source: 'Cherry Studio', 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 () => { const handleOk = async () => {
if (!selectedVault) {
setError(i18n.t('chat.topics.export.obsidian_no_vault_selected'))
return
}
//构建content 并复制到粘贴板 //构建content 并复制到粘贴板
let content = '' let content = ''
if (state.processingMethod !== '3') { if (state.processingMethod !== '3') {
@ -45,10 +232,18 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
} }
if (content === '') { if (content === '') {
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed')) window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
return
} }
await navigator.clipboard.writeText(content) await navigator.clipboard.writeText(content)
markdown = ''
exportMarkdownToObsidian(state) // 导出到Obsidian
exportMarkdownToObsidian({
...state,
folder: state.folder,
vault: selectedVault
})
onClose(true) onClose(true)
} }
@ -60,18 +255,56 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
setState((prevState) => ({ ...prevState, [key]: value })) 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 ( return (
<Modal <Modal
title={i18n.t('chat.topics.export.obsidian_atributes')} title={i18n.t('chat.topics.export.obsidian_atributes')}
open={open} // 使用 open 属性 open={open}
onOk={handleOk} onOk={handleOk}
onCancel={handleCancel} onCancel={handleCancel}
width={600} width={600}
closable closable
maskClosable maskClosable
centered centered
okButtonProps={{ type: 'primary' }} okButtonProps={{
type: 'primary',
disabled: vaults.length === 0 || loading || !!error
}}
okText={i18n.t('chat.topics.export.obsidian_btn')}> 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 layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}> <Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
<Input <Input
@ -80,6 +313,55 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')} placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
/> />
</Form.Item> </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')}> <Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
<Input <Input
value={state.tags} value={state.tags}
@ -101,6 +383,7 @@ const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')} placeholder={i18n.t('chat.topics.export.obsidian_source_placeholder')}
/> />
</Form.Item> </Form.Item>
<Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}> <Form.Item label={i18n.t('chat.topics.export.obsidian_operate')}>
<Select <Select
value={state.processingMethod} value={state.processingMethod}

View File

@ -1,6 +1,4 @@
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog' import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
interface ObsidianExportOptions { interface ObsidianExportOptions {
@ -17,14 +15,6 @@ interface ObsidianExportOptions {
* @returns * @returns
*/ */
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => { 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) => { return new Promise<boolean>((resolve) => {
const div = document.createElement('div') const div = document.createElement('div')
document.body.appendChild(div) document.body.appendChild(div)
@ -35,12 +25,12 @@ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise
document.body.removeChild(div) document.body.removeChild(div)
resolve(success) resolve(success)
} }
const obsidianTags = store.getState().settings.obsidianTages // 不再从store中获取tag配置
root.render( root.render(
<ObsidianExportDialog <ObsidianExportDialog
title={options.title} title={options.title}
markdown={options.markdown} markdown={options.markdown}
obsidianTags={obsidianTags} obsidianTags=""
processingMethod={options.processingMethod} processingMethod={options.processingMethod}
open={true} open={true}
onClose={handleClose} onClose={handleClose}
@ -49,8 +39,6 @@ const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise
}) })
} }
const ObsidianExportPopup = { export default {
show: showObsidianExportDialog show: showObsidianExportDialog
} }
export default ObsidianExportPopup

View File

@ -180,13 +180,16 @@
"topics.export.md": "Export as markdown", "topics.export.md": "Export as markdown",
"topics.export.notion": "Export to Notion", "topics.export.notion": "Export to Notion",
"topics.export.obsidian": "Export to Obsidian", "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_atributes": "Configure Note Attributes",
"topics.export.obsidian_btn": "Confirm", "topics.export.obsidian_btn": "Confirm",
"topics.export.obsidian_created": "Creation Time", "topics.export.obsidian_created": "Creation Time",
"topics.export.obsidian_created_placeholder": "Please select the creation time", "topics.export.obsidian_created_placeholder": "Please select the creation time",
"topics.export.obsidian_export_failed": "Export failed", "topics.export.obsidian_export_failed": "Export failed",
"topics.export.obsidian_export_success": "Export success", "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": "Operation Method",
"topics.export.obsidian_operate_append": "Append", "topics.export.obsidian_operate_append": "Append",
"topics.export.obsidian_operate_new_or_overwrite": "Create New (Overwrite if it exists)", "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": "Title",
"topics.export.obsidian_title_placeholder": "Please enter the title", "topics.export.obsidian_title_placeholder": "Please enter the title",
"topics.export.obsidian_title_required": "The title cannot be empty", "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.title": "Export",
"topics.export.word": "Export as Word", "topics.export.word": "Export as Word",
"topics.export.yuque": "Export to Yuque", "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_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.split_size_placeholder": "Enter block limit per page (default 90)",
"notion.title": "Notion Configuration", "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", "title": "Data Settings",
"webdav": { "webdav": {
"autoSync": "Auto Backup", "autoSync": "Auto Backup",
@ -850,6 +851,15 @@
"token": "Yuque Token", "token": "Yuque Token",
"token_placeholder": "Please enter the 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": { "siyuan": {
"title": "Siyuan Note Configuration", "title": "Siyuan Note Configuration",
"api_url": "Siyuan Note API URL", "api_url": "Siyuan Note API URL",

View File

@ -180,13 +180,16 @@
"topics.export.md": "Markdownとしてエクスポート", "topics.export.md": "Markdownとしてエクスポート",
"topics.export.notion": "Notion にエクスポート", "topics.export.notion": "Notion にエクスポート",
"topics.export.obsidian": "Obsidian にエクスポート", "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_atributes": "ノートの属性を設定",
"topics.export.obsidian_btn": "確定", "topics.export.obsidian_btn": "確定",
"topics.export.obsidian_created": "作成日時", "topics.export.obsidian_created": "作成日時",
"topics.export.obsidian_created_placeholder": "作成日時を選択してください", "topics.export.obsidian_created_placeholder": "作成日時を選択してください",
"topics.export.obsidian_export_failed": "エクスポート失敗", "topics.export.obsidian_export_failed": "エクスポート失敗",
"topics.export.obsidian_export_success": "エクスポート成功", "topics.export.obsidian_export_success": "エクスポート成功",
"topics.export.obsidian_not_configured": "Obsidian 未設定",
"topics.export.obsidian_operate": "処理方法", "topics.export.obsidian_operate": "処理方法",
"topics.export.obsidian_operate_append": "追加", "topics.export.obsidian_operate_append": "追加",
"topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)", "topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)",
@ -199,6 +202,13 @@
"topics.export.obsidian_title": "タイトル", "topics.export.obsidian_title": "タイトル",
"topics.export.obsidian_title_placeholder": "タイトルを入力してください", "topics.export.obsidian_title_placeholder": "タイトルを入力してください",
"topics.export.obsidian_title_required": "タイトルは空白にできません", "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.title": "エクスポート",
"topics.export.word": "Wordとしてエクスポート", "topics.export.word": "Wordとしてエクスポート",
"topics.export.yuque": "語雀にエクスポート", "topics.export.yuque": "語雀にエクスポート",
@ -796,15 +806,6 @@
"notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90", "notion.split_size_help": "Notion無料版ユーザーは90、有料版ユーザーは24990、デフォルトは90",
"notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)", "notion.split_size_placeholder": "ページごとのブロック数制限を入力してください(デフォルト90)",
"notion.title": "Notion 設定", "notion.title": "Notion 設定",
"obsidian": {
"folder": "フォルダー",
"folder_placeholder": "フォルダーの名前を入力してください",
"tags": "グローバルタグ",
"tags_placeholder": "タグの名前を入力してください。複数のタグは英語のコンマで区切ってください",
"title": "Obsidian の設定",
"vault": "ヴォールト(保管庫)",
"vault_placeholder": "保管庫の名前を入力してください"
},
"title": "データ設定", "title": "データ設定",
"webdav": { "webdav": {
"autoSync": "自動バックアップ", "autoSync": "自動バックアップ",
@ -850,6 +851,15 @@
"token": "Yuqueトークン", "token": "Yuqueトークン",
"token_placeholder": "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": { "siyuan": {
"title": "思源ノート設定", "title": "思源ノート設定",
"api_url": "APIアドレス", "api_url": "APIアドレス",

View File

@ -180,13 +180,16 @@
"topics.export.md": "Экспорт как markdown", "topics.export.md": "Экспорт как markdown",
"topics.export.notion": "Экспорт в Notion", "topics.export.notion": "Экспорт в Notion",
"topics.export.obsidian": "Экспорт в Obsidian", "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_atributes": "Настроить атрибуты заметки",
"topics.export.obsidian_btn": "Подтвердить", "topics.export.obsidian_btn": "Подтвердить",
"topics.export.obsidian_created": "Дата создания", "topics.export.obsidian_created": "Дата создания",
"topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания", "topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания",
"topics.export.obsidian_export_failed": "Экспорт не удалось", "topics.export.obsidian_export_failed": "Экспорт не удалось",
"topics.export.obsidian_export_success": "Экспорт успешно завершен", "topics.export.obsidian_export_success": "Экспорт успешно завершен",
"topics.export.obsidian_not_configured": "Obsidian не настроен",
"topics.export.obsidian_operate": "Метод обработки", "topics.export.obsidian_operate": "Метод обработки",
"topics.export.obsidian_operate_append": "Добавить в конец", "topics.export.obsidian_operate_append": "Добавить в конец",
"topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)", "topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)",
@ -199,6 +202,13 @@
"topics.export.obsidian_title": "Заголовок", "topics.export.obsidian_title": "Заголовок",
"topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок", "topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок",
"topics.export.obsidian_title_required": "Заголовок не может быть пустым", "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.title": "Экспорт",
"topics.export.word": "Экспорт как Word", "topics.export.word": "Экспорт как Word",
"topics.export.yuque": "Экспорт в Yuque", "topics.export.yuque": "Экспорт в Yuque",
@ -796,15 +806,6 @@
"notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90", "notion.split_size_help": "Рекомендуется 90 для пользователей бесплатной версии Notion, 24990 для пользователей премиум-версии, значение по умолчанию — 90",
"notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)", "notion.split_size_placeholder": "Введите ограничение количества блоков на странице (по умолчанию 90)",
"notion.title": "Настройки Notion", "notion.title": "Настройки Notion",
"obsidian": {
"folder": "Папка",
"folder_placeholder": "Пожалуйста, введите имя папки",
"tags": "Глобальные Теги",
"tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры.",
"title": "Конфигурация Obsidian",
"vault": "Хранилище",
"vault_placeholder": "Пожалуйста, введите имя хранилища"
},
"title": "Настройки данных", "title": "Настройки данных",
"webdav": { "webdav": {
"autoSync": "Автоматическое резервное копирование", "autoSync": "Автоматическое резервное копирование",
@ -850,6 +851,15 @@
"token": "Токен Yuque", "token": "Токен Yuque",
"token_placeholder": "Введите токен 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": { "siyuan": {
"title": "Конфигурация SiYuan Note", "title": "Конфигурация SiYuan Note",
"api_url": "API адрес", "api_url": "API адрес",

View File

@ -180,13 +180,16 @@
"topics.export.md": "导出为 Markdown", "topics.export.md": "导出为 Markdown",
"topics.export.notion": "导出到 Notion", "topics.export.notion": "导出到 Notion",
"topics.export.obsidian": "导出到 Obsidian", "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_atributes": "配置笔记属性",
"topics.export.obsidian_btn": "确定", "topics.export.obsidian_btn": "确定",
"topics.export.obsidian_created": "创建时间", "topics.export.obsidian_created": "创建时间",
"topics.export.obsidian_created_placeholder": "请选择创建时间", "topics.export.obsidian_created_placeholder": "请选择创建时间",
"topics.export.obsidian_export_failed": "导出失败", "topics.export.obsidian_export_failed": "导出到Obsidian失败",
"topics.export.obsidian_export_success": "导出成功", "topics.export.obsidian_export_success": "导出到Obsidian成功",
"topics.export.obsidian_not_configured": "Obsidian 未配置",
"topics.export.obsidian_operate": "处理方式", "topics.export.obsidian_operate": "处理方式",
"topics.export.obsidian_operate_append": "追加", "topics.export.obsidian_operate_append": "追加",
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)", "topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆盖)",
@ -199,6 +202,13 @@
"topics.export.obsidian_title": "标题", "topics.export.obsidian_title": "标题",
"topics.export.obsidian_title_placeholder": "请输入标题", "topics.export.obsidian_title_placeholder": "请输入标题",
"topics.export.obsidian_title_required": "标题不能为空", "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.title": "导出",
"topics.export.word": "导出为 Word", "topics.export.word": "导出为 Word",
"topics.export.yuque": "导出到语雀", "topics.export.yuque": "导出到语雀",
@ -796,15 +806,6 @@
"notion.split_size_help": "Notion免费版用户建议设置为90高级版用户建议设置为24990默认值为90", "notion.split_size_help": "Notion免费版用户建议设置为90高级版用户建议设置为24990默认值为90",
"notion.split_size_placeholder": "请输入每页块数限制(默认90)", "notion.split_size_placeholder": "请输入每页块数限制(默认90)",
"notion.title": "Notion 配置", "notion.title": "Notion 配置",
"obsidian": {
"folder": "文件夹",
"folder_placeholder": "请输入文件夹名称",
"tags": "全局标签",
"tags_placeholder": "请输入标签名称, 多个标签用英文逗号分隔",
"title": "Obsidian 配置",
"vault": "保管库",
"vault_placeholder": "请输入保管库名称"
},
"title": "数据设置", "title": "数据设置",
"webdav": { "webdav": {
"autoSync": "自动备份", "autoSync": "自动备份",
@ -850,6 +851,15 @@
"token": "语雀 Token", "token": "语雀 Token",
"token_placeholder": "请输入语雀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": { "siyuan": {
"title": "思源笔记配置", "title": "思源笔记配置",
"api_url": "API地址", "api_url": "API地址",

View File

@ -180,13 +180,16 @@
"topics.export.md": "匯出為 Markdown", "topics.export.md": "匯出為 Markdown",
"topics.export.notion": "匯出到 Notion", "topics.export.notion": "匯出到 Notion",
"topics.export.obsidian": "匯出到 Obsidian", "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_atributes": "配置筆記屬性",
"topics.export.obsidian_btn": "確定", "topics.export.obsidian_btn": "確定",
"topics.export.obsidian_created": "建立時間", "topics.export.obsidian_created": "建立時間",
"topics.export.obsidian_created_placeholder": "請選擇建立時間", "topics.export.obsidian_created_placeholder": "請選擇建立時間",
"topics.export.obsidian_export_failed": "匯出失敗", "topics.export.obsidian_export_failed": "匯出失敗",
"topics.export.obsidian_export_success": "匯出成功", "topics.export.obsidian_export_success": "匯出成功",
"topics.export.obsidian_not_configured": "Obsidian 未配置",
"topics.export.obsidian_operate": "處理方式", "topics.export.obsidian_operate": "處理方式",
"topics.export.obsidian_operate_append": "追加", "topics.export.obsidian_operate_append": "追加",
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)", "topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)",
@ -199,6 +202,13 @@
"topics.export.obsidian_title": "標題", "topics.export.obsidian_title": "標題",
"topics.export.obsidian_title_placeholder": "請輸入標題", "topics.export.obsidian_title_placeholder": "請輸入標題",
"topics.export.obsidian_title_required": "標題不能為空", "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.title": "匯出",
"topics.export.word": "匯出為 Word", "topics.export.word": "匯出為 Word",
"topics.export.yuque": "匯出到語雀", "topics.export.yuque": "匯出到語雀",
@ -796,15 +806,6 @@
"notion.split_size_help": "Notion 免費版使用者建議設定為 90進階版使用者建議設定為 24990預設值為 90", "notion.split_size_help": "Notion 免費版使用者建議設定為 90進階版使用者建議設定為 24990預設值為 90",
"notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)", "notion.split_size_placeholder": "請輸入每頁塊數限制 (預設 90)",
"notion.title": "Notion 設定", "notion.title": "Notion 設定",
"obsidian": {
"folder": "資料夾",
"folder_placeholder": "請輸入資料夾名稱",
"tags": "全域標籤",
"tags_placeholder": "請輸入標籤名稱多個標籤用英文逗號分隔。Obsidian 不可用純數字。",
"title": "Obsidian 設定",
"vault": "保險庫",
"vault_placeholder": "請輸入保險庫名稱"
},
"title": "資料設定", "title": "資料設定",
"webdav": { "webdav": {
"autoSync": "自動備份", "autoSync": "自動備份",
@ -850,6 +851,15 @@
"token": "語雀 Token", "token": "語雀 Token",
"token_placeholder": "請輸入語雀 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": { "siyuan": {
"title": "思源筆記配置", "title": "思源筆記配置",
"api_url": "API地址", "api_url": "API地址",

View File

@ -37,7 +37,7 @@ const DataSettings: FC = () => {
const [appInfo, setAppInfo] = useState<AppInfo>() const [appInfo, setAppInfo] = useState<AppInfo>()
const { size, removeAllFiles } = useKnowledgeFiles() const { size, removeAllFiles } = useKnowledgeFiles()
const { theme } = useTheme() const { theme } = useTheme()
const [menu, setMenu] = useState<string>('data') const [menu, setMenu] = useState<string>('common')
//joplin icon needs to be updated into iconfont //joplin icon needs to be updated into iconfont
const JoplinIcon = () => ( const JoplinIcon = () => (
@ -77,17 +77,17 @@ const DataSettings: FC = () => {
title: 'settings.data.yuque.title', title: 'settings.data.yuque.title',
icon: <YuqueOutlined style={{ fontSize: 16 }} /> icon: <YuqueOutlined style={{ fontSize: 16 }} />
}, },
{
key: 'obsidian',
title: 'settings.data.obsidian.title',
icon: <i className="iconfont icon-obsidian" />
},
{ {
key: 'joplin', key: 'joplin',
title: 'settings.data.joplin.title', title: 'settings.data.joplin.title',
//joplin icon needs to be updated into iconfont //joplin icon needs to be updated into iconfont
icon: <JoplinIcon /> icon: <JoplinIcon />
}, },
{
key: 'obsidian',
title: 'settings.data.obsidian.title',
icon: <i className="iconfont icon-obsidian" />
},
{ {
key: 'siyuan', key: 'siyuan',
title: 'settings.data.siyuan.title', title: 'settings.data.siyuan.title',
@ -230,8 +230,8 @@ const DataSettings: FC = () => {
{menu === 'markdown_export' && <MarkdownExportSettings />} {menu === 'markdown_export' && <MarkdownExportSettings />}
{menu === 'notion' && <NotionSettings />} {menu === 'notion' && <NotionSettings />}
{menu === 'yuque' && <YuqueSettings />} {menu === 'yuque' && <YuqueSettings />}
{menu === 'obsidian' && <ObsidianSettings />}
{menu === 'joplin' && <JoplinSettings />} {menu === 'joplin' && <JoplinSettings />}
{menu === 'obsidian' && <ObsidianSettings />}
{menu === 'siyuan' && <SiyuanSettings />} {menu === 'siyuan' && <SiyuanSettings />}
</SettingContainer> </SettingContainer>
</Container> </Container>

View File

@ -1,95 +1,90 @@
import { HStack } from '@renderer/components/Layout' import { HStack } from '@renderer/components/Layout'
import { useTheme } from '@renderer/context/ThemeProvider' import { useSettings } from '@renderer/hooks/useSettings'
import { RootState, useAppDispatch } from '@renderer/store' import { useAppDispatch } from '@renderer/store'
import { setObsidianFolder, setObsidianTages, setObsidianValut } from '@renderer/store/settings' import { setDefaultObsidianVault } from '@renderer/store/settings'
import Input from 'antd/es/input/Input' import { Empty, Select, Spin } from 'antd'
import { FC } from 'react' import { FC, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..' import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle } from '..'
const { Option } = Select
const ObsidianSettings: FC = () => { const ObsidianSettings: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { theme } = useTheme() const { defaultObsidianVault } = useSettings()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
// const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey) const [vaults, setVaults] = useState<Array<{ path: string; name: string }>>([])
// const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl) const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const obsidianVault = useSelector((state: RootState) => state.settings.obsidianValut) // 组件加载时获取Vault列表
const obsidianFolder = useSelector((state: RootState) => state.settings.obsidianFolder) useEffect(() => {
const obsidianTags = useSelector((state: RootState) => state.settings.obsidianTages) const fetchVaults = async () => {
try {
setLoading(true)
setError(null)
const vaultsData = await window.obsidian.getVaults()
const handleObsidianVaultChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (vaultsData.length === 0) {
dispatch(setObsidianValut(e.target.value)) setError(t('settings.data.obsidian.default_vault_no_vaults'))
setLoading(false)
return
} }
const handleObsidianFolderChange = (e: React.ChangeEvent<HTMLInputElement>) => { setVaults(vaultsData)
dispatch(setObsidianFolder(e.target.value))
// 如果没有设置默认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>) => { fetchVaults()
dispatch(setObsidianValut(e.target.value)) }, [dispatch, defaultObsidianVault, t])
}
const handleObsidianFolderBlur = (e: React.FocusEvent<HTMLInputElement>) => { const handleChange = (value: string) => {
dispatch(setObsidianFolder(e.target.value)) dispatch(setDefaultObsidianVault(value))
}
const handleObsidianTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setObsidianTages(e.target.value))
}
const handleObsidianTagsBlur = (e: React.FocusEvent<HTMLInputElement>) => {
dispatch(setObsidianTages(e.target.value))
} }
return ( return (
<SettingGroup theme={theme}> <SettingGroup>
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle> <SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
<SettingDivider /> <SettingDivider />
<SettingRow> <SettingRow>
<SettingRowTitle>{t('settings.data.obsidian.vault')}</SettingRowTitle> <SettingRowTitle>{t('settings.data.obsidian.default_vault')}</SettingRowTitle>
<HStack alignItems="center" gap="5px" style={{ width: 315 }}> <HStack gap="5px">
<Input <Spin spinning={loading} size="small">
type="text" {vaults.length > 0 ? (
value={obsidianVault || ''} <Select
onChange={handleObsidianVaultChange} value={defaultObsidianVault || undefined}
onBlur={handleObsidianVaultBlur} onChange={handleChange}
style={{ width: 315 }} placeholder={t('settings.data.obsidian.default_vault_placeholder')}
placeholder={t('settings.data.obsidian.vault_placeholder')} style={{ width: 300 }}>
/> {vaults.map((vault) => (
</HStack> <Option key={vault.name} value={vault.name}>
</SettingRow> {vault.name}
<SettingDivider /> </Option>
<SettingRow> ))}
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}> </Select>
<span>{t('settings.data.obsidian.folder')}</span> ) : (
</SettingRowTitle> <Empty
<HStack alignItems="center" gap="5px" style={{ width: 315 }}> description={
<Input loading
value={obsidianFolder || ''} ? t('settings.data.obsidian.default_vault_loading')
onChange={handleObsidianFolderChange} : error || t('settings.data.obsidian.default_vault_no_vaults')
onBlur={handleObsidianFolderBlur} }
style={{ width: 315 }} image={Empty.PRESENTED_IMAGE_SIMPLE}
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')}
/> />
)}
</Spin>
</HStack> </HStack>
</SettingRow> </SettingRow>
</SettingGroup> </SettingGroup>

View File

@ -90,12 +90,9 @@ export interface SettingsState {
yuqueToken: string | null yuqueToken: string | null
yuqueUrl: string | null yuqueUrl: string | null
yuqueRepoId: string | null yuqueRepoId: string | null
//obsidian settings obsidianVault, obisidanFolder
obsidianValut: string | null
obsidianFolder: string | null
obsidianTages: string | null
joplinToken: string | null joplinToken: string | null
joplinUrl: string | null joplinUrl: string | null
defaultObsidianVault: string | null
// 思源笔记配置 // 思源笔记配置
siyuanApiUrl: string | null siyuanApiUrl: string | null
siyuanToken: string | null siyuanToken: string | null
@ -172,11 +169,9 @@ const initialState: SettingsState = {
yuqueToken: '', yuqueToken: '',
yuqueUrl: '', yuqueUrl: '',
yuqueRepoId: '', yuqueRepoId: '',
obsidianValut: '',
obsidianFolder: '',
obsidianTages: '',
joplinToken: '', joplinToken: '',
joplinUrl: '', joplinUrl: '',
defaultObsidianVault: null,
// 思源笔记配置初始值 // 思源笔记配置初始值
siyuanApiUrl: null, siyuanApiUrl: null,
siyuanToken: null, siyuanToken: null,
@ -386,15 +381,6 @@ const settingsSlice = createSlice({
setYuqueUrl: (state, action: PayloadAction<string>) => { setYuqueUrl: (state, action: PayloadAction<string>) => {
state.yuqueUrl = action.payload 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>) => { setJoplinToken: (state, action: PayloadAction<string>) => {
state.joplinToken = action.payload state.joplinToken = action.payload
}, },
@ -415,6 +401,9 @@ const settingsSlice = createSlice({
}, },
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => { setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
state.messageNavigation = action.payload state.messageNavigation = action.payload
},
setDefaultObsidianVault: (state, action: PayloadAction<string>) => {
state.defaultObsidianVault = action.payload
} }
} }
}) })
@ -484,16 +473,14 @@ export const {
setYuqueToken, setYuqueToken,
setYuqueRepoId, setYuqueRepoId,
setYuqueUrl, setYuqueUrl,
setObsidianValut,
setObsidianFolder,
setObsidianTages,
setJoplinToken, setJoplinToken,
setJoplinUrl, setJoplinUrl,
setMessageNavigation,
setDefaultObsidianVault,
setSiyuanApiUrl, setSiyuanApiUrl,
setSiyuanToken, setSiyuanToken,
setSiyuanBoxId, setSiyuanBoxId,
setSiyuanRootPath, setSiyuanRootPath
setMessageNavigation
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

9
src/renderer/src/types/electron.d.ts vendored Normal file
View 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
}

View File

@ -326,31 +326,49 @@ export const exportMarkdownToYuque = async (title: string, content: string) => {
* @param attributes.source * @param attributes.source
* @param attributes.tags * @param attributes.tags
* @param attributes.processingMethod * @param attributes.processingMethod
* @param attributes.folder
* @param attributes.vault Vault名称
*/ */
export const exportMarkdownToObsidian = async (attributes: any) => { export const exportMarkdownToObsidian = async (attributes: any) => {
try { try {
const obsidianValut = store.getState().settings.obsidianValut // 从参数获取Vault名称
const obsidianFolder = store.getState().settings.obsidianFolder 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')) window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
return return
} }
let path = ''
if (!attributes.title) { if (!attributes.title) {
window.message.error(i18n.t('chat.topics.export.obsidian_title_required')) window.message.error(i18n.t('chat.topics.export.obsidian_title_required'))
return return
} }
//构建保存路径添加以 / 结尾 // 检查是否选择了.md文件
if (!obsidianFolder.endsWith('/')) { if (obsidianFolder && obsidianFolder.endsWith('.md')) {
path = obsidianFolder + '/' isMarkdownFile = true
} }
let filePath = ''
// 如果是.md文件直接使用该文件路径
if (isMarkdownFile) {
filePath = obsidianFolder
} else {
// 否则构建路径
//构建保存路径添加以 / 结尾
if (obsidianFolder && !obsidianFolder.endsWith('/')) {
obsidianFolder = obsidianFolder + '/'
}
//构建文件名 //构建文件名
const fileName = transformObsidianFileName(attributes.title) 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') { if (attributes.processingMethod === '3') {
obsidianUrl += '&overwrite=true' obsidianUrl += '&overwrite=true'
@ -359,6 +377,7 @@ export const exportMarkdownToObsidian = async (attributes: any) => {
} else if (attributes.processingMethod === '1') { } else if (attributes.processingMethod === '1') {
obsidianUrl += '&append=true' obsidianUrl += '&append=true'
} }
window.open(obsidianUrl) window.open(obsidianUrl)
window.message.success(i18n.t('chat.topics.export.obsidian_export_success')) window.message.success(i18n.t('chat.topics.export.obsidian_export_success'))
} catch (error) { } catch (error) {