(null)
+ const [canScroll, setCanScroll] = useState(false)
+ const [isExpanded, setIsExpanded] = useState(false)
+ const [isScrolledToEnd, setIsScrolledToEnd] = useState(false)
+
+ const handleScrollRight = (event: React.MouseEvent) => {
+ scrollRef.current?.scrollBy({ left: scrollDistance, behavior: 'smooth' })
+ event.stopPropagation()
+ }
+
+ const handleContainerClick = (e: React.MouseEvent) => {
+ if (expandable) {
+ // 确保不是点击了其他交互元素(如 tag 的关闭按钮)
+ const target = e.target as HTMLElement
+ if (!target.closest('[data-no-expand]')) {
+ setIsExpanded(!isExpanded)
+ }
+ }
+ }
+
+ const checkScrollability = () => {
+ const scrollElement = scrollRef.current
+ if (scrollElement) {
+ const parentElement = scrollElement.parentElement
+ const availableWidth = parentElement ? parentElement.clientWidth : scrollElement.clientWidth
+
+ // 确保容器不会超出可用宽度
+ const canScrollValue = scrollElement.scrollWidth > Math.min(availableWidth, scrollElement.clientWidth)
+ setCanScroll(canScrollValue)
+
+ // 检查是否滚动到最右侧
+ if (canScrollValue) {
+ const isAtEnd = Math.abs(scrollElement.scrollLeft + scrollElement.clientWidth - scrollElement.scrollWidth) <= 1
+ setIsScrolledToEnd(isAtEnd)
+ } else {
+ setIsScrolledToEnd(false)
+ }
+ }
+ }
+
+ useEffect(() => {
+ const scrollElement = scrollRef.current
+ if (!scrollElement) return
+
+ checkScrollability()
+
+ const handleScroll = () => {
+ checkScrollability()
+ }
+
+ const resizeObserver = new ResizeObserver(checkScrollability)
+ resizeObserver.observe(scrollElement)
+
+ scrollElement.addEventListener('scroll', handleScroll)
+ window.addEventListener('resize', checkScrollability)
+
+ return () => {
+ resizeObserver.disconnect()
+ scrollElement.removeEventListener('scroll', handleScroll)
+ window.removeEventListener('resize', checkScrollability)
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, dependencies)
+
+ return (
+
+
+ {children}
+
+ {canScroll && !isExpanded && !isScrolledToEnd && (
+
+
+
+ )}
+
+ )
+}
+
+const Container = styled.div<{ $expandable?: boolean; $disableHoverButton?: boolean }>`
+ display: flex;
+ align-items: center;
+ flex: 1 1 auto;
+ min-width: 0;
+ max-width: 100%;
+ position: relative;
+ cursor: ${(props) => (props.$expandable ? 'pointer' : 'default')};
+
+ ${(props) =>
+ !props.$disableHoverButton &&
+ `
+ &:hover {
+ .scroll-right-button {
+ opacity: 1;
+ }
+ }
+ `}
+`
+
+const ScrollContent = styled(Scrollbar)<{
+ $gap: string
+ $isExpanded?: boolean
+ $expandable?: boolean
+}>`
+ display: flex;
+ overflow-x: ${(props) => (props.$expandable && props.$isExpanded ? 'hidden' : 'auto')};
+ overflow-y: hidden;
+ white-space: ${(props) => (props.$expandable && props.$isExpanded ? 'normal' : 'nowrap')};
+ gap: ${(props) => props.$gap};
+ flex-wrap: ${(props) => (props.$expandable && props.$isExpanded ? 'wrap' : 'nowrap')};
+
+ &::-webkit-scrollbar {
+ display: none;
+ }
+`
+
+const ScrollButton = styled.div`
+ position: absolute;
+ right: 8px;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 1;
+ opacity: 0;
+ transition: opacity 0.2s ease-in-out;
+ cursor: pointer;
+ background: var(--color-background);
+ border-radius: 50%;
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow:
+ 0 6px 16px 0 rgba(0, 0, 0, 0.08),
+ 0 3px 6px -4px rgba(0, 0, 0, 0.12),
+ 0 9px 28px 8px rgba(0, 0, 0, 0.05);
+ color: var(--color-text-2);
+
+ &:hover {
+ color: var(--color-text);
+ background: var(--color-list-item);
+ }
+`
+
+export default HorizontalScrollContainer
diff --git a/src/renderer/src/components/MinApp/MinApp.tsx b/src/renderer/src/components/MinApp/MinApp.tsx
index 853d37d109..5833ed9b1a 100644
--- a/src/renderer/src/components/MinApp/MinApp.tsx
+++ b/src/renderer/src/components/MinApp/MinApp.tsx
@@ -135,6 +135,7 @@ const Container = styled.div`
align-items: center;
cursor: pointer;
overflow: hidden;
+ min-height: 85px;
`
const IconContainer = styled.div`
diff --git a/src/renderer/src/components/OGCard.tsx b/src/renderer/src/components/OGCard.tsx
index 8a0036e8e2..93446b6749 100644
--- a/src/renderer/src/components/OGCard.tsx
+++ b/src/renderer/src/components/OGCard.tsx
@@ -1,8 +1,7 @@
-import CherryLogo from '@renderer/assets/images/banner.png'
import Favicon from '@renderer/components/Icons/FallbackFavicon'
import { useMetaDataParser } from '@renderer/hooks/useMetaDataParser'
import { Skeleton, Typography } from 'antd'
-import { useEffect, useMemo } from 'react'
+import { useCallback, useEffect, useMemo } from 'react'
import styled from 'styled-components'
const { Title, Paragraph } = Typography
@@ -11,6 +10,8 @@ type Props = {
show: boolean
}
+const IMAGE_HEIGHT = '9rem' // equals h-36
+
export const OGCard = ({ link, show }: Props) => {
const openGraph = ['og:title', 'og:description', 'og:image', 'og:imageAlt'] as const
const { metadata, isLoading, parseMetadata } = useMetaDataParser(link, openGraph)
@@ -32,6 +33,14 @@ export const OGCard = ({ link, show }: Props) => {
}
}, [parseMetadata, isLoading, show])
+ const GeneratedGraph = useCallback(() => {
+ return (
+
+
{metadata['og:title'] || hostname}
+
+ )
+ }, [hostname, metadata])
+
if (isLoading) {
return
}
@@ -45,7 +54,7 @@ export const OGCard = ({ link, show }: Props) => {
)}
{!hasImage && (
-
+
)}
@@ -113,8 +122,8 @@ const PreviewContainer = styled.div<{ hasImage?: boolean }>`
const PreviewImageContainer = styled.div`
width: 100%;
- height: 140px;
- min-height: 140px;
+ height: ${IMAGE_HEIGHT};
+ min-height: ${IMAGE_HEIGHT};
overflow: hidden;
`
@@ -128,7 +137,7 @@ const PreviewContent = styled.div`
const PreviewImage = styled.img`
width: 100%;
- height: 140px;
+ height: ${IMAGE_HEIGHT};
object-fit: cover;
`
diff --git a/src/renderer/src/components/QuickPanel/provider.tsx b/src/renderer/src/components/QuickPanel/provider.tsx
index c06d337248..57eae70ef2 100644
--- a/src/renderer/src/components/QuickPanel/provider.tsx
+++ b/src/renderer/src/components/QuickPanel/provider.tsx
@@ -32,6 +32,11 @@ export const QuickPanelProvider: React.FC = ({ children
setList((prevList) => prevList.map((item) => (item === targetItem ? { ...item, isSelected } : item)))
}, [])
+ // 添加更新整个列表的方法
+ const updateList = useCallback((newList: QuickPanelListItem[]) => {
+ setList(newList)
+ }, [])
+
const open = useCallback((options: QuickPanelOpenOptions) => {
if (clearTimer.current) {
clearTimeout(clearTimer.current)
@@ -56,7 +61,7 @@ export const QuickPanelProvider: React.FC = ({ children
const close = useCallback(
(action?: QuickPanelCloseAction, searchText?: string) => {
setIsVisible(false)
- onClose?.({ symbol, action, triggerInfo, searchText, item: {} as QuickPanelListItem, multiple: false })
+ onClose?.({ action, searchText, item: {} as QuickPanelListItem, context: this })
clearTimer.current = setTimeout(() => {
setList([])
@@ -68,7 +73,7 @@ export const QuickPanelProvider: React.FC = ({ children
setTriggerInfo(undefined)
}, 200)
},
- [onClose, symbol, triggerInfo]
+ [onClose]
)
useEffect(() => {
@@ -85,6 +90,7 @@ export const QuickPanelProvider: React.FC = ({ children
open,
close,
updateItemSelection,
+ updateList,
isVisible,
symbol,
@@ -103,6 +109,7 @@ export const QuickPanelProvider: React.FC = ({ children
open,
close,
updateItemSelection,
+ updateList,
isVisible,
symbol,
list,
diff --git a/src/renderer/src/components/QuickPanel/types.ts b/src/renderer/src/components/QuickPanel/types.ts
index d8e2ff26b0..97e072dea0 100644
--- a/src/renderer/src/components/QuickPanel/types.ts
+++ b/src/renderer/src/components/QuickPanel/types.ts
@@ -8,13 +8,10 @@ export type QuickPanelTriggerInfo = {
}
export type QuickPanelCallBackOptions = {
- symbol: string
+ context: QuickPanelContextType
action: QuickPanelCloseAction
item: QuickPanelListItem
searchText?: string
- /** 是否处于多选状态 */
- multiple?: boolean
- triggerInfo?: QuickPanelTriggerInfo
}
export type QuickPanelOpenOptions = {
@@ -68,6 +65,7 @@ export interface QuickPanelContextType {
readonly open: (options: QuickPanelOpenOptions) => void
readonly close: (action?: QuickPanelCloseAction, searchText?: string) => void
readonly updateItemSelection: (targetItem: QuickPanelListItem, isSelected: boolean) => void
+ readonly updateList: (newList: QuickPanelListItem[]) => void
readonly isVisible: boolean
readonly symbol: string
readonly list: QuickPanelListItem[]
diff --git a/src/renderer/src/components/QuickPanel/view.tsx b/src/renderer/src/components/QuickPanel/view.tsx
index 08878b8478..30955f96f3 100644
--- a/src/renderer/src/components/QuickPanel/view.tsx
+++ b/src/renderer/src/components/QuickPanel/view.tsx
@@ -222,11 +222,10 @@ export const QuickPanelView: React.FC = ({ setInputText }) => {
// 创建更新后的item对象用于回调
const updatedItem = { ...item, isSelected: newSelectedState }
const quickPanelCallBackOptions: QuickPanelCallBackOptions = {
- symbol: ctx.symbol,
+ context: ctx,
action,
item: updatedItem,
- searchText: searchText,
- multiple: ctx.multiple
+ searchText: searchText
}
ctx.beforeAction?.(quickPanelCallBackOptions)
@@ -236,11 +235,10 @@ export const QuickPanelView: React.FC = ({ setInputText }) => {
}
const quickPanelCallBackOptions: QuickPanelCallBackOptions = {
- symbol: ctx.symbol,
+ context: ctx,
action,
item,
- searchText: searchText,
- multiple: ctx.multiple
+ searchText: searchText
}
ctx.beforeAction?.(quickPanelCallBackOptions)
diff --git a/src/renderer/src/components/Scrollbar/index.tsx b/src/renderer/src/components/Scrollbar/index.tsx
index 60258d8c8b..e50e128d50 100644
--- a/src/renderer/src/components/Scrollbar/index.tsx
+++ b/src/renderer/src/components/Scrollbar/index.tsx
@@ -2,12 +2,12 @@ import { throttle } from 'lodash'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import styled from 'styled-components'
-interface Props extends Omit, 'onScroll'> {
+export interface ScrollbarProps extends Omit, 'onScroll'> {
ref?: React.Ref
onScroll?: () => void // Custom onScroll prop for useScrollPosition's handleScroll
}
-const Scrollbar: FC = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
+const Scrollbar: FC = ({ ref: passedRef, children, onScroll: externalOnScroll, ...htmlProps }) => {
const [isScrolling, setIsScrolling] = useState(false)
const timeoutRef = useRef(null)
diff --git a/src/renderer/src/components/Tab/TabContainer.tsx b/src/renderer/src/components/Tab/TabContainer.tsx
index 4f7d9f13d6..efa19565d1 100644
--- a/src/renderer/src/components/Tab/TabContainer.tsx
+++ b/src/renderer/src/components/Tab/TabContainer.tsx
@@ -1,6 +1,6 @@
import { PlusOutlined } from '@ant-design/icons'
import { Sortable, useDndReorder } from '@renderer/components/dnd'
-import Scrollbar from '@renderer/components/Scrollbar'
+import HorizontalScrollContainer from '@renderer/components/HorizontalScrollContainer'
import { isMac } from '@renderer/config/constant'
import { DEFAULT_MIN_APPS } from '@renderer/config/minapps'
import { useTheme } from '@renderer/context/ThemeProvider'
@@ -14,9 +14,8 @@ import type { Tab } from '@renderer/store/tabs'
import { addTab, removeTab, setActiveTab, setTabs } from '@renderer/store/tabs'
import { ThemeMode } from '@renderer/types'
import { classNames } from '@renderer/utils'
-import { Button, Tooltip } from 'antd'
+import { Tooltip } from 'antd'
import {
- ChevronRight,
FileSearch,
Folder,
Hammer,
@@ -33,7 +32,7 @@ import {
Terminal,
X
} from 'lucide-react'
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { useCallback, useEffect, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocation, useNavigate } from 'react-router-dom'
import styled from 'styled-components'
@@ -98,8 +97,6 @@ const TabsContainer: React.FC = ({ children }) => {
const { hideMinappPopup } = useMinappPopup()
const { minapps } = useMinapps()
const { t } = useTranslation()
- const scrollRef = useRef(null)
- const [canScroll, setCanScroll] = useState(false)
const getTabId = (path: string): string => {
if (path === '/') return 'home'
@@ -175,31 +172,6 @@ const TabsContainer: React.FC = ({ children }) => {
navigate(tab.path)
}
- const handleScrollRight = () => {
- scrollRef.current?.scrollBy({ left: 200, behavior: 'smooth' })
- }
-
- useEffect(() => {
- const scrollElement = scrollRef.current
- if (!scrollElement) return
-
- const checkScrollability = () => {
- setCanScroll(scrollElement.scrollWidth > scrollElement.clientWidth)
- }
-
- checkScrollability()
-
- const resizeObserver = new ResizeObserver(checkScrollability)
- resizeObserver.observe(scrollElement)
-
- window.addEventListener('resize', checkScrollability)
-
- return () => {
- resizeObserver.disconnect()
- window.removeEventListener('resize', checkScrollability)
- }
- }, [tabs])
-
const visibleTabs = useMemo(() => tabs.filter((tab) => !specialTabs.includes(tab.id)), [tabs])
const { onSortEnd } = useDndReorder({
@@ -212,46 +184,39 @@ const TabsContainer: React.FC = ({ children }) => {
return (
-
-
- (
- handleTabClick(tab)}>
-
- {tab.id && {getTabIcon(tab.id, minapps)}}
- {getTabTitle(tab.id)}
-
- {tab.id !== 'home' && (
- {
- e.stopPropagation()
- closeTab(tab.id)
- }}>
-
-
- )}
-
- )}
- />
-
- {canScroll && (
-
-
-
- )}
+
+ (
+ handleTabClick(tab)}>
+
+ {tab.id && {getTabIcon(tab.id, minapps)}}
+ {getTabTitle(tab.id)}
+
+ {tab.id !== 'home' && (
+ {
+ e.stopPropagation()
+ closeTab(tab.id)
+ }}>
+
+
+ )}
+
+ )}
+ />
-
+
`
z-index: 1;
-webkit-app-region: no-drag;
}
-`
-const TabsArea = styled.div`
- display: flex;
- align-items: center;
- flex: 1 1 auto;
- min-width: 0;
- gap: 6px;
- padding-right: 2rem;
- position: relative;
+ .tab-scroll-container {
+ -webkit-app-region: drag;
- -webkit-app-region: drag;
-
- > * {
- -webkit-app-region: no-drag;
- }
-
- &:hover {
- .scroll-right-button {
- opacity: 1;
+ > * {
+ -webkit-app-region: no-drag;
}
}
`
-const TabsScroll = styled(Scrollbar)`
- &::-webkit-scrollbar {
- display: none;
- }
-`
-
const Tab = styled.div<{ active?: boolean }>`
display: flex;
align-items: center;
@@ -414,22 +359,6 @@ const AddTabButton = styled.div`
}
`
-const ScrollButton = styled(Button)`
- position: absolute;
- right: 4rem;
- top: 50%;
- transform: translateY(-50%);
- z-index: 1;
- opacity: 0;
- transition: opacity 0.2s ease-in-out;
-
- border: none;
- box-shadow:
- 0 6px 16px 0 rgba(0, 0, 0, 0.08),
- 0 3px 6px -4px rgba(0, 0, 0, 0.12),
- 0 9px 28px 8px rgba(0, 0, 0, 0.05);
-`
-
const RightButtonsContainer = styled.div`
display: flex;
align-items: center;
diff --git a/src/renderer/src/hooks/useAppInit.ts b/src/renderer/src/hooks/useAppInit.ts
index 2a481f564b..571e5877a8 100644
--- a/src/renderer/src/hooks/useAppInit.ts
+++ b/src/renderer/src/hooks/useAppInit.ts
@@ -12,6 +12,7 @@ import { handleSaveData } from '@renderer/store'
import { selectMemoryConfig } from '@renderer/store/memory'
import { setAvatar, setFilesPath, setResourcesPath, setUpdateState } from '@renderer/store/runtime'
import { delay, runAsyncFunction } from '@renderer/utils'
+import { checkDataLimit } from '@renderer/utils'
import { defaultLanguage } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { useLiveQuery } from 'dexie-react-hooks'
@@ -20,7 +21,7 @@ import { useEffect } from 'react'
import { useDefaultModel } from './useAssistant'
import useFullScreenNotice from './useFullScreenNotice'
import { useRuntime } from './useRuntime'
-import { useSettings } from './useSettings'
+import { useNavbarPosition, useSettings } from './useSettings'
import useUpdateHandler from './useUpdateHandler'
const logger = loggerService.withContext('useAppInit')
@@ -37,6 +38,7 @@ export function useAppInit() {
customCss,
enableDataCollection
} = useSettings()
+ const { isTopNavbar } = useNavbarPosition()
const { minappShow } = useRuntime()
const { setDefaultModel, setQuickModel, setTranslateModel } = useDefaultModel()
const avatar = useLiveQuery(() => db.settings.get('image://avatar'))
@@ -102,14 +104,14 @@ export function useAppInit() {
useEffect(() => {
const transparentWindow = windowStyle === 'transparent' && isMac && !minappShow
- if (minappShow) {
+ if (minappShow && isTopNavbar) {
window.root.style.background =
windowStyle === 'transparent' && isMac ? 'var(--color-background)' : 'var(--navbar-background)'
return
}
window.root.style.background = transparentWindow ? 'var(--navbar-background-mac)' : 'var(--navbar-background)'
- }, [windowStyle, minappShow, theme])
+ }, [windowStyle, minappShow, theme, isTopNavbar])
useEffect(() => {
if (isLocalAi) {
@@ -158,4 +160,8 @@ export function useAppInit() {
logger.error('Failed to update memory config:', error)
})
}, [memoryConfig])
+
+ useEffect(() => {
+ checkDataLimit()
+ }, [])
}
diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json
index 32fb1fd7e5..066478b2f1 100644
--- a/src/renderer/src/i18n/locales/en-us.json
+++ b/src/renderer/src/i18n/locales/en-us.json
@@ -1708,7 +1708,7 @@
"delete_confirm": "Are you sure you want to delete this {{type}}?",
"delete_folder_confirm": "Are you sure you want to delete the folder \"{{name}}\" and all of its contents?",
"delete_note_confirm": "Are you sure you want to delete the note \"{{name}}\"?",
- "drop_markdown_hint": "Drop markdown files here to import",
+ "drop_markdown_hint": "Drop .md files or folders here to import",
"empty": "No notes available yet",
"expand": "unfold",
"export_failed": "Failed to export to knowledge base",
@@ -1719,8 +1719,7 @@
"new_note": "Create a new note",
"no_content_to_copy": "No content to copy",
"no_file_selected": "Please select the file to upload",
- "only_markdown": "Only Markdown files are supported",
- "only_one_file_allowed": "Only one file can be uploaded",
+ "no_valid_files": "No valid file was uploaded",
"open_folder": "Open an external folder",
"open_outside": "Open from external",
"rename": "Rename",
@@ -1782,7 +1781,7 @@
"sort_updated_asc": "Update time (oldest first)",
"sort_updated_desc": "Update time (newest first)",
"sort_z2a": "File name (Z-A)",
- "star": "Favorite",
+ "star": "Favorite note",
"starred_notes": "Collected notes",
"title": "Notes",
"unsaved_changes": "You have unsaved content, are you sure you want to leave?",
@@ -2651,6 +2650,10 @@
"url": "Joplin Web Clipper Service URL",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "Disk Space Warning",
+ "appDataDiskQuotaDescription": "Data directory space is almost full, please clear disk space, otherwise data will be lost"
+ },
"local": {
"autoSync": {
"label": "Auto Backup",
diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json
index b432dac525..002284335b 100644
--- a/src/renderer/src/i18n/locales/zh-cn.json
+++ b/src/renderer/src/i18n/locales/zh-cn.json
@@ -132,6 +132,7 @@
},
"title": "API 服务器"
},
+
"assistants": {
"abbr": "助手",
"clear": {
@@ -1708,7 +1709,7 @@
"delete_confirm": "确定要删除这个{{type}}吗?",
"delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?",
"delete_note_confirm": "确定要删除笔记 \"{{name}}\" 吗?",
- "drop_markdown_hint": "拖拽 Markdown 文件到此处导入",
+ "drop_markdown_hint": "拖拽 .md 文件或目录到此处导入",
"empty": "暂无笔记",
"expand": "展开",
"export_failed": "导出到知识库失败",
@@ -1719,8 +1720,7 @@
"new_note": "新建笔记",
"no_content_to_copy": "没有内容可复制",
"no_file_selected": "请选择要上传的文件",
- "only_markdown": "仅支持 Markdown 格式",
- "only_one_file_allowed": "只能上传一个文件",
+ "no_valid_files": "没有上传有效的文件",
"open_folder": "打开外部文件夹",
"open_outside": "从外部打开",
"rename": "重命名",
@@ -1782,7 +1782,7 @@
"sort_updated_asc": "更新时间(从旧到新)",
"sort_updated_desc": "更新时间(从新到旧)",
"sort_z2a": "文件名(Z-A)",
- "star": "收藏",
+ "star": "收藏笔记",
"starred_notes": "收藏的笔记",
"title": "笔记",
"unsaved_changes": "你有未保存的内容,确定要离开吗?",
@@ -2651,6 +2651,10 @@
"url": "Joplin 剪裁服务监听 URL",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "磁盘空间警告",
+ "appDataDiskQuotaDescription": "数据目录空间即将用尽, 请清理磁盘空间, 否则会丢失数据"
+ },
"local": {
"autoSync": {
"label": "自动备份",
diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json
index 73288e916f..753981be90 100644
--- a/src/renderer/src/i18n/locales/zh-tw.json
+++ b/src/renderer/src/i18n/locales/zh-tw.json
@@ -1708,7 +1708,7 @@
"delete_confirm": "確定要刪除此 {{type}} 嗎?",
"delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?",
"delete_note_confirm": "確定要刪除筆記 \"{{name}}\" 嗎?",
- "drop_markdown_hint": "拖拽 Markdown 文件到此處導入",
+ "drop_markdown_hint": "拖拽 .md 文件或資料夾到此處導入",
"empty": "暫無筆記",
"expand": "展開",
"export_failed": "匯出至知識庫失敗",
@@ -1719,8 +1719,7 @@
"new_note": "新建筆記",
"no_content_to_copy": "沒有內容可複制",
"no_file_selected": "請選擇要上傳的文件",
- "only_markdown": "僅支援 Markdown 格式",
- "only_one_file_allowed": "只能上傳一個文件",
+ "no_valid_files": "沒有上傳有效的檔案",
"open_folder": "打開外部文件夾",
"open_outside": "從外部打開",
"rename": "重命名",
@@ -1782,7 +1781,7 @@
"sort_updated_asc": "更新時間(從舊到新)",
"sort_updated_desc": "更新時間(從新到舊)",
"sort_z2a": "文件名(Z-A)",
- "star": "收藏",
+ "star": "收藏筆記",
"starred_notes": "收藏的筆記",
"title": "筆記",
"unsaved_changes": "你有未儲存的內容,確定要離開嗎?",
@@ -2651,6 +2650,10 @@
"url": "Joplin 剪輯服務 URL",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "磁碟空間警告",
+ "appDataDiskQuotaDescription": "資料目錄空間即將用盡, 請清理磁碟空間, 否則會丟失數據"
+ },
"local": {
"autoSync": {
"label": "自動備份",
diff --git a/src/renderer/src/i18n/translate/el-gr.json b/src/renderer/src/i18n/translate/el-gr.json
index 2403c487b5..8f1c235ece 100644
--- a/src/renderer/src/i18n/translate/el-gr.json
+++ b/src/renderer/src/i18n/translate/el-gr.json
@@ -538,6 +538,10 @@
"tip": "Στη γραμμή εργαλείων των εκτελέσιμων blocks κώδικα θα εμφανίζεται το κουμπί εκτέλεσης· προσέξτε να μην εκτελέσετε επικίνδυνο κώδικα!",
"title": "Εκτέλεση Κώδικα"
},
+ "code_fancy_block": {
+ "label": "Μπλοκ κώδικα με στυλ",
+ "tip": "Χρησιμοποιήστε πιο όμορφο στιλ μπλοκ κώδικα, για παράδειγμα κάρτες HTML"
+ },
"code_image_tools": {
"label": "Ενεργοποίηση εργαλείου προεπισκόπησης",
"tip": "Ενεργοποίηση εργαλείου προεπισκόπησης για εικόνες που αποδίδονται από blocks κώδικα όπως το mermaid"
@@ -1704,7 +1708,7 @@
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};",
"delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;",
"delete_note_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη σημείωση \"{{name}}\";;",
- "drop_markdown_hint": "Σύρετε το αρχείο Markdown εδώ για εισαγωγή",
+ "drop_markdown_hint": "Σύρετε και αποθέστε αρχεία ή φακέλους .md εδώ για εισαγωγή",
"empty": "δεν υπάρχει σημείωση για τώρα",
"expand": "να ανοίξει",
"export_failed": "Εξαγωγή στη βάση γνώσης απέτυχε",
@@ -1715,8 +1719,7 @@
"new_note": "Δημιουργία νέας σημείωσης",
"no_content_to_copy": "Δεν υπάρχει περιεχόμενο προς αντιγραφή",
"no_file_selected": "Επιλέξτε το αρχείο για μεταφόρτωση",
- "only_markdown": "Υποστηρίζεται μόνο η μορφή Markdown",
- "only_one_file_allowed": "Μπορείτε να ανεβάσετε μόνο ένα αρχείο",
+ "no_valid_files": "Δεν ανέβηκε έγκυρο αρχείο",
"open_folder": "Άνοιγμα εξωτερικού φακέλου",
"open_outside": "Από το εξωτερικό",
"rename": "μετονομασία",
@@ -1742,8 +1745,15 @@
"compress_content": "μείωση πλάτους στήλης",
"compress_content_description": "Ενεργοποιώντας το, θα περιορίζεται ο αριθμός των χαρακτήρων ανά γραμμή, μειώνοντας την οθόνη που εμφανίζεται",
"default_font": "προεπιλεγμένη γραμματοσειρά",
+ "font_size": "μέγεθος γραμματοσειράς",
+ "font_size_description": "Ρυθμίστε το μέγεθος της γραμματοσειράς για καλύτερη εμπειρία ανάγνωσης (10-30px)",
+ "font_size_large": "Μεγάλος",
+ "font_size_medium": "中",
+ "font_size_small": "μικρό",
"font_title": "ρυθμίσεις γραμματοσειράς",
"serif_font": "σειρά γραμματοσειρών",
+ "show_table_of_contents": "Εμφάνιση περιεχομένων καταλόγου",
+ "show_table_of_contents_description": "Εμφάνιση πλευρικής στήλης περιεχομένων για εύκολη πλοήγηση στο έγγραφο",
"title": "ρυθμίσεις εμφάνισης"
},
"editor": {
@@ -1771,7 +1781,7 @@
"sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)",
"sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)",
"sort_z2a": "όνομα αρχείου (Z-A)",
- "star": "Αποθήκευση",
+ "star": "Αγαπημένες σημειώσεις",
"starred_notes": "Σημειώσεις συλλογής",
"title": "σημειώσεις",
"unsaved_changes": "Έχετε μη αποθηκευμένο περιεχόμενο, είστε βέβαιοι ότι θέλετε να φύγετε;",
@@ -2235,7 +2245,7 @@
"changeType": "Αλλαγή τύπου",
"deleteProperty": "διαγραφή χαρακτηριστικού",
"editValue": "Επεξεργασία τιμής",
- "empty": "\n空\n",
+ "empty": "Κενό",
"moreActions": "Περισσότερες ενέργειες",
"propertyName": "όνομα χαρακτηριστικού"
},
@@ -2640,6 +2650,10 @@
"url": "URL υπηρεσίας περικοπής Joplin",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "Προειδοποίηση χώρου δίσκου",
+ "appDataDiskQuotaDescription": "Ο κατάλογος δεδομένων της εφαρμογής είναι σχεδόν γεμάτος, παρακαλώ απομακρύνετε τον χώρο δίσκου, αλλιώς θα χαθούν τα δεδομένα"
+ },
"local": {
"autoSync": {
"label": "Αυτόματο αντίγραφο ασφαλείας",
diff --git a/src/renderer/src/i18n/translate/es-es.json b/src/renderer/src/i18n/translate/es-es.json
index f25b2be8f5..905e720b8e 100644
--- a/src/renderer/src/i18n/translate/es-es.json
+++ b/src/renderer/src/i18n/translate/es-es.json
@@ -538,6 +538,10 @@
"tip": "En la barra de herramientas de bloques de código ejecutables se mostrará un botón de ejecución. ¡Tenga cuidado en no ejecutar código peligroso!",
"title": "Ejecución de Código"
},
+ "code_fancy_block": {
+ "label": "Bloque de código con estilo",
+ "tip": "Utiliza un estilo de bloque de código más atractivo, como una tarjeta HTML"
+ },
"code_image_tools": {
"label": "Habilitar herramienta de vista previa",
"tip": "Habilitar herramientas de vista previa para imágenes renderizadas de bloques de código como mermaid"
@@ -1704,7 +1708,7 @@
"delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?",
"delete_folder_confirm": "¿Está seguro de que desea eliminar la carpeta \"{{name}}\" y todo su contenido?",
"delete_note_confirm": "¿Está seguro de que desea eliminar la nota \"{{name}}\"?",
- "drop_markdown_hint": "Arrastra archivos Markdown aquí para importar",
+ "drop_markdown_hint": "Arrastre y suelte archivos o carpetas de .md aquí para importar",
"empty": "Sin notas por el momento",
"expand": "expandir",
"export_failed": "Exportación a la base de conocimientos fallida",
@@ -1715,8 +1719,7 @@
"new_note": "Crear nota nueva",
"no_content_to_copy": "No hay contenido para copiar",
"no_file_selected": "Por favor, seleccione el archivo a subir",
- "only_markdown": "Solo se admite el formato Markdown",
- "only_one_file_allowed": "solo se puede subir un archivo",
+ "no_valid_files": "No se ha cargado un archivo válido",
"open_folder": "abrir carpeta externa",
"open_outside": "Abrir desde el exterior",
"rename": "renombrar",
@@ -1742,8 +1745,15 @@
"compress_content": "reducir el ancho de la columna",
"compress_content_description": "Al activarlo, se limitará el número de caracteres por línea, reduciendo el contenido mostrado en pantalla.",
"default_font": "fuente predeterminada",
+ "font_size": "Tamaño de fuente",
+ "font_size_description": "Ajusta el tamaño de la fuente para una mejor experiencia de lectura (10-30px)",
+ "font_size_large": "Grande",
+ "font_size_medium": "中",
+ "font_size_small": "pequeño",
"font_title": "Configuración de fuente",
"serif_font": "fuente serif",
+ "show_table_of_contents": "Mostrar esquema del directorio",
+ "show_table_of_contents_description": "Mostrar la barra lateral del índice para facilitar la navegación dentro del documento",
"title": "configuración de visualización"
},
"editor": {
@@ -1771,7 +1781,7 @@
"sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)",
"sort_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)",
"sort_z2a": "Nombre de archivo (Z-A)",
- "star": "Colección",
+ "star": "Notas guardadas",
"starred_notes": "notas guardadas",
"title": "notas",
"unsaved_changes": "Tienes contenido no guardado, ¿estás seguro de que quieres salir?",
@@ -2640,6 +2650,10 @@
"url": "URL a la que escucha el servicio de recorte de Joplin",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "Advertencia de espacio en disco",
+ "appDataDiskQuotaDescription": "El espacio de almacenamiento de datos está casi lleno, por favor, limpie el espacio en disco, de lo contrario, se perderán los datos"
+ },
"local": {
"autoSync": {
"label": "Copia de seguridad automática",
diff --git a/src/renderer/src/i18n/translate/fr-fr.json b/src/renderer/src/i18n/translate/fr-fr.json
index 36f9cc192b..5c2a0d2a5d 100644
--- a/src/renderer/src/i18n/translate/fr-fr.json
+++ b/src/renderer/src/i18n/translate/fr-fr.json
@@ -538,6 +538,10 @@
"tip": "Une bouton d'exécution s'affichera dans la barre d'outils des blocs de code exécutables. Attention à ne pas exécuter de code dangereux !",
"title": "Exécution de code"
},
+ "code_fancy_block": {
+ "label": "bloc de code fantaisie",
+ "tip": "Utiliser un style de bloc de code plus esthétique, comme une carte HTML"
+ },
"code_image_tools": {
"label": "Activer l'outil d'aperçu",
"tip": "Activer les outils de prévisualisation pour les images rendues des blocs de code tels que mermaid"
@@ -1704,7 +1708,7 @@
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?",
"delete_folder_confirm": "Êtes-vous sûr de vouloir supprimer le dossier \"{{name}}\" et tout son contenu ?",
"delete_note_confirm": "Êtes-vous sûr de vouloir supprimer la note \"{{name}}\" ?",
- "drop_markdown_hint": "Glissez-déposez le fichier Markdown ici pour l'importer",
+ "drop_markdown_hint": "Déposez ici des fichiers ou dossiers .md pour les importer",
"empty": "Aucune note pour le moment",
"expand": "développer",
"export_failed": "Échec de l'exportation vers la base de connaissances",
@@ -1715,8 +1719,7 @@
"new_note": "Nouvelle note",
"no_content_to_copy": "Aucun contenu à copier",
"no_file_selected": "Veuillez sélectionner le fichier à télécharger",
- "only_markdown": "uniquement le format Markdown est pris en charge",
- "only_one_file_allowed": "On ne peut télécharger qu'un seul fichier",
+ "no_valid_files": "Aucun fichier valide n’a été téléversé",
"open_folder": "ouvrir le dossier externe",
"open_outside": "Ouvrir depuis l'extérieur",
"rename": "renommer",
@@ -1742,8 +1745,15 @@
"compress_content": "réduire la largeur des colonnes",
"compress_content_description": "L'activation limitera le nombre de caractères par ligne, réduisant ainsi le contenu affiché à l'écran.",
"default_font": "police par défaut",
+ "font_size": "Taille de police",
+ "font_size_description": "Ajuster la taille de la police pour une meilleure expérience de lecture (10-30px)",
+ "font_size_large": "Grand",
+ "font_size_medium": "中",
+ "font_size_small": "petit",
"font_title": "paramétrage des polices",
"serif_font": "police à empattements",
+ "show_table_of_contents": "Afficher le plan du sommaire",
+ "show_table_of_contents_description": "Afficher la barre latérale de la table des matières pour faciliter la navigation dans le document",
"title": "Paramètres d'affichage"
},
"editor": {
@@ -1771,7 +1781,7 @@
"sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)",
"sort_updated_desc": "Date de mise à jour (du plus récent au plus ancien)",
"sort_z2a": "Nom de fichier (Z-A)",
- "star": "Favori",
+ "star": "Notes enregistrées",
"starred_notes": "notes de collection",
"title": "notes",
"unsaved_changes": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter ?",
@@ -2640,6 +2650,10 @@
"url": "URL surveillée par le service de découpage de Joplin",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "Avertissement d'espace sur le disque",
+ "appDataDiskQuotaDescription": "L'espace de stockage des données est presque plein, veuillez nettoyer l'espace sur le disque, sinon les données seront perdues"
+ },
"local": {
"autoSync": {
"label": "Sauvegarde automatique",
diff --git a/src/renderer/src/i18n/translate/ja-jp.json b/src/renderer/src/i18n/translate/ja-jp.json
index 2635cf6500..391cbec4e3 100644
--- a/src/renderer/src/i18n/translate/ja-jp.json
+++ b/src/renderer/src/i18n/translate/ja-jp.json
@@ -538,6 +538,10 @@
"tip": "実行可能なコードブロックのツールバーには実行ボタンが表示されます。危険なコードを実行しないでください!",
"title": "コード実行"
},
+ "code_fancy_block": {
+ "label": "\n装飾的なコードブロック\n",
+ "tip": "より見栄えの良いコードブロックスタイルを使用する、例えばHTMLカード"
+ },
"code_image_tools": {
"label": "プレビューツールを有効にする",
"tip": "mermaid などのコードブロックから生成された画像に対してプレビューツールを有効にする"
@@ -1704,7 +1708,7 @@
"delete_confirm": "この{{type}}を本当に削除しますか?",
"delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?",
"delete_note_confirm": "メモ \"{{name}}\" を削除してもよろしいですか?",
- "drop_markdown_hint": "マークダウンファイルをドラッグアンドドロップしてここにインポートします",
+ "drop_markdown_hint": ".md ファイルまたはディレクトリをここにドラッグ&ドロップしてインポートしてください",
"empty": "暫無ノート",
"expand": "展開",
"export_failed": "知識ベースへのエクスポートに失敗しました",
@@ -1715,8 +1719,7 @@
"new_note": "新規ノート作成",
"no_content_to_copy": "コピーするコンテンツはありません",
"no_file_selected": "アップロードするファイルを選択してください",
- "only_markdown": "Markdown ファイルのみをアップロードできます",
- "only_one_file_allowed": "アップロードできるファイルは1つだけです",
+ "no_valid_files": "有効なファイルがアップロードされていません",
"open_folder": "外部フォルダーを開きます",
"open_outside": "外部から開く",
"rename": "名前の変更",
@@ -1742,8 +1745,15 @@
"compress_content": "バーの幅を減らします",
"compress_content_description": "有効にすると、1行あたりの単語数が制限され、画面に表示されるコンテンツが減少します。",
"default_font": "デフォルトフォント",
+ "font_size": "フォントサイズ",
+ "font_size_description": "フォントサイズを調整して読書体験を向上させる(10-30px)",
+ "font_size_large": "大",
+ "font_size_medium": "中",
+ "font_size_small": "小",
"font_title": "フォント設定",
"serif_font": "セリフフォント",
+ "show_table_of_contents": "目次アウトラインを表示",
+ "show_table_of_contents_description": "目次アウトラインサイドバーを表示し、文書内のナビゲーションを容易にする",
"title": "見せる"
},
"editor": {
@@ -1771,7 +1781,7 @@
"sort_updated_asc": "更新日時(古い順)",
"sort_updated_desc": "更新日時(新しい順)",
"sort_z2a": "ファイル名(Z-A)",
- "star": "お気に入りに追加する",
+ "star": "お気に入りのノート",
"starred_notes": "収集したノート",
"title": "ノート",
"unsaved_changes": "保存されていないコンテンツがあります。本当に離れますか?",
@@ -2640,6 +2650,10 @@
"url": "Joplin 剪輯服務 URL",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "ディスク容量警告",
+ "appDataDiskQuotaDescription": "データディレクトリの容量がほぼ満杯になっており、新しいデータの保存ができなくなる可能性があります。まずデータをバックアップしてから、ディスク容量を整理してください。"
+ },
"local": {
"autoSync": {
"label": "自動バックアップ",
diff --git a/src/renderer/src/i18n/translate/pt-pt.json b/src/renderer/src/i18n/translate/pt-pt.json
index 227f5b3c4b..4d1edba05d 100644
--- a/src/renderer/src/i18n/translate/pt-pt.json
+++ b/src/renderer/src/i18n/translate/pt-pt.json
@@ -538,6 +538,10 @@
"tip": "A barra de ferramentas de blocos de código executáveis exibirá um botão de execução; atenção para não executar códigos perigosos!",
"title": "Execução de Código"
},
+ "code_fancy_block": {
+ "label": "Bloco de código estilizado",
+ "tip": "Use um estilo de bloco de código mais agradável, como cartões HTML"
+ },
"code_image_tools": {
"label": "Habilitar ferramenta de visualização",
"tip": "Ativar ferramentas de visualização para imagens renderizadas de blocos de código como mermaid"
@@ -1704,7 +1708,7 @@
"delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?",
"delete_folder_confirm": "Tem a certeza de que deseja eliminar a pasta \"{{name}}\" e todos os seus conteúdos?",
"delete_note_confirm": "Tem a certeza de que deseja eliminar a nota \"{{name}}\"?",
- "drop_markdown_hint": "Arraste o arquivo Markdown para aqui e importe",
+ "drop_markdown_hint": "Arraste e solte arquivos ou pastas .md aqui para importar",
"empty": "Ainda não existem notas",
"expand": "expandir",
"export_failed": "Falha ao exportar para a base de conhecimento",
@@ -1715,8 +1719,7 @@
"new_note": "Nova nota",
"no_content_to_copy": "Não há conteúdo para copiar",
"no_file_selected": "Selecione o arquivo a ser enviado",
- "only_markdown": "Apenas o formato Markdown é suportado",
- "only_one_file_allowed": "só é possível enviar um arquivo",
+ "no_valid_files": "Nenhum arquivo válido foi carregado",
"open_folder": "Abrir pasta externa",
"open_outside": "Abrir externamente",
"rename": "renomear",
@@ -1742,8 +1745,15 @@
"compress_content": "reduzir a largura da coluna",
"compress_content_description": "Ativando isso limitará o número de caracteres por linha, reduzindo o conteúdo exibido na tela.",
"default_font": "fonte padrão",
+ "font_size": "tamanho da fonte",
+ "font_size_description": "Ajuste o tamanho da fonte para uma melhor experiência de leitura (10-30px)",
+ "font_size_large": "Grande",
+ "font_size_medium": "中",
+ "font_size_small": "pequeno",
"font_title": "configuração de fonte",
"serif_font": "fonte com serifa",
+ "show_table_of_contents": "Mostrar esboço do diretório",
+ "show_table_of_contents_description": "Mostrar barra lateral do índice, facilitando a navegação dentro do documento",
"title": "configurações de exibição"
},
"editor": {
@@ -1771,7 +1781,7 @@
"sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)",
"sort_updated_desc": "atualização de tempo (do mais novo para o mais antigo)",
"sort_z2a": "Nome do arquivo (Z-A)",
- "star": "coleções",
+ "star": "Notas favoritas",
"starred_notes": "notas salvas",
"title": "nota",
"unsaved_changes": "Você tem conteúdo não salvo, tem certeza que deseja sair?",
@@ -2640,6 +2650,10 @@
"url": "URL para o qual o serviço de recorte do Joplin está escutando",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "Aviso de espaço em disco",
+ "appDataDiskQuotaDescription": "O espaço de armazenamento de dados está quase cheio, por favor, limpe o espaço em disco, caso contrário, os dados serão perdidos"
+ },
"local": {
"autoSync": {
"label": "Backup automático",
diff --git a/src/renderer/src/i18n/translate/ru-ru.json b/src/renderer/src/i18n/translate/ru-ru.json
index 903d809a13..493a565508 100644
--- a/src/renderer/src/i18n/translate/ru-ru.json
+++ b/src/renderer/src/i18n/translate/ru-ru.json
@@ -538,6 +538,10 @@
"tip": "Выполнение кода в блоке кода возможно, но не рекомендуется выполнять опасный код!",
"title": "Выполнение кода"
},
+ "code_fancy_block": {
+ "label": "Форматированные блоки кода",
+ "tip": "Используйте более эстетичный стиль блоков кода, например, HTML-карточки"
+ },
"code_image_tools": {
"label": "Включить инструменты предпросмотра",
"tip": "Включить инструменты предпросмотра для изображений, сгенерированных из блоков кода (например mermaid)"
@@ -1704,7 +1708,7 @@
"delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?",
"delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?",
"delete_note_confirm": "Вы действительно хотите удалить заметку \"{{name}}\"?",
- "drop_markdown_hint": "Перетаскивать файл разметки, чтобы импортировать его здесь",
+ "drop_markdown_hint": "Перетащите сюда файлы или папки .md для импорта",
"empty": "заметок пока нет",
"expand": "развернуть",
"export_failed": "Экспорт в базу знаний не выполнен",
@@ -1715,8 +1719,7 @@
"new_note": "Создать заметку",
"no_content_to_copy": "Нет контента для копирования",
"no_file_selected": "Пожалуйста, выберите файл для загрузки",
- "only_markdown": "Только Markdown",
- "only_one_file_allowed": "Можно загрузить только один файл",
+ "no_valid_files": "Не загружен действительный файл",
"open_folder": "Откройте внешнюю папку",
"open_outside": "открыть снаружи",
"rename": "переименовать",
@@ -1742,8 +1745,15 @@
"compress_content": "Уменьшить ширину стержня",
"compress_content_description": "При включении он ограничит количество слов на строку, уменьшая содержимое, отображаемое на экране.",
"default_font": "По умолчанию шрифт",
+ "font_size": "Размер шрифта",
+ "font_size_description": "Отрегулируйте размер шрифта для лучшего чтения (10–30 пикселей)",
+ "font_size_large": "Большой",
+ "font_size_medium": "中",
+ "font_size_small": "\nмаленький\n",
"font_title": "Настройки шрифта",
"serif_font": "Serif Font",
+ "show_table_of_contents": "Показать оглавление",
+ "show_table_of_contents_description": "显示目录大纲侧边栏,方便文档内导航",
"title": "показывать"
},
"editor": {
@@ -1771,7 +1781,7 @@
"sort_updated_asc": "Время обновления (от старого к новому)",
"sort_updated_desc": "Время обновления (от нового к старому)",
"sort_z2a": "Имя файла (Я-А)",
- "star": "Сохранить",
+ "star": "Избранные заметки",
"starred_notes": "Сохраненные заметки",
"title": "заметки",
"unsaved_changes": "Вы не сохранили содержимое. Вы уверены, что хотите уйти?",
@@ -2640,6 +2650,10 @@
"url": "URL Joplin",
"url_placeholder": "http://127.0.0.1:41184/"
},
+ "limit": {
+ "appDataDiskQuota": "Предупреждение о пространстве на диске",
+ "appDataDiskQuotaDescription": "Каталог данных почти заполнен, что может привести к невозможности сохранения новых данных. Сначала создайте резервную копию данных, затем освободите дисковое пространство."
+ },
"local": {
"autoSync": {
"label": "Автоматическое резервное копирование",
diff --git a/src/renderer/src/pages/home/Chat.tsx b/src/renderer/src/pages/home/Chat.tsx
index 68fa4e5dc0..4ea6bcdd57 100644
--- a/src/renderer/src/pages/home/Chat.tsx
+++ b/src/renderer/src/pages/home/Chat.tsx
@@ -43,6 +43,7 @@ const Chat: FC = (props) => {
const { showTopics } = useShowTopics()
const { isMultiSelectMode } = useChatContext(props.activeTopic)
const { isTopNavbar } = useNavbarPosition()
+ const chatMaxWidth = useChatMaxWidth()
const mainRef = React.useRef(null)
const contentSearchRef = React.useRef(null)
@@ -153,7 +154,7 @@ const Chat: FC = (props) => {
vertical
flex={1}
justify="space-between"
- style={{ maxWidth: '100%', height: mainHeight }}>
+ style={{ maxWidth: chatMaxWidth, height: mainHeight }}>
= ({ assistant: _assistant, setActiveTopic, topic }) =
setSelectedKnowledgeBases(bases ?? [])
}
+ const handleRemoveModel = (model: Model) => {
+ setMentionedModels(mentionedModels.filter((m) => m.id !== model.id))
+ }
+
+ const handleRemoveKnowledgeBase = (knowledgeBase: KnowledgeBase) => {
+ const newKnowledgeBases = assistant.knowledge_bases?.filter((kb) => kb.id !== knowledgeBase.id)
+ updateAssistant({
+ ...assistant,
+ knowledge_bases: newKnowledgeBases
+ })
+ setSelectedKnowledgeBases(newKnowledgeBases ?? [])
+ }
+
const onEnableGenerateImage = () => {
updateAssistant({ ...assistant, enableGenerateImage: !assistant.enableGenerateImage })
}
@@ -851,6 +866,15 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) =
className={classNames('inputbar-container', inputFocus && 'focus', isFileDragging && 'file-dragging')}
ref={containerRef}>
{files.length > 0 && }
+ {selectedKnowledgeBases.length > 0 && (
+
+ )}
+ {mentionedModels.length > 0 && (
+
+ )}
)}
-
-
+
+
+
+ {breadcrumbItems.map((item, index) => (
+
+ handleBreadcrumbClick(item)}
+ $clickable={item.isFolder && index < breadcrumbItems.length - 1}>
+ {item.title}
+
+
+ ))}
+
+
{canShowStarButton && (
@@ -225,4 +279,55 @@ export const StarButton = styled.div`
}
`
+export const BreadcrumbsContainer = styled.div`
+ width: 100%;
+ overflow: hidden;
+
+ /* 确保 HeroUI Breadcrumbs 组件保持在一行 */
+ & > nav {
+ white-space: nowrap;
+ overflow: hidden;
+ }
+
+ & ol {
+ flex-wrap: nowrap !important;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ }
+
+ & li {
+ flex-shrink: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ }
+
+ /* 确保分隔符不会与标题重叠 */
+ & li:not(:last-child)::after {
+ flex-shrink: 0;
+ margin: 0 8px;
+ }
+`
+
+export const BreadcrumbTitle = styled.span<{ $clickable?: boolean }>`
+ max-width: 150px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ flex-shrink: 1;
+ min-width: 0;
+
+ ${({ $clickable }) =>
+ $clickable &&
+ `
+ cursor: pointer;
+ &:hover {
+ color: var(--color-primary);
+ text-decoration: underline;
+ }
+ `}
+`
+
export default HeaderNavbar
diff --git a/src/renderer/src/pages/notes/NotesPage.tsx b/src/renderer/src/pages/notes/NotesPage.tsx
index e1976fcd20..c85793e781 100644
--- a/src/renderer/src/pages/notes/NotesPage.tsx
+++ b/src/renderer/src/pages/notes/NotesPage.tsx
@@ -12,7 +12,7 @@ import {
moveNode,
renameNode,
sortAllLevels,
- uploadNote
+ uploadFiles
} from '@renderer/services/NotesService'
import { getNotesTree, isParentNode, updateNodeInTree } from '@renderer/services/NotesTreeService'
import { useAppDispatch, useAppSelector } from '@renderer/store'
@@ -525,38 +525,36 @@ const NotesPage: FC = () => {
const handleUploadFiles = useCallback(
async (files: File[]) => {
try {
- const fileToUpload = files[0]
-
- if (!fileToUpload) {
+ if (!files || files.length === 0) {
window.toast.warning(t('notes.no_file_selected'))
return
}
- // 暂时这么处理
- if (files.length > 1) {
- window.toast.warning(t('notes.only_one_file_allowed'))
+
+ const targetFolderPath = getTargetFolderPath()
+ if (!targetFolderPath) {
+ throw new Error('No folder path selected')
}
- if (!fileToUpload.name.toLowerCase().endsWith('.md')) {
- window.toast.warning(t('notes.only_markdown'))
+ const result = await uploadFiles(files, targetFolderPath)
+
+ // 检查上传结果
+ if (result.fileCount === 0) {
+ window.toast.warning(t('notes.no_valid_files'))
return
}
- try {
- if (!notesPath) {
- throw new Error('No folder path selected')
- }
- await uploadNote(fileToUpload, notesPath)
- window.toast.success(t('notes.upload_success', { count: 1 }))
- } catch (error) {
- logger.error(`Failed to upload note file ${fileToUpload.name}:`, error as Error)
- window.toast.error(t('notes.upload_failed', { name: fileToUpload.name }))
- }
+ // 排序并显示成功信息
+ await sortAllLevels(sortType)
+
+ const successMessage = t('notes.upload_success')
+
+ window.toast.success(successMessage)
} catch (error) {
logger.error('Failed to handle file upload:', error as Error)
window.toast.error(t('notes.upload_failed'))
}
},
- [notesPath, t]
+ [getTargetFolderPath, sortType, t]
)
// 处理节点移动
diff --git a/src/renderer/src/pages/notes/NotesSidebar.tsx b/src/renderer/src/pages/notes/NotesSidebar.tsx
index 045697857b..c9803208c3 100644
--- a/src/renderer/src/pages/notes/NotesSidebar.tsx
+++ b/src/renderer/src/pages/notes/NotesSidebar.tsx
@@ -461,19 +461,82 @@ const NotesSidebar: FC = ({
)
const handleDropFiles = useCallback(
- (e: React.DragEvent) => {
+ async (e: React.DragEvent) => {
e.preventDefault()
setIsDragOverSidebar(false)
- const files = Array.from(e.dataTransfer.files)
+ // 处理文件夹拖拽:从 dataTransfer.items 获取完整的文件路径信息
+ const items = Array.from(e.dataTransfer.items)
+ const files: File[] = []
- if (files.length > 0) {
- onUploadFiles(files)
+ const processEntry = async (entry: FileSystemEntry, path: string = '') => {
+ if (entry.isFile) {
+ const fileEntry = entry as FileSystemFileEntry
+ return new Promise((resolve) => {
+ fileEntry.file((file) => {
+ // 手动设置 webkitRelativePath 以保持文件夹结构
+ Object.defineProperty(file, 'webkitRelativePath', {
+ value: path + file.name,
+ writable: false
+ })
+ files.push(file)
+ resolve()
+ })
+ })
+ } else if (entry.isDirectory) {
+ const dirEntry = entry as FileSystemDirectoryEntry
+ const reader = dirEntry.createReader()
+ return new Promise((resolve) => {
+ reader.readEntries(async (entries) => {
+ const promises = entries.map((subEntry) => processEntry(subEntry, path + entry.name + '/'))
+ await Promise.all(promises)
+ resolve()
+ })
+ })
+ }
+ }
+
+ // 如果支持 DataTransferItem API(文件夹拖拽)
+ if (items.length > 0 && items[0].webkitGetAsEntry()) {
+ const promises = items.map((item) => {
+ const entry = item.webkitGetAsEntry()
+ return entry ? processEntry(entry) : Promise.resolve()
+ })
+
+ await Promise.all(promises)
+
+ if (files.length > 0) {
+ onUploadFiles(files)
+ }
+ } else {
+ const regularFiles = Array.from(e.dataTransfer.files)
+ if (regularFiles.length > 0) {
+ onUploadFiles(regularFiles)
+ }
}
},
[onUploadFiles]
)
+ const handleClickToSelectFiles = useCallback(() => {
+ const fileInput = document.createElement('input')
+ fileInput.type = 'file'
+ fileInput.multiple = true
+ fileInput.accept = '.md,.markdown'
+ fileInput.webkitdirectory = false
+
+ fileInput.onchange = (e) => {
+ const target = e.target as HTMLInputElement
+ if (target.files && target.files.length > 0) {
+ const selectedFiles = Array.from(target.files)
+ onUploadFiles(selectedFiles)
+ }
+ fileInput.remove()
+ }
+
+ fileInput.click()
+ }, [onUploadFiles])
+
return (
{
@@ -512,7 +575,7 @@ const NotesSidebar: FC = ({
- {t('notes.drop_markdown_hint')}
+ {t('notes.drop_markdown_hint')}
@@ -674,12 +737,6 @@ const NodeName = styled.div`
const EditInput = styled(Input)`
flex: 1;
font-size: 13px;
-
- .ant-input {
- font-size: 13px;
- padding: 2px 6px;
- border: 0.5px solid var(--color-primary);
- }
`
const DragOverIndicator = styled.div`
diff --git a/src/renderer/src/pages/notes/NotesSidebarHeader.tsx b/src/renderer/src/pages/notes/NotesSidebarHeader.tsx
index 7f47ad14c7..f4d4e17e7d 100644
--- a/src/renderer/src/pages/notes/NotesSidebarHeader.tsx
+++ b/src/renderer/src/pages/notes/NotesSidebarHeader.tsx
@@ -69,18 +69,18 @@ const NotesSidebarHeader: FC = ({
{!isShowStarred && !isShowSearch && (
<>
-
-
-
-
-
-
+
+
+
+
+
+
= ({ resolve }) => {
const server: MCPServer = {
id,
isActive: false,
- ...(serverConfig as any)
- }
-
- if (!server.name) {
- server.name = id
+ name: serverConfig.name || id,
+ ...serverConfig
}
serversArray.push(server)
@@ -95,9 +92,8 @@ const PopupContainer: React.FC = ({ resolve }) => {
window.toast.success(t('settings.mcp.jsonSaveSuccess'))
setJsonError('')
setOpen(false)
- } catch (error: any) {
- logger.error('Failed to save JSON config:', error)
- setJsonError(error.message || t('settings.mcp.jsonSaveError'))
+ } catch (error: unknown) {
+ setJsonError(formatErrorMessage(error) || t('settings.mcp.jsonSaveError'))
window.toast.error(t('settings.mcp.jsonSaveError'))
} finally {
setJsonSaving(false)
diff --git a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx
index db7484ac21..70488d5b41 100644
--- a/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx
+++ b/src/renderer/src/pages/settings/MCPSettings/McpSettings.tsx
@@ -1,4 +1,5 @@
import { loggerService } from '@logger'
+import type { McpError } from '@modelcontextprotocol/sdk/types.js'
import { DeleteIcon } from '@renderer/components/Icons'
import { useTheme } from '@renderer/context/ThemeProvider'
import { useMCPServer, useMCPServers } from '@renderer/hooks/useMCPServers'
@@ -424,7 +425,7 @@ const McpSettings: React.FC = () => {
} catch (error: any) {
window.modal.error({
title: t('settings.mcp.startError'),
- content: formatMcpError(error),
+ content: formatMcpError(error as McpError),
centered: true
})
updateMCPServer({ ...server, isActive: oldActiveState })
diff --git a/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts b/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts
index 0b1a9b585a..36fd4b0c04 100644
--- a/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts
+++ b/src/renderer/src/pages/settings/MCPSettings/providers/modelscope.ts
@@ -1,6 +1,6 @@
import { loggerService } from '@logger'
import { nanoid } from '@reduxjs/toolkit'
-import type { MCPServer } from '@renderer/types'
+import { getMcpServerType, type MCPServer } from '@renderer/types'
import i18next from 'i18next'
const logger = loggerService.withContext('ModelScopeSyncUtils')
@@ -104,13 +104,13 @@ export const syncModelScopeServers = async (
// Check if server already exists
const existingServer = existingServers.find((s) => s.id === `@modelscope/${server.id}`)
-
+ const url = server.operational_urls[0].url
const mcpServer: MCPServer = {
id: `@modelscope/${server.id}`,
name: server.chinese_name || server.name || `ModelScope Server ${nanoid()}`,
description: server.description || '',
- type: 'sse',
- baseUrl: server.operational_urls[0].url,
+ type: getMcpServerType(url),
+ baseUrl: url,
command: '',
args: [],
env: {},
diff --git a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx
index d5da53a23f..0bd7b152f4 100644
--- a/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx
+++ b/src/renderer/src/pages/settings/ProviderSettings/ProviderSetting.tsx
@@ -328,18 +328,16 @@ const ProviderSetting: FC = ({ providerId }) => {
)}
- {apiKeyWebsite && (
-
-
- {!isDmxapi && (
-
- {t('settings.provider.get_api_key')}
-
- )}
-
- {t('settings.provider.api_key.tip')}
-
- )}
+
+
+ {apiKeyWebsite && !isDmxapi && (
+
+ {t('settings.provider.get_api_key')}
+
+ )}
+
+ {t('settings.provider.api_key.tip')}
+
{!isDmxapi && !isAnthropicOAuth() && (
<>
diff --git a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx
index 79ac56237c..2f301ac957 100644
--- a/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx
+++ b/src/renderer/src/pages/settings/WebSearchSettings/BasicSettings.tsx
@@ -11,7 +11,7 @@ import { SettingDivider, SettingGroup, SettingRow, SettingRowTitle, SettingTitle
const BasicSettings: FC = () => {
const { theme } = useTheme()
- const { searchWithTime, maxResults } = useWebSearchSettings()
+ const { searchWithTime, maxResults, compressionConfig } = useWebSearchSettings()
const dispatch = useAppDispatch()
@@ -28,7 +28,7 @@ const BasicSettings: FC = () => {
{t('settings.tool.websearch.search_max_result.label')}
- {maxResults > 20 && (
+ {maxResults > 20 && compressionConfig?.method === 'none' && (
diff --git a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx
index f1fa43fab8..cb0f3fc8c0 100644
--- a/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx
+++ b/src/renderer/src/pages/settings/WebSearchSettings/WebSearchProviderSetting.tsx
@@ -5,6 +5,7 @@ import ExaLogo from '@renderer/assets/images/search/exa.png'
import SearxngLogo from '@renderer/assets/images/search/searxng.svg'
import TavilyLogo from '@renderer/assets/images/search/tavily.png'
import ZhipuLogo from '@renderer/assets/images/search/zhipu.png'
+import { HStack } from '@renderer/components/Layout'
import ApiKeyListPopup from '@renderer/components/Popups/ApiKeyListPopup/popup'
import { WEB_SEARCH_PROVIDER_CONFIG } from '@renderer/config/webSearchProviders'
import { useTimer } from '@renderer/hooks/useTimer'
@@ -205,9 +206,13 @@ const WebSearchProviderSetting: FC = ({ providerId }) => {
-
- {t('settings.provider.api_key.tip')}
-
+
+ {apiKeyWebsite && (
+
+ {t('settings.provider.get_api_key')}
+
+ )}
+
{t('settings.provider.api_key.tip')}
>
diff --git a/src/renderer/src/services/NotesService.ts b/src/renderer/src/services/NotesService.ts
index 4e27a41366..45383344d4 100644
--- a/src/renderer/src/services/NotesService.ts
+++ b/src/renderer/src/services/NotesService.ts
@@ -96,41 +96,48 @@ export async function createNote(name: string, content: string = '', folderPath:
return note
}
+export interface UploadResult {
+ uploadedNodes: NotesTreeNode[]
+ totalFiles: number
+ skippedFiles: number
+ fileCount: number
+ folderCount: number
+}
+
/**
- * 上传笔记
+ * 上传文件或文件夹,支持单个或批量上传,保持文件夹结构
*/
-export async function uploadNote(file: File, folderPath: string): Promise {
+export async function uploadFiles(files: File[], targetFolderPath: string): Promise {
const tree = await getNotesTree()
- const fileName = file.name.toLowerCase()
- if (!fileName.endsWith(MARKDOWN_EXT)) {
- throw new Error('Only markdown files are allowed')
+ const uploadedNodes: NotesTreeNode[] = []
+ let skippedFiles = 0
+
+ const markdownFiles = filterMarkdownFiles(files)
+ skippedFiles = files.length - markdownFiles.length
+
+ if (markdownFiles.length === 0) {
+ return createEmptyUploadResult(files.length, skippedFiles)
}
- const noteId = uuidv4()
- const nameWithoutExt = fileName.replace(MARKDOWN_EXT, '')
+ // 处理重复的根文件夹名称
+ const processedFiles = await processDuplicateRootFolders(markdownFiles, targetFolderPath)
- const { safeName, exists } = await window.api.file.checkFileName(folderPath, nameWithoutExt, true)
- if (exists) {
- logger.warn(`Note already exists: ${safeName}`)
+ const { filesByPath, foldersToCreate } = groupFilesByPath(processedFiles, targetFolderPath)
+
+ const createdFolders = await createFoldersSequentially(foldersToCreate, targetFolderPath, tree, uploadedNodes)
+
+ await uploadAllFiles(filesByPath, targetFolderPath, tree, createdFolders, uploadedNodes)
+
+ const fileCount = uploadedNodes.filter((node) => node.type === 'file').length
+ const folderCount = uploadedNodes.filter((node) => node.type === 'folder').length
+
+ return {
+ uploadedNodes,
+ totalFiles: files.length,
+ skippedFiles,
+ fileCount,
+ folderCount
}
-
- const notePath = `${folderPath}/${safeName}${MARKDOWN_EXT}`
-
- const note: NotesTreeNode = {
- id: noteId,
- name: safeName,
- treePath: `/${safeName}`,
- externalPath: notePath,
- type: 'file',
- createdAt: new Date().toISOString(),
- updatedAt: new Date().toISOString()
- }
-
- const content = await file.text()
- await window.api.file.write(notePath, content)
- insertNodeIntoTree(tree, note)
-
- return note
}
/**
@@ -148,7 +155,7 @@ export async function deleteNode(nodeId: string): Promise {
await window.api.file.deleteExternalFile(node.externalPath)
}
- removeNodeFromTree(tree, nodeId)
+ await removeNodeFromTree(tree, nodeId)
}
/**
@@ -387,3 +394,351 @@ function findNodeByExternalPath(nodes: NotesTreeNode[], externalPath: string): N
}
return null
}
+
+/**
+ * 过滤出 Markdown 文件
+ */
+function filterMarkdownFiles(files: File[]): File[] {
+ return Array.from(files).filter((file) => {
+ if (file.name.toLowerCase().endsWith(MARKDOWN_EXT)) {
+ return true
+ }
+ logger.warn(`Skipping non-markdown file: ${file.name}`)
+ return false
+ })
+}
+
+/**
+ * 创建空的上传结果
+ */
+function createEmptyUploadResult(totalFiles: number, skippedFiles: number): UploadResult {
+ return {
+ uploadedNodes: [],
+ totalFiles,
+ skippedFiles,
+ fileCount: 0,
+ folderCount: 0
+ }
+}
+
+/**
+ * 处理重复的根文件夹名称,为重复的文件夹重写 webkitRelativePath
+ */
+async function processDuplicateRootFolders(markdownFiles: File[], targetFolderPath: string): Promise {
+ // 按根文件夹名称分组文件
+ const filesByRootFolder = new Map()
+ const processedFiles: File[] = []
+
+ for (const file of markdownFiles) {
+ const filePath = file.webkitRelativePath || file.name
+
+ if (filePath.includes('/')) {
+ const rootFolderName = filePath.substring(0, filePath.indexOf('/'))
+ if (!filesByRootFolder.has(rootFolderName)) {
+ filesByRootFolder.set(rootFolderName, [])
+ }
+ filesByRootFolder.get(rootFolderName)!.push(file)
+ } else {
+ // 单个文件,直接添加
+ processedFiles.push(file)
+ }
+ }
+
+ // 为每个根文件夹组生成唯一的文件夹名称
+ for (const [rootFolderName, files] of filesByRootFolder.entries()) {
+ const { safeName } = await window.api.file.checkFileName(targetFolderPath, rootFolderName, false)
+
+ for (const file of files) {
+ // 创建一个新的 File 对象,并修改 webkitRelativePath
+ const originalPath = file.webkitRelativePath || file.name
+ const relativePath = originalPath.substring(originalPath.indexOf('/') + 1)
+ const newPath = `${safeName}/${relativePath}`
+
+ const newFile = new File([file], file.name, {
+ type: file.type,
+ lastModified: file.lastModified
+ })
+
+ Object.defineProperty(newFile, 'webkitRelativePath', {
+ value: newPath,
+ writable: false
+ })
+
+ processedFiles.push(newFile)
+ }
+ }
+
+ return processedFiles
+}
+
+/**
+ * 按路径分组文件并收集需要创建的文件夹
+ */
+function groupFilesByPath(
+ markdownFiles: File[],
+ targetFolderPath: string
+): { filesByPath: Map; foldersToCreate: Set } {
+ const filesByPath = new Map()
+ const foldersToCreate = new Set()
+
+ for (const file of markdownFiles) {
+ const filePath = file.webkitRelativePath || file.name
+ const relativeDirPath = filePath.includes('/') ? filePath.substring(0, filePath.lastIndexOf('/')) : ''
+ const fullDirPath = relativeDirPath ? `${targetFolderPath}/${relativeDirPath}` : targetFolderPath
+
+ if (relativeDirPath) {
+ const pathParts = relativeDirPath.split('/')
+
+ let currentPath = targetFolderPath
+ for (const part of pathParts) {
+ currentPath = `${currentPath}/${part}`
+ foldersToCreate.add(currentPath)
+ }
+ }
+
+ if (!filesByPath.has(fullDirPath)) {
+ filesByPath.set(fullDirPath, [])
+ }
+ filesByPath.get(fullDirPath)!.push(file)
+ }
+
+ return { filesByPath, foldersToCreate }
+}
+
+/**
+ * 顺序创建文件夹(避免竞争条件)
+ */
+async function createFoldersSequentially(
+ foldersToCreate: Set,
+ targetFolderPath: string,
+ tree: NotesTreeNode[],
+ uploadedNodes: NotesTreeNode[]
+): Promise