- Replace text input with native dialog.showOpenDialog for vault path selection - Add path validity detection with three-state UI (unselected/valid/invalid) - Fix YAML frontmatter parsing failure with Chinese/special characters - Fix ENOENT error when vault subdirectory doesn't exist - Add AI summary (description) to Obsidian frontmatter export - Add author fallback to feed title when entry author is empty - Increase filename truncation limit from 20 to 80 characters for CJK titles Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
037a912a9b
commit
bd879d5f9e
|
|
@ -285,4 +285,23 @@ export class AppService extends IpcService {
|
|||
getCacheSize(_context: IpcContext) {
|
||||
return getCacheSize()
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async selectDirectory(_context: IpcContext): Promise<string | null> {
|
||||
const result = await dialog.showOpenDialog({
|
||||
properties: ["openDirectory"],
|
||||
})
|
||||
if (result.canceled || result.filePaths.length === 0) return null
|
||||
return result.filePaths[0]!
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async checkPathExists(_context: IpcContext, input: string): Promise<boolean> {
|
||||
try {
|
||||
await fsp.access(input)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,24 +66,29 @@ export class IntegrationService extends IpcService {
|
|||
author: string
|
||||
publishedAt: string
|
||||
vaultPath: string
|
||||
description?: string
|
||||
},
|
||||
) {
|
||||
try {
|
||||
const { url, title, content, author, publishedAt, vaultPath } = input
|
||||
const { url, title, content, author, publishedAt, vaultPath, description } = input
|
||||
|
||||
const fileName = `${sanitizeFileName(title || publishedAt)
|
||||
.trim()
|
||||
.slice(0, 20)}.md`
|
||||
.slice(0, 80)}.md`
|
||||
const filePath = path.join(vaultPath, fileName)
|
||||
const exists = existsSync(filePath)
|
||||
if (exists) {
|
||||
return { success: false, error: "File already exists" }
|
||||
}
|
||||
|
||||
await fsp.mkdir(path.dirname(filePath), { recursive: true })
|
||||
|
||||
const yamlEscape = (s: string) => `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
|
||||
|
||||
const markdown = `---
|
||||
url: ${url}
|
||||
author: ${author}
|
||||
publishedAt: ${publishedAt}
|
||||
url: ${yamlEscape(url)}
|
||||
author: ${yamlEscape(author)}
|
||||
publishedAt: ${yamlEscape(publishedAt)}${description ? `\ndescription: ${yamlEscape(description)}` : ""}
|
||||
---
|
||||
|
||||
# ${title}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { getEntry } from "@follow/store/entry/getter"
|
||||
import type { EntryModel } from "@follow/store/entry/types"
|
||||
import { getFeedById } from "@follow/store/feed/getter"
|
||||
import { getSummary } from "@follow/store/summary/getters"
|
||||
import { tracker } from "@follow/tracker"
|
||||
import { useMutation, useQuery } from "@tanstack/react-query"
|
||||
|
|
@ -321,9 +322,10 @@ const useRegisterObsidianCommands = () => {
|
|||
url: entry.url || "",
|
||||
title: entry.title || "",
|
||||
content: markdownContent,
|
||||
author: entry.author || "",
|
||||
author: entry.author || getFeedById(entry.feedId)?.title || "",
|
||||
publishedAt: entry.publishedAt.toISOString() || "",
|
||||
vaultPath: obsidianVaultPath,
|
||||
description: getDescription(entry),
|
||||
})
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import {
|
|||
SimpleIconsZotero,
|
||||
} from "@follow/components/ui/platform-icon/icons.js"
|
||||
import { IN_ELECTRON } from "@follow/shared/constants"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import type { FC } from "react"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { toast } from "sonner"
|
||||
|
||||
|
|
@ -22,12 +23,13 @@ import {
|
|||
setIntegrationSetting,
|
||||
useIntegrationSettingValue,
|
||||
} from "~/atoms/settings/integration"
|
||||
import { ipcServices } from "~/lib/client"
|
||||
import { downloadJsonFile, selectJsonFile } from "~/lib/export"
|
||||
import { getFetchAdapter } from "~/modules/integration/fetch-adapter"
|
||||
|
||||
import { createSetting } from "../../helper/builder"
|
||||
import { useSetSettingCanSync } from "../../modal/hooks"
|
||||
import { SettingSectionTitle } from "../../section"
|
||||
import { SettingItemGroup, SettingSectionTitle } from "../../section"
|
||||
import { CustomIntegrationSection } from "./CustomIntegrationSection"
|
||||
|
||||
const { defineSettingItem, SettingBuilder } = createSetting(
|
||||
|
|
@ -35,6 +37,61 @@ const { defineSettingItem, SettingBuilder } = createSetting(
|
|||
useIntegrationSettingValue,
|
||||
setIntegrationSetting,
|
||||
)
|
||||
const ObsidianVaultPathPicker: FC = () => {
|
||||
const vaultPath = useIntegrationSettingValue().obsidianVaultPath
|
||||
const { t } = useTranslation("settings")
|
||||
const [pathValid, setPathValid] = useState<boolean | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!vaultPath) {
|
||||
setPathValid(null)
|
||||
return
|
||||
}
|
||||
ipcServices?.app.checkPathExists(vaultPath).then((exists) => {
|
||||
setPathValid(exists)
|
||||
})
|
||||
}, [vaultPath])
|
||||
|
||||
const handleBrowse = async () => {
|
||||
const selected = await ipcServices?.app.selectDirectory()
|
||||
if (selected) {
|
||||
setIntegrationSetting("obsidianVaultPath", selected)
|
||||
}
|
||||
}
|
||||
|
||||
const buttonText = !vaultPath
|
||||
? t("integration.obsidian.vaultPath.select")
|
||||
: pathValid === false
|
||||
? t("integration.obsidian.vaultPath.reselect")
|
||||
: t("integration.obsidian.vaultPath.change")
|
||||
|
||||
return (
|
||||
<SettingItemGroup>
|
||||
<div className="mb-2 mt-4 flex flex-col gap-3">
|
||||
<label className="shrink-0 text-sm font-medium leading-none">
|
||||
{t("integration.obsidian.vaultPath.label")}
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="outline" size="sm" onClick={handleBrowse}>
|
||||
{buttonText}
|
||||
</Button>
|
||||
{vaultPath && (
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
<span className="min-w-0 truncate text-xs text-text-secondary">{vaultPath}</span>
|
||||
{pathValid === false && (
|
||||
<span className="inline-flex shrink-0 items-center gap-1 text-xs text-red">
|
||||
<i className="i-mgc-warning-cute-re" />
|
||||
{t("integration.obsidian.vaultPath.invalid")}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingItemGroup>
|
||||
)
|
||||
}
|
||||
|
||||
export const SettingIntegration = () => {
|
||||
const { t } = useTranslation("settings")
|
||||
const setSync = useSetSettingCanSync()
|
||||
|
|
@ -100,11 +157,7 @@ export const SettingIntegration = () => {
|
|||
label: t("integration.obsidian.enable.label"),
|
||||
description: t("integration.obsidian.enable.description"),
|
||||
}),
|
||||
defineSettingItem("obsidianVaultPath", {
|
||||
label: t("integration.obsidian.vaultPath.label"),
|
||||
vertical: true,
|
||||
description: t("integration.obsidian.vaultPath.description"),
|
||||
}),
|
||||
ObsidianVaultPathPicker,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -472,8 +472,12 @@
|
|||
"integration.obsidian.enable.description": "Display 'Save to Obsidian' button when available.",
|
||||
"integration.obsidian.enable.label": "Enable",
|
||||
"integration.obsidian.title": "Obsidian",
|
||||
"integration.obsidian.vaultPath.change": "Change",
|
||||
"integration.obsidian.vaultPath.description": "The path to your Obsidian vault.",
|
||||
"integration.obsidian.vaultPath.invalid": "Path not available",
|
||||
"integration.obsidian.vaultPath.label": "Obsidian Vault Path",
|
||||
"integration.obsidian.vaultPath.reselect": "Reselect",
|
||||
"integration.obsidian.vaultPath.select": "Select Folder",
|
||||
"integration.outline.collection.description": "The UUID or urlId of the collection where the documents is saved.",
|
||||
"integration.outline.collection.label": "Outline Collection",
|
||||
"integration.outline.enable.description": "Display 'Save to Outline' button when available.",
|
||||
|
|
|
|||
|
|
@ -472,8 +472,12 @@
|
|||
"integration.obsidian.enable.description": "显示「保存到 Obsidian」按钮(如果可用)。",
|
||||
"integration.obsidian.enable.label": "启用",
|
||||
"integration.obsidian.title": "Obsidian",
|
||||
"integration.obsidian.vaultPath.change": "更改",
|
||||
"integration.obsidian.vaultPath.description": "你的 Obsidian 仓库的路径。",
|
||||
"integration.obsidian.vaultPath.invalid": "路径不可用",
|
||||
"integration.obsidian.vaultPath.label": "Obsidian 仓库路径",
|
||||
"integration.obsidian.vaultPath.reselect": "重新选择",
|
||||
"integration.obsidian.vaultPath.select": "选择文件夹",
|
||||
"integration.outline.collection.description": "保存文档的文档集的 UUID 或 urlId。",
|
||||
"integration.outline.collection.label": "Outline 文档集",
|
||||
"integration.outline.enable.description": "显示「保存到 Outline」按钮(如果可用)。",
|
||||
|
|
|
|||
Loading…
Reference in New Issue