refactor: 改进 Obsidian 导出,不再依赖 Obsidian 第三方插件 (#3637)
改进 Obsidian 导出,不在依赖 Obsidian 第三方插件
This commit is contained in:
parent
e0f1768c4f
commit
1e4d6f196f
120
src/renderer/src/components/ObsidianExportDialog.tsx
Normal file
120
src/renderer/src/components/ObsidianExportDialog.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import i18n from '@renderer/i18n'
|
||||||
|
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
||||||
|
import { Form, Input, Modal, Select } from 'antd'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
const { Option } = Select
|
||||||
|
|
||||||
|
interface ObsidianExportDialogProps {
|
||||||
|
title: string
|
||||||
|
markdown: string
|
||||||
|
open: boolean // 使用 open 属性替代 visible
|
||||||
|
onClose: (success: boolean) => void
|
||||||
|
obsidianTags: string | null
|
||||||
|
processingMethod: string | '3' //默认新增(存在就覆盖)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ObsidianExportDialog: React.FC<ObsidianExportDialogProps> = ({
|
||||||
|
title,
|
||||||
|
markdown,
|
||||||
|
obsidianTags,
|
||||||
|
processingMethod,
|
||||||
|
open,
|
||||||
|
onClose
|
||||||
|
}) => {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
title: title,
|
||||||
|
tags: obsidianTags || '',
|
||||||
|
createdAt: new Date().toISOString().split('T')[0],
|
||||||
|
source: 'Cherry Studio',
|
||||||
|
processingMethod: processingMethod
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOk = async () => {
|
||||||
|
//构建content 并复制到粘贴板
|
||||||
|
let content = ''
|
||||||
|
if (state.processingMethod !== '3') {
|
||||||
|
content = `\n---\n${markdown}`
|
||||||
|
} else {
|
||||||
|
content = `---
|
||||||
|
\ntitle: ${state.title}
|
||||||
|
\ncreated: ${state.createdAt}
|
||||||
|
\nsource: ${state.source}
|
||||||
|
\ntags: ${state.tags}
|
||||||
|
\n---\n${markdown}`
|
||||||
|
}
|
||||||
|
if (content === '') {
|
||||||
|
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
||||||
|
}
|
||||||
|
await navigator.clipboard.writeText(content)
|
||||||
|
markdown = ''
|
||||||
|
exportMarkdownToObsidian(state)
|
||||||
|
onClose(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
onClose(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleChange = (key: string, value: any) => {
|
||||||
|
setState((prevState) => ({ ...prevState, [key]: value }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={i18n.t('chat.topics.export.obsidian_atributes')}
|
||||||
|
open={open} // 使用 open 属性
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
width={600}
|
||||||
|
closable
|
||||||
|
maskClosable
|
||||||
|
centered
|
||||||
|
okButtonProps={{ type: 'primary' }}
|
||||||
|
okText={i18n.t('chat.topics.export.obsidian_btn')}>
|
||||||
|
<Form layout="horizontal" labelCol={{ span: 6 }} wrapperCol={{ span: 18 }} labelAlign="left">
|
||||||
|
<Form.Item label={i18n.t('chat.topics.export.obsidian_title')}>
|
||||||
|
<Input
|
||||||
|
value={state.title}
|
||||||
|
onChange={(e) => handleChange('title', e.target.value)}
|
||||||
|
placeholder={i18n.t('chat.topics.export.obsidian_title_placeholder')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={i18n.t('chat.topics.export.obsidian_tags')}>
|
||||||
|
<Input
|
||||||
|
value={state.tags}
|
||||||
|
onChange={(e) => handleChange('tags', e.target.value)}
|
||||||
|
placeholder={i18n.t('chat.topics.export.obsidian_tags_placeholder')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={i18n.t('chat.topics.export.obsidian_created')}>
|
||||||
|
<Input
|
||||||
|
value={state.createdAt}
|
||||||
|
onChange={(e) => handleChange('createdAt', e.target.value)}
|
||||||
|
placeholder={i18n.t('chat.topics.export.obsidian_created_placeholder')}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item label={i18n.t('chat.topics.export.obsidian_source')}>
|
||||||
|
<Input
|
||||||
|
value={state.source}
|
||||||
|
onChange={(e) => handleChange('source', e.target.value)}
|
||||||
|
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}
|
||||||
|
onChange={(value) => handleChange('processingMethod', value)}
|
||||||
|
placeholder={i18n.t('chat.topics.export.obsidian_operate_placeholder')}
|
||||||
|
allowClear>
|
||||||
|
<Option value="1">{i18n.t('chat.topics.export.obsidian_operate_append')}</Option>
|
||||||
|
<Option value="2">{i18n.t('chat.topics.export.obsidian_operate_prepend')}</Option>
|
||||||
|
<Option value="3">{i18n.t('chat.topics.export.obsidian_operate_new_or_overwrite')}</Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ObsidianExportDialog
|
||||||
@ -1,228 +0,0 @@
|
|||||||
import { FileOutlined, FolderOutlined } from '@ant-design/icons'
|
|
||||||
import { Spin, Switch, Tree } from 'antd'
|
|
||||||
import { FC, useEffect, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import styled from 'styled-components'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
defaultPath: string
|
|
||||||
obsidianUrl: string
|
|
||||||
obsidianApiKey: string
|
|
||||||
onPathChange: (path: string, isMdFile: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TreeNode {
|
|
||||||
title: string
|
|
||||||
key: string
|
|
||||||
isLeaf: boolean
|
|
||||||
isMdFile?: boolean
|
|
||||||
children?: TreeNode[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const ObsidianFolderSelector: FC<Props> = ({ defaultPath, obsidianUrl, obsidianApiKey, onPathChange }) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const [treeData, setTreeData] = useState<TreeNode[]>([])
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
|
||||||
const [expandedKeys, setExpandedKeys] = useState<string[]>(['/'])
|
|
||||||
const [showMdFiles, setShowMdFiles] = useState<boolean>(false)
|
|
||||||
// 当前选中的节点信息
|
|
||||||
const [currentSelection, setCurrentSelection] = useState({
|
|
||||||
path: defaultPath,
|
|
||||||
isMdFile: false
|
|
||||||
})
|
|
||||||
// 使用key强制Tree组件重新渲染
|
|
||||||
const [treeKey, setTreeKey] = useState<number>(0)
|
|
||||||
|
|
||||||
// 只初始化根节点,不立即加载内容
|
|
||||||
useEffect(() => {
|
|
||||||
initializeRootNode()
|
|
||||||
}, [showMdFiles])
|
|
||||||
|
|
||||||
// 初始化根节点,但不自动加载子节点
|
|
||||||
const initializeRootNode = () => {
|
|
||||||
const rootNode: TreeNode = {
|
|
||||||
title: '/',
|
|
||||||
key: '/',
|
|
||||||
isLeaf: false
|
|
||||||
}
|
|
||||||
|
|
||||||
setTreeData([rootNode])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 异步加载子节点
|
|
||||||
const loadData = async (node: any) => {
|
|
||||||
if (node.isLeaf) return // 如果是叶子节点(md文件),不加载子节点
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
// 确保路径末尾有斜杠
|
|
||||||
const path = node.key === '/' ? '' : node.key
|
|
||||||
const requestPath = path.endsWith('/') ? path : `${path}/`
|
|
||||||
|
|
||||||
const response = await fetch(`${obsidianUrl}vault${requestPath}`, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${obsidianApiKey}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok || (!data?.files && data?.errorCode !== 40400)) {
|
|
||||||
throw new Error('获取文件夹失败')
|
|
||||||
}
|
|
||||||
|
|
||||||
const childNodes: TreeNode[] = (data.files || [])
|
|
||||||
.filter((file: string) => file.endsWith('/') || (showMdFiles && file.endsWith('.md'))) // 根据开关状态决定是否显示md文件
|
|
||||||
.map((file: string) => {
|
|
||||||
// 修复路径问题,避免重复的斜杠
|
|
||||||
const normalizedFile = file.replace('/', '')
|
|
||||||
const isMdFile = file.endsWith('.md')
|
|
||||||
const childPath = requestPath.endsWith('/')
|
|
||||||
? `${requestPath}${normalizedFile}${isMdFile ? '' : '/'}`
|
|
||||||
: `${requestPath}/${normalizedFile}${isMdFile ? '' : '/'}`
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: normalizedFile,
|
|
||||||
key: childPath,
|
|
||||||
isLeaf: isMdFile,
|
|
||||||
isMdFile
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 更新节点的子节点
|
|
||||||
setTreeData((origin) => {
|
|
||||||
const loop = (data: TreeNode[], key: string, children: TreeNode[]): TreeNode[] => {
|
|
||||||
return data.map((item) => {
|
|
||||||
if (item.key === key) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
children
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.children) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
children: loop(item.children, key, children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return loop(origin, node.key, childNodes)
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
window.message.error(t('chat.topics.export.obsidian_fetch_failed'))
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理开关切换
|
|
||||||
const handleSwitchChange = (checked: boolean) => {
|
|
||||||
setShowMdFiles(checked)
|
|
||||||
// 重置选择
|
|
||||||
setCurrentSelection({
|
|
||||||
path: defaultPath,
|
|
||||||
isMdFile: false
|
|
||||||
})
|
|
||||||
onPathChange(defaultPath, false)
|
|
||||||
|
|
||||||
// 重置Tree状态并强制重新渲染
|
|
||||||
setTreeData([])
|
|
||||||
setExpandedKeys(['/'])
|
|
||||||
|
|
||||||
// 递增key值以强制Tree组件完全重新渲染
|
|
||||||
setTreeKey((prev) => prev + 1)
|
|
||||||
|
|
||||||
// 延迟初始化根节点,让状态完全清除
|
|
||||||
setTimeout(() => {
|
|
||||||
initializeRootNode()
|
|
||||||
}, 50)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 自定义图标,为md文件和文件夹显示不同的图标
|
|
||||||
const renderIcon = (props: any) => {
|
|
||||||
const { data } = props
|
|
||||||
if (data.isMdFile) {
|
|
||||||
return <FileOutlined />
|
|
||||||
}
|
|
||||||
return <FolderOutlined />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<SwitchContainer>
|
|
||||||
<span>{t('chat.topics.export.obsidian_show_md_files')}</span>
|
|
||||||
<Switch checked={showMdFiles} onChange={handleSwitchChange} />
|
|
||||||
</SwitchContainer>
|
|
||||||
<Spin spinning={loading}>
|
|
||||||
<TreeContainer>
|
|
||||||
<Tree
|
|
||||||
key={treeKey} // 使用key来强制重新渲染
|
|
||||||
defaultSelectedKeys={[defaultPath]}
|
|
||||||
expandedKeys={expandedKeys}
|
|
||||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
|
||||||
treeData={treeData}
|
|
||||||
loadData={loadData}
|
|
||||||
onSelect={(selectedKeys, info) => {
|
|
||||||
if (selectedKeys.length > 0) {
|
|
||||||
const path = selectedKeys[0] as string
|
|
||||||
const isMdFile = !!(info.node as any).isMdFile
|
|
||||||
|
|
||||||
setCurrentSelection({
|
|
||||||
path,
|
|
||||||
isMdFile
|
|
||||||
})
|
|
||||||
|
|
||||||
onPathChange?.(path, isMdFile)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
showLine
|
|
||||||
showIcon
|
|
||||||
icon={renderIcon}
|
|
||||||
/>
|
|
||||||
</TreeContainer>
|
|
||||||
</Spin>
|
|
||||||
<div>
|
|
||||||
{currentSelection.path !== defaultPath && (
|
|
||||||
<SelectedPath>
|
|
||||||
{t('chat.topics.export.obsidian_selected_path')}: {currentSelection.path}
|
|
||||||
</SelectedPath>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 400px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const TreeContainer = styled.div`
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
border: 1px solid var(--color-border);
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
height: 320px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const SwitchContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding: 0 10px;
|
|
||||||
`
|
|
||||||
|
|
||||||
const SelectedPath = styled.div`
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--color-text-2);
|
|
||||||
margin-top: 5px;
|
|
||||||
padding: 0 10px;
|
|
||||||
word-break: break-all;
|
|
||||||
`
|
|
||||||
|
|
||||||
export default ObsidianFolderSelector
|
|
||||||
@ -1,68 +1,52 @@
|
|||||||
import ObsidianFolderSelector from '@renderer/components/ObsidianFolderSelector'
|
import ObsidianExportDialog from '@renderer/components/ObsidianExportDialog'
|
||||||
import i18n from '@renderer/i18n'
|
import i18n from '@renderer/i18n'
|
||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { exportMarkdownToObsidian } from '@renderer/utils/export'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
|
||||||
interface ObsidianExportOptions {
|
interface ObsidianExportOptions {
|
||||||
title: string
|
title: string
|
||||||
markdown: string
|
markdown: string
|
||||||
|
processingMethod: string | '3' // 默认新增(存在就覆盖)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用于显示 Obsidian 导出对话框
|
/**
|
||||||
|
* 配置Obsidian 笔记属性弹窗
|
||||||
|
* @param options.title 标题
|
||||||
|
* @param options.markdown markdown内容
|
||||||
|
* @param options.processingMethod 处理方式
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
const showObsidianExportDialog = async (options: ObsidianExportOptions): Promise<boolean> => {
|
||||||
const { title, markdown } = options
|
const obsidianValut = store.getState().settings.obsidianValut
|
||||||
const obsidianUrl = store.getState().settings.obsidianUrl
|
const obsidianFolder = store.getState().settings.obsidianFolder
|
||||||
const obsidianApiKey = store.getState().settings.obsidianApiKey
|
|
||||||
|
|
||||||
if (!obsidianUrl || !obsidianApiKey) {
|
if (!obsidianValut || !obsidianFolder) {
|
||||||
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
window.message.error(i18n.t('chat.topics.export.obsidian_not_configured'))
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
// 创建一个状态变量来存储选择的路径
|
|
||||||
let selectedPath = '/'
|
|
||||||
let selectedIsMdFile = false
|
|
||||||
|
|
||||||
// 显示文件夹选择对话框
|
|
||||||
return new Promise<boolean>((resolve) => {
|
return new Promise<boolean>((resolve) => {
|
||||||
window.modal.confirm({
|
const div = document.createElement('div')
|
||||||
title: i18n.t('chat.topics.export.obsidian_select_folder'),
|
document.body.appendChild(div)
|
||||||
content: (
|
const root = createRoot(div)
|
||||||
<ObsidianFolderSelector
|
|
||||||
defaultPath={selectedPath}
|
|
||||||
obsidianUrl={obsidianUrl}
|
|
||||||
obsidianApiKey={obsidianApiKey}
|
|
||||||
onPathChange={(path, isMdFile) => {
|
|
||||||
selectedPath = path
|
|
||||||
selectedIsMdFile = isMdFile
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
width: 600,
|
|
||||||
icon: null,
|
|
||||||
closable: true,
|
|
||||||
maskClosable: true,
|
|
||||||
centered: true,
|
|
||||||
okButtonProps: { type: 'primary' },
|
|
||||||
okText: i18n.t('chat.topics.export.obsidian_select_folder.btn'),
|
|
||||||
onOk: () => {
|
|
||||||
// 如果选择的是md文件,则使用选择的文件名而不是传入的标题
|
|
||||||
const fileName = selectedIsMdFile ? selectedPath.split('/').pop()?.replace('.md', '') : title
|
|
||||||
|
|
||||||
exportMarkdownToObsidian(fileName as string, markdown, selectedPath, selectedIsMdFile)
|
const handleClose = (success: boolean) => {
|
||||||
resolve(true)
|
root.unmount()
|
||||||
},
|
document.body.removeChild(div)
|
||||||
onCancel: () => {
|
resolve(success)
|
||||||
resolve(false)
|
|
||||||
}
|
}
|
||||||
|
const obsidianTags = store.getState().settings.obsidianTages
|
||||||
|
root.render(
|
||||||
|
<ObsidianExportDialog
|
||||||
|
title={options.title}
|
||||||
|
markdown={options.markdown}
|
||||||
|
obsidianTags={obsidianTags}
|
||||||
|
processingMethod={options.processingMethod}
|
||||||
|
open={true}
|
||||||
|
onClose={handleClose}
|
||||||
|
/>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
window.message.error(i18n.t('chat.topics.export.obsidian_fetch_failed'))
|
|
||||||
console.error(error)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ObsidianExportPopup = {
|
const ObsidianExportPopup = {
|
||||||
|
|||||||
@ -164,13 +164,24 @@
|
|||||||
"topics.export.yuque": "Export to Yuque",
|
"topics.export.yuque": "Export to Yuque",
|
||||||
"topics.export.obsidian": "Export to Obsidian",
|
"topics.export.obsidian": "Export to Obsidian",
|
||||||
"topics.export.obsidian_not_configured": "Obsidian not configured",
|
"topics.export.obsidian_not_configured": "Obsidian not configured",
|
||||||
"topics.export.obsidian_fetch_failed": "Failed to fetch Obsidian folder structure",
|
"topics.export.obsidian_title": "Title",
|
||||||
"topics.export.obsidian_select_folder": "Select Obsidian folder",
|
"topics.export.obsidian_title_placeholder": "Please enter the title",
|
||||||
"topics.export.obsidian_select_folder.btn": "Confirm",
|
"topics.export.obsidian_title_required": "The title cannot be empty",
|
||||||
|
"topics.export.obsidian_tags": "Tags",
|
||||||
|
"topics.export.obsidian_tags_placeholder": "Please enter tags, separate multiple tags with commas in English,In Obsidian, pure numbers cannot be used.",
|
||||||
|
"topics.export.obsidian_created": "Creation Time",
|
||||||
|
"topics.export.obsidian_created_placeholder": "Please select the creation time",
|
||||||
|
"topics.export.obsidian_source": "Source",
|
||||||
|
"topics.export.obsidian_source_placeholder": "Please enter the source",
|
||||||
|
"topics.export.obsidian_operate": "Operation Method",
|
||||||
|
"topics.export.obsidian_operate_placeholder": "Please select the operation method",
|
||||||
|
"topics.export.obsidian_operate_append": "Append",
|
||||||
|
"topics.export.obsidian_operate_prepend": "Prepend",
|
||||||
|
"topics.export.obsidian_operate_new_or_overwrite": "Create New (Overwrite if it exists)",
|
||||||
|
"topics.export.obsidian_atributes": "Configure Note Attributes",
|
||||||
|
"topics.export.obsidian_btn": "Confirm",
|
||||||
"topics.export.obsidian_export_success": "Export success",
|
"topics.export.obsidian_export_success": "Export success",
|
||||||
"topics.export.obsidian_export_failed": "Export failed",
|
"topics.export.obsidian_export_failed": "Export failed",
|
||||||
"topics.export.obsidian_show_md_files": "Show MD Files",
|
|
||||||
"topics.export.obsidian_selected_path": "Selected Path",
|
|
||||||
"topics.export.joplin": "Export to Joplin",
|
"topics.export.joplin": "Export to Joplin",
|
||||||
"topics.list": "Topic List",
|
"topics.list": "Topic List",
|
||||||
"topics.move_to": "Move to",
|
"topics.move_to": "Move to",
|
||||||
@ -792,19 +803,13 @@
|
|||||||
"token_placeholder": "Please enter the Yuque Token"
|
"token_placeholder": "Please enter the Yuque Token"
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"check": {
|
|
||||||
"button": "Check",
|
|
||||||
"empty_url": "Please enter the Obsidian REST API URL first",
|
|
||||||
"empty_api_key": "Please enter the Obsidian API Key first",
|
|
||||||
"fail": "Obsidian connection verification failed",
|
|
||||||
"success": "Obsidian connection verification successful"
|
|
||||||
},
|
|
||||||
"help": "Install the Obsidian plugin Local REST API first, then get the Obsidian API Key",
|
|
||||||
"url": "Obsidian Knowledge Base URL",
|
|
||||||
"url_placeholder": "http://127.0.0.1:27123/",
|
|
||||||
"title": "Obsidian Configuration",
|
"title": "Obsidian Configuration",
|
||||||
"api_key": "Obsidian API Key",
|
"vault": "Vault",
|
||||||
"api_key_placeholder": "Please enter the Obsidian API Key"
|
"vault_placeholder": "Please enter the vault name",
|
||||||
|
"folder": "Folder",
|
||||||
|
"folder_placeholder": "Please enter the folder name",
|
||||||
|
"tags": "Global Tags",
|
||||||
|
"tags_placeholder": "Please enter the tag name, separate multiple tags with commas in English,In Obsidian, pure numbers cannot be used."
|
||||||
},
|
},
|
||||||
"joplin": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -164,13 +164,24 @@
|
|||||||
"topics.export.yuque": "語雀にエクスポート",
|
"topics.export.yuque": "語雀にエクスポート",
|
||||||
"topics.export.obsidian": "Obsidian にエクスポート",
|
"topics.export.obsidian": "Obsidian にエクスポート",
|
||||||
"topics.export.obsidian_not_configured": "Obsidian 未設定",
|
"topics.export.obsidian_not_configured": "Obsidian 未設定",
|
||||||
"topics.export.obsidian_fetch_failed": "Obsidian ファイルフォルダ構造取得失敗",
|
"topics.export.obsidian_title": "タイトル",
|
||||||
"topics.export.obsidian_select_folder": "Obsidian ファイルフォルダ選択",
|
"topics.export.obsidian_title_placeholder": "タイトルを入力してください",
|
||||||
"topics.export.obsidian_select_folder.btn": "確定",
|
"topics.export.obsidian_title_required": "タイトルは空白にできません",
|
||||||
|
"topics.export.obsidian_tags": "タグ",
|
||||||
|
"topics.export.obsidian_tags_placeholder": "タグを入力してください。複数のタグは英語のコンマで区切ってください,Obsidian では、純粋な数字を使用することはできません。",
|
||||||
|
"topics.export.obsidian_created": "作成日時",
|
||||||
|
"topics.export.obsidian_created_placeholder": "作成日時を選択してください",
|
||||||
|
"topics.export.obsidian_source": "ソース",
|
||||||
|
"topics.export.obsidian_source_placeholder": "ソースを入力してください",
|
||||||
|
"topics.export.obsidian_operate": "処理方法",
|
||||||
|
"topics.export.obsidian_operate_placeholder": "処理方法を選択してください",
|
||||||
|
"topics.export.obsidian_operate_append": "追加",
|
||||||
|
"topics.export.obsidian_operate_prepend": "先頭に追加",
|
||||||
|
"topics.export.obsidian_operate_new_or_overwrite": "新規作成(既に存在する場合は上書き)",
|
||||||
|
"topics.export.obsidian_atributes": "ノートの属性を設定",
|
||||||
|
"topics.export.obsidian_btn": "確定",
|
||||||
"topics.export.obsidian_export_success": "エクスポート成功",
|
"topics.export.obsidian_export_success": "エクスポート成功",
|
||||||
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
"topics.export.obsidian_export_failed": "エクスポート失敗",
|
||||||
"topics.export.obsidian_show_md_files": "mdファイルを表示",
|
|
||||||
"topics.export.obsidian_selected_path": "選択済みパス",
|
|
||||||
"topics.export.joplin": "Joplin にエクスポート",
|
"topics.export.joplin": "Joplin にエクスポート",
|
||||||
"topics.list": "トピックリスト",
|
"topics.list": "トピックリスト",
|
||||||
"topics.move_to": "移動先",
|
"topics.move_to": "移動先",
|
||||||
@ -792,19 +803,13 @@
|
|||||||
"token_placeholder": "Yuqueトークンを入力してください"
|
"token_placeholder": "Yuqueトークンを入力してください"
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"check": {
|
"title": "Obsidian の設定",
|
||||||
"button": "確認",
|
"vault": "ヴォールト(保管庫)",
|
||||||
"empty_url": "Obsidian REST API URL を先に入力してください",
|
"vault_placeholder": "保管庫の名前を入力してください",
|
||||||
"empty_api_key": "Obsidian API Key を先に入力してください",
|
"folder": "フォルダー",
|
||||||
"fail": "Obsidian 接続確認に失敗しました",
|
"folder_placeholder": "フォルダーの名前を入力してください",
|
||||||
"success": "Obsidian 接続確認に成功しました"
|
"tags": "グローバルタグ",
|
||||||
},
|
"tags_placeholder": "タグの名前を入力してください。複数のタグは英語のコンマで区切ってください,Obsidian では、純粋な数字を使用することはできません。"
|
||||||
"help": "Obsidian プラグイン Local REST API を先にインストールしてください。その後、Obsidian API Key を取得してください",
|
|
||||||
"url": "Obsidian 知識ベース URL",
|
|
||||||
"url_placeholder": "http://127.0.0.1:27123/",
|
|
||||||
"title": "Obsidian 設定",
|
|
||||||
"api_key": "Obsidian API Key",
|
|
||||||
"api_key_placeholder": "Obsidian API Key を入力してください"
|
|
||||||
},
|
},
|
||||||
"joplin": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -164,13 +164,24 @@
|
|||||||
"topics.export.yuque": "Экспорт в Yuque",
|
"topics.export.yuque": "Экспорт в Yuque",
|
||||||
"topics.export.obsidian": "Экспорт в Obsidian",
|
"topics.export.obsidian": "Экспорт в Obsidian",
|
||||||
"topics.export.obsidian_not_configured": "Obsidian не настроен",
|
"topics.export.obsidian_not_configured": "Obsidian не настроен",
|
||||||
"topics.export.obsidian_fetch_failed": "Не удалось получить структуру файлов Obsidian",
|
"topics.export.obsidian_title": "Заголовок",
|
||||||
"topics.export.obsidian_select_folder": "Выберите папку Obsidian",
|
"topics.export.obsidian_title_placeholder": "Пожалуйста, введите заголовок",
|
||||||
"topics.export.obsidian_select_folder.btn": "Определить",
|
"topics.export.obsidian_title_required": "Заголовок не может быть пустым",
|
||||||
|
"topics.export.obsidian_tags": "Тэги",
|
||||||
|
"topics.export.obsidian_tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры.",
|
||||||
|
"topics.export.obsidian_created": "Дата создания",
|
||||||
|
"topics.export.obsidian_created_placeholder": "Пожалуйста, выберите дату создания",
|
||||||
|
"topics.export.obsidian_source": "Источник",
|
||||||
|
"topics.export.obsidian_source_placeholder": "Пожалуйста, введите источник",
|
||||||
|
"topics.export.obsidian_operate": "Метод обработки",
|
||||||
|
"topics.export.obsidian_operate_placeholder": "Пожалуйста, выберите метод обработки",
|
||||||
|
"topics.export.obsidian_operate_append": "Добавить в конец",
|
||||||
|
"topics.export.obsidian_operate_prepend": "Добавить в начало",
|
||||||
|
"topics.export.obsidian_operate_new_or_overwrite": "Создать новый (перезаписать, если уже существует)",
|
||||||
|
"topics.export.obsidian_atributes": "Настроить атрибуты заметки",
|
||||||
|
"topics.export.obsidian_btn": "Подтвердить",
|
||||||
"topics.export.obsidian_export_success": "Экспорт успешно завершен",
|
"topics.export.obsidian_export_success": "Экспорт успешно завершен",
|
||||||
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
"topics.export.obsidian_export_failed": "Экспорт не удалось",
|
||||||
"topics.export.obsidian_show_md_files": "Показать файлы MD",
|
|
||||||
"topics.export.obsidian_selected_path": "Выбранный путь",
|
|
||||||
"topics.export.joplin": "Экспорт в Joplin",
|
"topics.export.joplin": "Экспорт в Joplin",
|
||||||
"topics.list": "Список топиков",
|
"topics.list": "Список топиков",
|
||||||
"topics.move_to": "Переместить в",
|
"topics.move_to": "Переместить в",
|
||||||
@ -792,19 +803,13 @@
|
|||||||
"token_placeholder": "Введите токен Yuque"
|
"token_placeholder": "Введите токен Yuque"
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"check": {
|
"title": "Конфигурация Obsidian",
|
||||||
"button": "Проверить",
|
"vault": "Хранилище",
|
||||||
"empty_url": "Сначала введите URL REST API Obsidian",
|
"vault_placeholder": "Пожалуйста, введите имя хранилища",
|
||||||
"empty_api_key": "Сначала введите API Key Obsidian",
|
"folder": "Папка",
|
||||||
"fail": "Не удалось проверить подключение к Obsidian",
|
"folder_placeholder": "Пожалуйста, введите имя папки",
|
||||||
"success": "Подключение к Obsidian успешно проверено"
|
"tags": "Глобальные Теги",
|
||||||
},
|
"tags_placeholder": "Пожалуйста, введите имена тегов. Разделяйте несколько тегов запятыми на английском языке. В Obsidian нельзя использовать только цифры."
|
||||||
"help": "Сначала установите плагин Local REST API Obsidian, затем получите API Key Obsidian",
|
|
||||||
"url": "URL базы знаний Obsidian",
|
|
||||||
"url_placeholder": "http://127.0.0.1:27123/",
|
|
||||||
"title": "Настройка Obsidian",
|
|
||||||
"api_key": "API Key Obsidian",
|
|
||||||
"api_key_placeholder": "Введите API Key Obsidian"
|
|
||||||
},
|
},
|
||||||
"joplin": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -164,13 +164,24 @@
|
|||||||
"topics.export.yuque": "导出到语雀",
|
"topics.export.yuque": "导出到语雀",
|
||||||
"topics.export.obsidian": "导出到 Obsidian",
|
"topics.export.obsidian": "导出到 Obsidian",
|
||||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||||
"topics.export.obsidian_fetch_failed": "获取 Obsidian 文件夹结构失败",
|
"topics.export.obsidian_title":"标题",
|
||||||
"topics.export.obsidian_select_folder": "选择 Obsidian 文件夹",
|
"topics.export.obsidian_title_placeholder":"请输入标题",
|
||||||
"topics.export.obsidian_select_folder.btn": "确定",
|
"topics.export.obsidian_title_required":"标题不能为空",
|
||||||
|
"topics.export.obsidian_tags":"标签",
|
||||||
|
"topics.export.obsidian_tags_placeholder":"请输入标签,多个标签用英文逗号分隔,Obsidian不可用纯数字",
|
||||||
|
"topics.export.obsidian_created":"创建时间",
|
||||||
|
"topics.export.obsidian_created_placeholder":"请选择创建时间",
|
||||||
|
"topics.export.obsidian_source":"来源",
|
||||||
|
"topics.export.obsidian_source_placeholder":"请输入来源",
|
||||||
|
"topics.export.obsidian_operate":"处理方式",
|
||||||
|
"topics.export.obsidian_operate_placeholder":"请选择处理方式",
|
||||||
|
"topics.export.obsidian_operate_append":"追加",
|
||||||
|
"topics.export.obsidian_operate_prepend":"前置",
|
||||||
|
"topics.export.obsidian_operate_new_or_overwrite":"新建(如果存在就覆盖)",
|
||||||
|
"topics.export.obsidian_atributes": "配置笔记属性",
|
||||||
|
"topics.export.obsidian_btn": "确定",
|
||||||
"topics.export.obsidian_export_success": "导出成功",
|
"topics.export.obsidian_export_success": "导出成功",
|
||||||
"topics.export.obsidian_export_failed": "导出失败",
|
"topics.export.obsidian_export_failed": "导出失败",
|
||||||
"topics.export.obsidian_show_md_files": "显示md文件",
|
|
||||||
"topics.export.obsidian_selected_path": "已选择路径",
|
|
||||||
"topics.export.joplin": "导出到 Joplin",
|
"topics.export.joplin": "导出到 Joplin",
|
||||||
"topics.list": "话题列表",
|
"topics.list": "话题列表",
|
||||||
"topics.move_to": "移动到",
|
"topics.move_to": "移动到",
|
||||||
@ -792,19 +803,13 @@
|
|||||||
"token_placeholder": "请输入语雀Token"
|
"token_placeholder": "请输入语雀Token"
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"check": {
|
|
||||||
"button": "检查",
|
|
||||||
"empty_url": "请先输入 Obsidian REST API URL",
|
|
||||||
"empty_api_key": "请先输入 Obsidian API Key",
|
|
||||||
"fail": "Obsidian 连接验证失败",
|
|
||||||
"success": "Obsidian 连接验证成功"
|
|
||||||
},
|
|
||||||
"help": "先安装 Obsidian 插件 Local REST API,然后获取 Obsidian API Key",
|
|
||||||
"url": "Obsidian 知识库 URL",
|
|
||||||
"url_placeholder": "http://127.0.0.1:27123/",
|
|
||||||
"title": "Obsidian 配置",
|
"title": "Obsidian 配置",
|
||||||
"api_key": "Obsidian API Key",
|
"vault": "保管库",
|
||||||
"api_key_placeholder": "请输入 Obsidian API Key"
|
"vault_placeholder": "请输入保管库名称",
|
||||||
|
"folder": "文件夹",
|
||||||
|
"folder_placeholder": "请输入文件夹名称",
|
||||||
|
"tags": "全局标签",
|
||||||
|
"tags_placeholder": "请输入标签名称, 多个标签用英文逗号分隔,Obsidian不可用纯数字"
|
||||||
},
|
},
|
||||||
"joplin": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -164,10 +164,24 @@
|
|||||||
"topics.export.yuque": "匯出到語雀",
|
"topics.export.yuque": "匯出到語雀",
|
||||||
"topics.export.obsidian": "匯出到 Obsidian",
|
"topics.export.obsidian": "匯出到 Obsidian",
|
||||||
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
"topics.export.obsidian_not_configured": "Obsidian 未配置",
|
||||||
"topics.export.obsidian_fetch_failed": "獲取 Obsidian 文件夾結構失敗",
|
"topics.export.obsidian_title":"標題",
|
||||||
"topics.export.obsidian_select_folder": "選擇 Obsidian 文件夾",
|
"topics.export.obsidian_title_placeholder": "請輸入標題",
|
||||||
"topics.export.obsidian_select_folder.btn": "確定",
|
"topics.export.obsidian_title_required": "標題不能為空",
|
||||||
|
"topics.export.obsidian_tags": "標籤",
|
||||||
|
"topics.export.obsidian_tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。",
|
||||||
|
"topics.export.obsidian_created": "建立時間",
|
||||||
|
"topics.export.obsidian_created_placeholder": "請選擇建立時間",
|
||||||
|
"topics.export.obsidian_source": "來源",
|
||||||
|
"topics.export.obsidian_source_placeholder": "請輸入來源",
|
||||||
|
"topics.export.obsidian_operate": "處理方式",
|
||||||
|
"topics.export.obsidian_operate_placeholder": "請選擇處理方式",
|
||||||
|
"topics.export.obsidian_operate_append": "追加",
|
||||||
|
"topics.export.obsidian_operate_prepend": "前置",
|
||||||
|
"topics.export.obsidian_operate_new_or_overwrite": "新建(如果存在就覆蓋)",
|
||||||
|
"topics.export.obsidian_atributes": "配置筆記屬性",
|
||||||
|
"topics.export.obsidian_btn": "確定",
|
||||||
"topics.export.obsidian_export_success": "匯出成功",
|
"topics.export.obsidian_export_success": "匯出成功",
|
||||||
|
"topics.export.obsidian_export_failed": "匯出失败",
|
||||||
"topics.export.obsidian_export_failed": "匯出失敗",
|
"topics.export.obsidian_export_failed": "匯出失敗",
|
||||||
"topics.export.obsidian_show_md_files": "顯示md文件",
|
"topics.export.obsidian_show_md_files": "顯示md文件",
|
||||||
"topics.export.obsidian_selected_path": "已選擇路徑",
|
"topics.export.obsidian_selected_path": "已選擇路徑",
|
||||||
@ -792,19 +806,13 @@
|
|||||||
"token_placeholder": "請輸入語雀 Token"
|
"token_placeholder": "請輸入語雀 Token"
|
||||||
},
|
},
|
||||||
"obsidian": {
|
"obsidian": {
|
||||||
"check": {
|
|
||||||
"button": "檢查",
|
|
||||||
"empty_url": "請先輸入 Obsidian REST API URL",
|
|
||||||
"empty_api_key": "請先輸入 Obsidian API Key",
|
|
||||||
"fail": "Obsidian 連接驗證失敗",
|
|
||||||
"success": "Obsidian 連接驗證成功"
|
|
||||||
},
|
|
||||||
"help": "先安裝 Obsidian 插件 Local REST API,然後獲取 Obsidian API Key",
|
|
||||||
"url": "Obsidian 知識庫 URL",
|
|
||||||
"url_placeholder": "http://127.0.0.1:27123/",
|
|
||||||
"title": "Obsidian 設定",
|
"title": "Obsidian 設定",
|
||||||
"api_key": "Obsidian API Key",
|
"vault": "保險庫",
|
||||||
"api_key_placeholder": "請輸入 Obsidian API Key"
|
"vault_placeholder": "請輸入保險庫名稱",
|
||||||
|
"folder": "資料夾",
|
||||||
|
"folder_placeholder": "請輸入資料夾名稱",
|
||||||
|
"tags": "全域標籤",
|
||||||
|
"tags_placeholder": "請輸入標籤名稱,多個標籤用英文逗號分隔。Obsidian 不可用純數字。"
|
||||||
},
|
},
|
||||||
"joplin": {
|
"joplin": {
|
||||||
"check": {
|
"check": {
|
||||||
|
|||||||
@ -242,8 +242,8 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
key: 'obsidian',
|
key: 'obsidian',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const markdown = messageToMarkdown(message)
|
const markdown = messageToMarkdown(message)
|
||||||
const title = getMessageTitle(message)
|
const title = topic.name?.replace(/\//g, '_') || 'Untitled'
|
||||||
await ObsidianExportPopup.show({ title, markdown })
|
await ObsidianExportPopup.show({ title, markdown, processingMethod: '1' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -262,7 +262,7 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
|
|||||||
key: 'obsidian',
|
key: 'obsidian',
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const markdown = await topicToMarkdown(topic)
|
const markdown = await topicToMarkdown(topic)
|
||||||
await ObsidianExportPopup.show({ title: topic.name, markdown })
|
await ObsidianExportPopup.show({ title: topic.name, markdown, processingMethod: '3' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import MinApp from '@renderer/components/MinApp'
|
|
||||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||||
import { RootState, useAppDispatch } from '@renderer/store'
|
import { RootState, useAppDispatch } from '@renderer/store'
|
||||||
import { setObsidianApiKey, setObsidianUrl } from '@renderer/store/settings'
|
import { setObsidianFolder, setObsidianTages, setObsidianValut } from '@renderer/store/settings'
|
||||||
import { Button, Tooltip } from 'antd'
|
|
||||||
import Input from 'antd/es/input/Input'
|
import Input from 'antd/es/input/Input'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@ -17,62 +14,35 @@ const ObsidianSettings: FC = () => {
|
|||||||
const { theme } = useTheme()
|
const { theme } = useTheme()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey)
|
// const obsidianApiKey = useSelector((state: RootState) => state.settings.obsidianApiKey)
|
||||||
const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl)
|
// const obsidianUrl = useSelector((state: RootState) => state.settings.obsidianUrl)
|
||||||
|
|
||||||
const handleObsidianApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const obsidianVault = useSelector((state: RootState) => state.settings.obsidianValut)
|
||||||
dispatch(setObsidianApiKey(e.target.value))
|
const obsidianFolder = useSelector((state: RootState) => state.settings.obsidianFolder)
|
||||||
|
const obsidianTags = useSelector((state: RootState) => state.settings.obsidianTages)
|
||||||
|
|
||||||
|
const handleObsidianVaultChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(setObsidianValut(e.target.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleObsidianUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleObsidianFolderChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
dispatch(setObsidianUrl(e.target.value))
|
dispatch(setObsidianFolder(e.target.value))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleObsidianUrlBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
const handleObsidianVaultBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
let url = e.target.value
|
dispatch(setObsidianValut(e.target.value))
|
||||||
// 确保URL以/结尾,但只在失去焦点时执行
|
|
||||||
if (url && !url.endsWith('/')) {
|
|
||||||
url = `${url}/`
|
|
||||||
dispatch(setObsidianUrl(url))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleObsidianConnectionCheck = async () => {
|
const handleObsidianFolderBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
try {
|
dispatch(setObsidianFolder(e.target.value))
|
||||||
if (!obsidianApiKey) {
|
|
||||||
window.message.error(t('settings.data.obsidian.check.empty_api_key'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!obsidianUrl) {
|
|
||||||
window.message.error(t('settings.data.obsidian.check.empty_url'))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${obsidianUrl}`, {
|
const handleObsidianTagsChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
headers: {
|
dispatch(setObsidianTages(e.target.value))
|
||||||
Authorization: `Bearer ${obsidianApiKey}`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok || !data?.authenticated) {
|
|
||||||
window.message.error(t('settings.data.obsidian.check.fail'))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.message.success(t('settings.data.obsidian.check.success'))
|
const handleObsidianTagsBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||||
} catch (e) {
|
dispatch(setObsidianTages(e.target.value))
|
||||||
window.message.error(t('settings.data.obsidian.check.fail'))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleObsidianHelpClick = () => {
|
|
||||||
MinApp.start({
|
|
||||||
id: 'obsidian-help',
|
|
||||||
name: 'Obsidian Help',
|
|
||||||
url: 'https://github.com/coddingtonbear/obsidian-local-rest-api'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -80,38 +50,46 @@ const ObsidianSettings: FC = () => {
|
|||||||
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
|
<SettingTitle>{t('settings.data.obsidian.title')}</SettingTitle>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle>{t('settings.data.obsidian.url')}</SettingRowTitle>
|
<SettingRowTitle>{t('settings.data.obsidian.vault')}</SettingRowTitle>
|
||||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
value={obsidianUrl || ''}
|
value={obsidianVault || ''}
|
||||||
onChange={handleObsidianUrlChange}
|
onChange={handleObsidianVaultChange}
|
||||||
onBlur={handleObsidianUrlBlur}
|
onBlur={handleObsidianVaultBlur}
|
||||||
style={{ width: 315 }}
|
style={{ width: 315 }}
|
||||||
placeholder={t('settings.data.obsidian.url_placeholder')}
|
placeholder={t('settings.data.obsidian.vault_placeholder')}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<SettingDivider />
|
||||||
<SettingRow>
|
<SettingRow>
|
||||||
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
<SettingRowTitle style={{ display: 'flex', alignItems: 'center' }}>
|
||||||
<span>{t('settings.data.obsidian.api_key')}</span>
|
<span>{t('settings.data.obsidian.folder')}</span>
|
||||||
<Tooltip title={t('settings.data.obsidian.help')} placement="left">
|
|
||||||
<InfoCircleOutlined
|
|
||||||
style={{ color: 'var(--color-text-2)', cursor: 'pointer', marginLeft: 4 }}
|
|
||||||
onClick={handleObsidianHelpClick}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</SettingRowTitle>
|
</SettingRowTitle>
|
||||||
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
<HStack alignItems="center" gap="5px" style={{ width: 315 }}>
|
||||||
<Input
|
<Input
|
||||||
type="password"
|
value={obsidianFolder || ''}
|
||||||
value={obsidianApiKey || ''}
|
onChange={handleObsidianFolderChange}
|
||||||
onChange={handleObsidianApiKeyChange}
|
onBlur={handleObsidianFolderBlur}
|
||||||
style={{ width: 250 }}
|
style={{ width: 315 }}
|
||||||
placeholder={t('settings.data.obsidian.api_key_placeholder')}
|
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')}
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleObsidianConnectionCheck}>{t('settings.data.obsidian.check.button')}</Button>
|
|
||||||
</HStack>
|
</HStack>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
</SettingGroup>
|
</SettingGroup>
|
||||||
|
|||||||
@ -86,8 +86,10 @@ export interface SettingsState {
|
|||||||
yuqueToken: string | null
|
yuqueToken: string | null
|
||||||
yuqueUrl: string | null
|
yuqueUrl: string | null
|
||||||
yuqueRepoId: string | null
|
yuqueRepoId: string | null
|
||||||
obsidianApiKey: string | null
|
//obsidian settings obsidianVault, obisidanFolder
|
||||||
obsidianUrl: string | null
|
obsidianValut: string | null
|
||||||
|
obsidianFolder: string | null
|
||||||
|
obsidianTages: string | null
|
||||||
joplinToken: string | null
|
joplinToken: string | null
|
||||||
joplinUrl: string | null
|
joplinUrl: string | null
|
||||||
}
|
}
|
||||||
@ -161,10 +163,12 @@ const initialState: SettingsState = {
|
|||||||
yuqueToken: '',
|
yuqueToken: '',
|
||||||
yuqueUrl: '',
|
yuqueUrl: '',
|
||||||
yuqueRepoId: '',
|
yuqueRepoId: '',
|
||||||
obsidianApiKey: '',
|
obsidianValut: '',
|
||||||
obsidianUrl: '',
|
obsidianFolder: '',
|
||||||
|
obsidianTages: '',
|
||||||
joplinToken: '',
|
joplinToken: '',
|
||||||
joplinUrl: ''
|
joplinUrl: ''
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@ -369,11 +373,14 @@ const settingsSlice = createSlice({
|
|||||||
setYuqueUrl: (state, action: PayloadAction<string>) => {
|
setYuqueUrl: (state, action: PayloadAction<string>) => {
|
||||||
state.yuqueUrl = action.payload
|
state.yuqueUrl = action.payload
|
||||||
},
|
},
|
||||||
setObsidianApiKey: (state, action: PayloadAction<string>) => {
|
setObsidianValut: (state, action: PayloadAction<string>) => {
|
||||||
state.obsidianApiKey = action.payload
|
state.obsidianValut = action.payload
|
||||||
},
|
},
|
||||||
setObsidianUrl: (state, action: PayloadAction<string>) => {
|
setObsidianFolder: (state, action: PayloadAction<string>) => {
|
||||||
state.obsidianUrl = action.payload
|
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
|
||||||
@ -452,8 +459,9 @@ export const {
|
|||||||
setYuqueToken,
|
setYuqueToken,
|
||||||
setYuqueRepoId,
|
setYuqueRepoId,
|
||||||
setYuqueUrl,
|
setYuqueUrl,
|
||||||
setObsidianApiKey,
|
setObsidianValut,
|
||||||
setObsidianUrl,
|
setObsidianFolder,
|
||||||
|
setObsidianTages,
|
||||||
setJoplinToken,
|
setJoplinToken,
|
||||||
setJoplinUrl,
|
setJoplinUrl,
|
||||||
setMessageNavigation
|
setMessageNavigation
|
||||||
|
|||||||
@ -320,58 +320,46 @@ export const exportMarkdownToYuque = async (title: string, content: string) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出Markdown到Obsidian
|
* 导出Markdown到Obsidian
|
||||||
|
* @param attributes 文档属性
|
||||||
|
* @param attributes.title 标题
|
||||||
|
* @param attributes.created 创建时间
|
||||||
|
* @param attributes.source 来源
|
||||||
|
* @param attributes.tags 标签
|
||||||
|
* @param attributes.processingMethod 处理方式
|
||||||
*/
|
*/
|
||||||
export const exportMarkdownToObsidian = async (
|
export const exportMarkdownToObsidian = async (attributes: any) => {
|
||||||
fileName: string,
|
|
||||||
markdown: string,
|
|
||||||
selectedPath: string,
|
|
||||||
isMdFile: boolean = false
|
|
||||||
) => {
|
|
||||||
try {
|
try {
|
||||||
const obsidianUrl = store.getState().settings.obsidianUrl
|
const obsidianValut = store.getState().settings.obsidianValut
|
||||||
const obsidianApiKey = store.getState().settings.obsidianApiKey
|
const obsidianFolder = store.getState().settings.obsidianFolder
|
||||||
|
|
||||||
if (!obsidianUrl || !obsidianApiKey) {
|
if (!obsidianValut || !obsidianFolder) {
|
||||||
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 = ''
|
||||||
|
|
||||||
// 如果是md文件,直接将内容追加到该文件
|
if (!attributes.title) {
|
||||||
if (isMdFile) {
|
window.message.error(i18n.t('chat.topics.export.obsidian_title_required'))
|
||||||
const response = await fetch(`${obsidianUrl}vault${selectedPath}`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'text/markdown',
|
|
||||||
Authorization: `Bearer ${obsidianApiKey}`
|
|
||||||
},
|
|
||||||
body: `\n\n${markdown}` // 添加两个换行后追加内容
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// 创建新文件
|
|
||||||
const sanitizedFileName = removeSpecialCharactersForFileName(fileName)
|
|
||||||
const path = selectedPath === '/' ? '' : selectedPath
|
|
||||||
const fullPath = path.endsWith('/') ? `${path}${sanitizedFileName}.md` : `${path}/${sanitizedFileName}.md`
|
|
||||||
|
|
||||||
const response = await fetch(`${obsidianUrl}vault${fullPath}`, {
|
//构建保存路径添加以 / 结尾
|
||||||
method: 'PUT',
|
if (!obsidianFolder.endsWith('/')) {
|
||||||
headers: {
|
path = obsidianFolder + '/'
|
||||||
'Content-Type': 'text/markdown',
|
|
||||||
Authorization: `Bearer ${obsidianApiKey}`
|
|
||||||
},
|
|
||||||
body: markdown
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
window.message.error(i18n.t('chat.topics.export.obsidian_export_failed'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//构建文件名
|
||||||
|
const fileName = transformObsidianFileName(attributes.title)
|
||||||
|
|
||||||
|
let obsidianUrl = `obsidian://new?file=${encodeURIComponent(path + fileName)}&vault=${encodeURIComponent(obsidianValut)}&clipboard`
|
||||||
|
|
||||||
|
if (attributes.processingMethod === '3') {
|
||||||
|
obsidianUrl += '&overwrite=true'
|
||||||
|
} else if (attributes.processingMethod === '2') {
|
||||||
|
obsidianUrl += '&prepend=true'
|
||||||
|
} else if (attributes.processingMethod === '1') {
|
||||||
|
obsidianUrl += '&append=true'
|
||||||
|
}
|
||||||
|
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) {
|
||||||
console.error('导出到Obsidian失败:', error)
|
console.error('导出到Obsidian失败:', error)
|
||||||
@ -379,6 +367,51 @@ export const exportMarkdownToObsidian = async (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成Obsidian文件名,源自 Obsidian Web Clipper 官方实现,修改了一些细节
|
||||||
|
* @param fileName
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
|
||||||
|
function transformObsidianFileName(fileName: string): string {
|
||||||
|
const platform = window.navigator.userAgent
|
||||||
|
const isWindows = /win/i.test(platform)
|
||||||
|
const isMac = /mac/i.test(platform)
|
||||||
|
|
||||||
|
// 删除Obsidian 全平台无效字符
|
||||||
|
let sanitized = fileName.replace(/[#|\\^\\[\]]/g, '')
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
// Windows 的清理
|
||||||
|
sanitized = sanitized
|
||||||
|
.replace(/[<>:"\\/\\|?*]/g, '') // 移除无效字符
|
||||||
|
.replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\..*)?$/i, '_$1$2') // 避免保留名称
|
||||||
|
.replace(/[\s.]+$/, '') // 移除结尾的空格和句点
|
||||||
|
} else if (isMac) {
|
||||||
|
// Mac 的清理
|
||||||
|
sanitized = sanitized
|
||||||
|
.replace(/[/:\u0020-\u007E]/g, '') // 移除无效字符
|
||||||
|
.replace(/^\./, '_') // 避免以句点开头
|
||||||
|
} else {
|
||||||
|
// Linux 或其他系统
|
||||||
|
sanitized = sanitized
|
||||||
|
.replace(/[<>:"\\/\\|?*]/g, '') // 移除无效字符
|
||||||
|
.replace(/^\./, '_') // 避免以句点开头
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有平台的通用操作
|
||||||
|
sanitized = sanitized
|
||||||
|
.replace(/^\.+/, '') // 移除开头的句点
|
||||||
|
.trim() // 移除前后空格
|
||||||
|
.slice(0, 245) // 截断为 245 个字符,留出空间以追加 ' 1.md'
|
||||||
|
|
||||||
|
// 确保文件名不为空
|
||||||
|
if (sanitized.length === 0) {
|
||||||
|
sanitized = 'Untitled'
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized
|
||||||
|
}
|
||||||
export const exportMarkdownToJoplin = async (title: string, content: string) => {
|
export const exportMarkdownToJoplin = async (title: string, content: string) => {
|
||||||
const { joplinUrl, joplinToken } = store.getState().settings
|
const { joplinUrl, joplinToken } = store.getState().settings
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user