mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-09 23:10:20 +08:00
Merge remote-tracking branch 'origin/main' into feat/print-to-pdf
This commit is contained in:
commit
b248a6932e
@ -398,11 +398,15 @@ export function validateFileName(fileName: string, platform = process.platform):
|
|||||||
* @returns 合法的文件名
|
* @returns 合法的文件名
|
||||||
*/
|
*/
|
||||||
export function checkName(fileName: string): string {
|
export function checkName(fileName: string): string {
|
||||||
const validation = validateFileName(fileName)
|
const baseName = path.basename(fileName)
|
||||||
|
const validation = validateFileName(baseName)
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
throw new Error(`Invalid file name: ${fileName}. ${validation.error}`)
|
// 自动清理非法字符,而不是抛出错误
|
||||||
|
const sanitized = sanitizeFilename(baseName)
|
||||||
|
logger.warn(`File name contains invalid characters, auto-sanitized: "${baseName}" -> "${sanitized}"`)
|
||||||
|
return sanitized
|
||||||
}
|
}
|
||||||
return fileName
|
return baseName
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const QuickPanelProvider: React.FC<React.PropsWithChildren> = ({ children
|
|||||||
const close = useCallback(
|
const close = useCallback(
|
||||||
(action?: QuickPanelCloseAction, searchText?: string) => {
|
(action?: QuickPanelCloseAction, searchText?: string) => {
|
||||||
setIsVisible(false)
|
setIsVisible(false)
|
||||||
onClose?.({ symbol, action, triggerInfo, searchText, item: {} as QuickPanelListItem, multiple: false })
|
onClose?.({ action, searchText, item: {} as QuickPanelListItem, context: this })
|
||||||
|
|
||||||
clearTimer.current = setTimeout(() => {
|
clearTimer.current = setTimeout(() => {
|
||||||
setList([])
|
setList([])
|
||||||
@ -73,7 +73,7 @@ export const QuickPanelProvider: React.FC<React.PropsWithChildren> = ({ children
|
|||||||
setTriggerInfo(undefined)
|
setTriggerInfo(undefined)
|
||||||
}, 200)
|
}, 200)
|
||||||
},
|
},
|
||||||
[onClose, symbol, triggerInfo]
|
[onClose]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -8,13 +8,10 @@ export type QuickPanelTriggerInfo = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type QuickPanelCallBackOptions = {
|
export type QuickPanelCallBackOptions = {
|
||||||
symbol: string
|
context: QuickPanelContextType
|
||||||
action: QuickPanelCloseAction
|
action: QuickPanelCloseAction
|
||||||
item: QuickPanelListItem
|
item: QuickPanelListItem
|
||||||
searchText?: string
|
searchText?: string
|
||||||
/** 是否处于多选状态 */
|
|
||||||
multiple?: boolean
|
|
||||||
triggerInfo?: QuickPanelTriggerInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type QuickPanelOpenOptions = {
|
export type QuickPanelOpenOptions = {
|
||||||
|
|||||||
@ -222,11 +222,10 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
// 创建更新后的item对象用于回调
|
// 创建更新后的item对象用于回调
|
||||||
const updatedItem = { ...item, isSelected: newSelectedState }
|
const updatedItem = { ...item, isSelected: newSelectedState }
|
||||||
const quickPanelCallBackOptions: QuickPanelCallBackOptions = {
|
const quickPanelCallBackOptions: QuickPanelCallBackOptions = {
|
||||||
symbol: ctx.symbol,
|
context: ctx,
|
||||||
action,
|
action,
|
||||||
item: updatedItem,
|
item: updatedItem,
|
||||||
searchText: searchText,
|
searchText: searchText
|
||||||
multiple: ctx.multiple
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.beforeAction?.(quickPanelCallBackOptions)
|
ctx.beforeAction?.(quickPanelCallBackOptions)
|
||||||
@ -236,11 +235,10 @@ export const QuickPanelView: React.FC<Props> = ({ setInputText }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const quickPanelCallBackOptions: QuickPanelCallBackOptions = {
|
const quickPanelCallBackOptions: QuickPanelCallBackOptions = {
|
||||||
symbol: ctx.symbol,
|
context: ctx,
|
||||||
action,
|
action,
|
||||||
item,
|
item,
|
||||||
searchText: searchText,
|
searchText: searchText
|
||||||
multiple: ctx.multiple
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.beforeAction?.(quickPanelCallBackOptions)
|
ctx.beforeAction?.(quickPanelCallBackOptions)
|
||||||
|
|||||||
@ -1714,7 +1714,7 @@
|
|||||||
"delete_confirm": "Are you sure you want to delete this {{type}}?",
|
"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_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}}\"?",
|
"delete_note_confirm": "Are you sure you want to delete the note \"{{name}}\"?",
|
||||||
"drop_markdown_hint": "Drop markdown files or folders here to import",
|
"drop_markdown_hint": "Drop .md files or folders here to import",
|
||||||
"empty": "No notes available yet",
|
"empty": "No notes available yet",
|
||||||
"expand": "unfold",
|
"expand": "unfold",
|
||||||
"exportPDF": "Export to PDF",
|
"exportPDF": "Export to PDF",
|
||||||
@ -1788,7 +1788,7 @@
|
|||||||
"sort_updated_asc": "Update time (oldest first)",
|
"sort_updated_asc": "Update time (oldest first)",
|
||||||
"sort_updated_desc": "Update time (newest first)",
|
"sort_updated_desc": "Update time (newest first)",
|
||||||
"sort_z2a": "File name (Z-A)",
|
"sort_z2a": "File name (Z-A)",
|
||||||
"star": "Favorite",
|
"star": "Favorite note",
|
||||||
"starred_notes": "Collected notes",
|
"starred_notes": "Collected notes",
|
||||||
"title": "Notes",
|
"title": "Notes",
|
||||||
"unsaved_changes": "You have unsaved content, are you sure you want to leave?",
|
"unsaved_changes": "You have unsaved content, are you sure you want to leave?",
|
||||||
|
|||||||
@ -1715,7 +1715,7 @@
|
|||||||
"delete_confirm": "确定要删除这个{{type}}吗?",
|
"delete_confirm": "确定要删除这个{{type}}吗?",
|
||||||
"delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?",
|
"delete_folder_confirm": "确定要删除文件夹 \"{{name}}\" 及其所有内容吗?",
|
||||||
"delete_note_confirm": "确定要删除笔记 \"{{name}}\" 吗?",
|
"delete_note_confirm": "确定要删除笔记 \"{{name}}\" 吗?",
|
||||||
"drop_markdown_hint": "拖拽 Markdown 文件或目录到此处导入",
|
"drop_markdown_hint": "拖拽 .md 文件或目录到此处导入",
|
||||||
"empty": "暂无笔记",
|
"empty": "暂无笔记",
|
||||||
"expand": "展开",
|
"expand": "展开",
|
||||||
"exportPDF": "导出为PDF",
|
"exportPDF": "导出为PDF",
|
||||||
@ -1789,7 +1789,7 @@
|
|||||||
"sort_updated_asc": "更新时间(从旧到新)",
|
"sort_updated_asc": "更新时间(从旧到新)",
|
||||||
"sort_updated_desc": "更新时间(从新到旧)",
|
"sort_updated_desc": "更新时间(从新到旧)",
|
||||||
"sort_z2a": "文件名(Z-A)",
|
"sort_z2a": "文件名(Z-A)",
|
||||||
"star": "收藏",
|
"star": "收藏笔记",
|
||||||
"starred_notes": "收藏的笔记",
|
"starred_notes": "收藏的笔记",
|
||||||
"title": "笔记",
|
"title": "笔记",
|
||||||
"unsaved_changes": "你有未保存的内容,确定要离开吗?",
|
"unsaved_changes": "你有未保存的内容,确定要离开吗?",
|
||||||
|
|||||||
@ -1714,7 +1714,7 @@
|
|||||||
"delete_confirm": "確定要刪除此 {{type}} 嗎?",
|
"delete_confirm": "確定要刪除此 {{type}} 嗎?",
|
||||||
"delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?",
|
"delete_folder_confirm": "確定要刪除資料夾 \"{{name}}\" 及其所有內容嗎?",
|
||||||
"delete_note_confirm": "確定要刪除筆記 \"{{name}}\" 嗎?",
|
"delete_note_confirm": "確定要刪除筆記 \"{{name}}\" 嗎?",
|
||||||
"drop_markdown_hint": "拖拽 Markdown 文件或資料夾到此處導入",
|
"drop_markdown_hint": "拖拽 .md 文件或資料夾到此處導入",
|
||||||
"empty": "暫無筆記",
|
"empty": "暫無筆記",
|
||||||
"expand": "展開",
|
"expand": "展開",
|
||||||
"exportPDF": "匯出為PDF",
|
"exportPDF": "匯出為PDF",
|
||||||
@ -1788,7 +1788,7 @@
|
|||||||
"sort_updated_asc": "更新時間(從舊到新)",
|
"sort_updated_asc": "更新時間(從舊到新)",
|
||||||
"sort_updated_desc": "更新時間(從新到舊)",
|
"sort_updated_desc": "更新時間(從新到舊)",
|
||||||
"sort_z2a": "文件名(Z-A)",
|
"sort_z2a": "文件名(Z-A)",
|
||||||
"star": "收藏",
|
"star": "收藏筆記",
|
||||||
"starred_notes": "收藏的筆記",
|
"starred_notes": "收藏的筆記",
|
||||||
"title": "筆記",
|
"title": "筆記",
|
||||||
"unsaved_changes": "你有未儲存的內容,確定要離開嗎?",
|
"unsaved_changes": "你有未儲存的內容,確定要離開嗎?",
|
||||||
|
|||||||
@ -1708,7 +1708,7 @@
|
|||||||
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};",
|
"delete_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το {{type}};",
|
||||||
"delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;",
|
"delete_folder_confirm": "Θέλετε να διαγράψετε τον φάκελο «{{name}}» και όλο το περιεχόμενό του;",
|
||||||
"delete_note_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη σημείωση \"{{name}}\";;",
|
"delete_note_confirm": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη σημείωση \"{{name}}\";;",
|
||||||
"drop_markdown_hint": "Σύρετε και αποθέστε αρχεία ή φακέλους Markdown εδώ για εισαγωγή",
|
"drop_markdown_hint": "Σύρετε και αποθέστε αρχεία ή φακέλους .md εδώ για εισαγωγή",
|
||||||
"empty": "δεν υπάρχει σημείωση για τώρα",
|
"empty": "δεν υπάρχει σημείωση για τώρα",
|
||||||
"expand": "να ανοίξει",
|
"expand": "να ανοίξει",
|
||||||
"export_failed": "Εξαγωγή στη βάση γνώσης απέτυχε",
|
"export_failed": "Εξαγωγή στη βάση γνώσης απέτυχε",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)",
|
"sort_updated_asc": "χρόνος ενημέρωσης (από παλιά στα νέα)",
|
||||||
"sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)",
|
"sort_updated_desc": "χρόνος ενημέρωσης (από νεώτερο σε παλαιότερο)",
|
||||||
"sort_z2a": "όνομα αρχείου (Z-A)",
|
"sort_z2a": "όνομα αρχείου (Z-A)",
|
||||||
"star": "Αποθήκευση",
|
"star": "Αγαπημένες σημειώσεις",
|
||||||
"starred_notes": "Σημειώσεις συλλογής",
|
"starred_notes": "Σημειώσεις συλλογής",
|
||||||
"title": "σημειώσεις",
|
"title": "σημειώσεις",
|
||||||
"unsaved_changes": "Έχετε μη αποθηκευμένο περιεχόμενο, είστε βέβαιοι ότι θέλετε να φύγετε;",
|
"unsaved_changes": "Έχετε μη αποθηκευμένο περιεχόμενο, είστε βέβαιοι ότι θέλετε να φύγετε;",
|
||||||
|
|||||||
@ -1708,7 +1708,7 @@
|
|||||||
"delete_confirm": "¿Estás seguro de que deseas eliminar este {{type}}?",
|
"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_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}}\"?",
|
"delete_note_confirm": "¿Está seguro de que desea eliminar la nota \"{{name}}\"?",
|
||||||
"drop_markdown_hint": "Arrastre y suelte archivos o carpetas de Markdown aquí para importar",
|
"drop_markdown_hint": "Arrastre y suelte archivos o carpetas de .md aquí para importar",
|
||||||
"empty": "Sin notas por el momento",
|
"empty": "Sin notas por el momento",
|
||||||
"expand": "expandir",
|
"expand": "expandir",
|
||||||
"export_failed": "Exportación a la base de conocimientos fallida",
|
"export_failed": "Exportación a la base de conocimientos fallida",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"sort_updated_asc": "Fecha de actualización (de más antigua a más reciente)",
|
"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_updated_desc": "Fecha de actualización (de más nuevo a más antiguo)",
|
||||||
"sort_z2a": "Nombre de archivo (Z-A)",
|
"sort_z2a": "Nombre de archivo (Z-A)",
|
||||||
"star": "Colección",
|
"star": "Notas guardadas",
|
||||||
"starred_notes": "notas guardadas",
|
"starred_notes": "notas guardadas",
|
||||||
"title": "notas",
|
"title": "notas",
|
||||||
"unsaved_changes": "Tienes contenido no guardado, ¿estás seguro de que quieres salir?",
|
"unsaved_changes": "Tienes contenido no guardado, ¿estás seguro de que quieres salir?",
|
||||||
|
|||||||
@ -1708,7 +1708,7 @@
|
|||||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer ce {{type}} ?",
|
"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_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}}\" ?",
|
"delete_note_confirm": "Êtes-vous sûr de vouloir supprimer la note \"{{name}}\" ?",
|
||||||
"drop_markdown_hint": "Déposez ici des fichiers ou dossiers Markdown pour les importer",
|
"drop_markdown_hint": "Déposez ici des fichiers ou dossiers .md pour les importer",
|
||||||
"empty": "Aucune note pour le moment",
|
"empty": "Aucune note pour le moment",
|
||||||
"expand": "développer",
|
"expand": "développer",
|
||||||
"export_failed": "Échec de l'exportation vers la base de connaissances",
|
"export_failed": "Échec de l'exportation vers la base de connaissances",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"sort_updated_asc": "Heure de mise à jour (du plus ancien au plus récent)",
|
"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_updated_desc": "Date de mise à jour (du plus récent au plus ancien)",
|
||||||
"sort_z2a": "Nom de fichier (Z-A)",
|
"sort_z2a": "Nom de fichier (Z-A)",
|
||||||
"star": "Favori",
|
"star": "Notes enregistrées",
|
||||||
"starred_notes": "notes de collection",
|
"starred_notes": "notes de collection",
|
||||||
"title": "notes",
|
"title": "notes",
|
||||||
"unsaved_changes": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter ?",
|
"unsaved_changes": "Vous avez des modifications non enregistrées, êtes-vous sûr de vouloir quitter ?",
|
||||||
|
|||||||
@ -1708,7 +1708,7 @@
|
|||||||
"delete_confirm": "この{{type}}を本当に削除しますか?",
|
"delete_confirm": "この{{type}}を本当に削除しますか?",
|
||||||
"delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?",
|
"delete_folder_confirm": "「{{name}}」フォルダーとそのすべての内容を削除してもよろしいですか?",
|
||||||
"delete_note_confirm": "メモ \"{{name}}\" を削除してもよろしいですか?",
|
"delete_note_confirm": "メモ \"{{name}}\" を削除してもよろしいですか?",
|
||||||
"drop_markdown_hint": "Markdown ファイルまたはディレクトリをここにドラッグ&ドロップしてインポートしてください",
|
"drop_markdown_hint": ".md ファイルまたはディレクトリをここにドラッグ&ドロップしてインポートしてください",
|
||||||
"empty": "暫無ノート",
|
"empty": "暫無ノート",
|
||||||
"expand": "展開",
|
"expand": "展開",
|
||||||
"export_failed": "知識ベースへのエクスポートに失敗しました",
|
"export_failed": "知識ベースへのエクスポートに失敗しました",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"sort_updated_asc": "更新日時(古い順)",
|
"sort_updated_asc": "更新日時(古い順)",
|
||||||
"sort_updated_desc": "更新日時(新しい順)",
|
"sort_updated_desc": "更新日時(新しい順)",
|
||||||
"sort_z2a": "ファイル名(Z-A)",
|
"sort_z2a": "ファイル名(Z-A)",
|
||||||
"star": "お気に入りに追加する",
|
"star": "お気に入りのノート",
|
||||||
"starred_notes": "収集したノート",
|
"starred_notes": "収集したノート",
|
||||||
"title": "ノート",
|
"title": "ノート",
|
||||||
"unsaved_changes": "保存されていないコンテンツがあります。本当に離れますか?",
|
"unsaved_changes": "保存されていないコンテンツがあります。本当に離れますか?",
|
||||||
|
|||||||
@ -1708,7 +1708,7 @@
|
|||||||
"delete_confirm": "Tem a certeza de que deseja eliminar este {{type}}?",
|
"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_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}}\"?",
|
"delete_note_confirm": "Tem a certeza de que deseja eliminar a nota \"{{name}}\"?",
|
||||||
"drop_markdown_hint": "Arraste e solte arquivos ou pastas Markdown aqui para importar",
|
"drop_markdown_hint": "Arraste e solte arquivos ou pastas .md aqui para importar",
|
||||||
"empty": "Ainda não existem notas",
|
"empty": "Ainda não existem notas",
|
||||||
"expand": "expandir",
|
"expand": "expandir",
|
||||||
"export_failed": "Falha ao exportar para a base de conhecimento",
|
"export_failed": "Falha ao exportar para a base de conhecimento",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"sort_updated_asc": "Tempo de atualização (do mais antigo para o mais recente)",
|
"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_updated_desc": "atualização de tempo (do mais novo para o mais antigo)",
|
||||||
"sort_z2a": "Nome do arquivo (Z-A)",
|
"sort_z2a": "Nome do arquivo (Z-A)",
|
||||||
"star": "coleções",
|
"star": "Notas favoritas",
|
||||||
"starred_notes": "notas salvas",
|
"starred_notes": "notas salvas",
|
||||||
"title": "nota",
|
"title": "nota",
|
||||||
"unsaved_changes": "Você tem conteúdo não salvo, tem certeza que deseja sair?",
|
"unsaved_changes": "Você tem conteúdo não salvo, tem certeza que deseja sair?",
|
||||||
|
|||||||
@ -1708,7 +1708,7 @@
|
|||||||
"delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?",
|
"delete_confirm": "Вы уверены, что хотите удалить этот объект {{type}}?",
|
||||||
"delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?",
|
"delete_folder_confirm": "Вы уверены, что хотите удалить папку \"{{name}}\" со всем ее содержимым?",
|
||||||
"delete_note_confirm": "Вы действительно хотите удалить заметку \"{{name}}\"?",
|
"delete_note_confirm": "Вы действительно хотите удалить заметку \"{{name}}\"?",
|
||||||
"drop_markdown_hint": "Перетащите сюда файлы или папки Markdown для импорта",
|
"drop_markdown_hint": "Перетащите сюда файлы или папки .md для импорта",
|
||||||
"empty": "заметок пока нет",
|
"empty": "заметок пока нет",
|
||||||
"expand": "развернуть",
|
"expand": "развернуть",
|
||||||
"export_failed": "Экспорт в базу знаний не выполнен",
|
"export_failed": "Экспорт в базу знаний не выполнен",
|
||||||
@ -1781,7 +1781,7 @@
|
|||||||
"sort_updated_asc": "Время обновления (от старого к новому)",
|
"sort_updated_asc": "Время обновления (от старого к новому)",
|
||||||
"sort_updated_desc": "Время обновления (от нового к старому)",
|
"sort_updated_desc": "Время обновления (от нового к старому)",
|
||||||
"sort_z2a": "Имя файла (Я-А)",
|
"sort_z2a": "Имя файла (Я-А)",
|
||||||
"star": "Сохранить",
|
"star": "Избранные заметки",
|
||||||
"starred_notes": "Сохраненные заметки",
|
"starred_notes": "Сохраненные заметки",
|
||||||
"title": "заметки",
|
"title": "заметки",
|
||||||
"unsaved_changes": "Вы не сохранили содержимое. Вы уверены, что хотите уйти?",
|
"unsaved_changes": "Вы не сохранили содержимое. Вы уверены, что хотите уйти?",
|
||||||
|
|||||||
@ -64,14 +64,14 @@ const KnowledgeBaseButton: FC<Props> = ({ ref, selectedBases, onSelect, disabled
|
|||||||
description: t('settings.input.clear.knowledge_base'),
|
description: t('settings.input.clear.knowledge_base'),
|
||||||
icon: <CircleX />,
|
icon: <CircleX />,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
action: () => {
|
action: ({ context: ctx }) => {
|
||||||
onSelect([])
|
onSelect([])
|
||||||
quickPanel.close()
|
ctx.close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
}, [knowledgeState.bases, t, selectedBases, handleBaseSelect, navigate, onSelect, quickPanel])
|
}, [knowledgeState.bases, t, selectedBases, handleBaseSelect, navigate, onSelect])
|
||||||
|
|
||||||
const openQuickPanel = useCallback(() => {
|
const openQuickPanel = useCallback(() => {
|
||||||
quickPanel.open({
|
quickPanel.open({
|
||||||
|
|||||||
@ -202,7 +202,7 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
icon: <CircleX />,
|
icon: <CircleX />,
|
||||||
alwaysVisible: true,
|
alwaysVisible: true,
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
action: () => {
|
action: ({ context: ctx }) => {
|
||||||
onClearMentionModels()
|
onClearMentionModels()
|
||||||
|
|
||||||
// 只有输入触发时才需要删除 @ 与搜索文本(未知搜索词,按光标就近删除)
|
// 只有输入触发时才需要删除 @ 与搜索文本(未知搜索词,按光标就近删除)
|
||||||
@ -214,7 +214,7 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
quickPanel.close()
|
ctx.close()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -227,7 +227,6 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
mentionedModels,
|
mentionedModels,
|
||||||
onMentionModel,
|
onMentionModel,
|
||||||
navigate,
|
navigate,
|
||||||
quickPanel,
|
|
||||||
onClearMentionModels,
|
onClearMentionModels,
|
||||||
setText,
|
setText,
|
||||||
removeAtSymbolAndText
|
removeAtSymbolAndText
|
||||||
@ -249,20 +248,20 @@ const MentionModelsButton: FC<Props> = ({
|
|||||||
afterAction({ item }) {
|
afterAction({ item }) {
|
||||||
item.isSelected = !item.isSelected
|
item.isSelected = !item.isSelected
|
||||||
},
|
},
|
||||||
onClose({ action, triggerInfo: closeTriggerInfo, searchText }) {
|
onClose({ action, searchText, context: ctx }) {
|
||||||
// ESC关闭时的处理:删除 @ 和搜索文本
|
// ESC关闭时的处理:删除 @ 和搜索文本
|
||||||
if (action === 'esc') {
|
if (action === 'esc') {
|
||||||
// 只有在输入触发且有模型选择动作时才删除@字符和搜索文本
|
// 只有在输入触发且有模型选择动作时才删除@字符和搜索文本
|
||||||
if (
|
if (
|
||||||
hasModelActionRef.current &&
|
hasModelActionRef.current &&
|
||||||
closeTriggerInfo?.type === 'input' &&
|
ctx.triggerInfo?.type === 'input' &&
|
||||||
closeTriggerInfo?.position !== undefined
|
ctx.triggerInfo?.position !== undefined
|
||||||
) {
|
) {
|
||||||
// 基于当前光标 + 搜索词精确定位并删除,position 仅作兜底
|
// 基于当前光标 + 搜索词精确定位并删除,position 仅作兜底
|
||||||
setText((currentText) => {
|
setText((currentText) => {
|
||||||
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement | null
|
const textArea = document.querySelector('.inputbar textarea') as HTMLTextAreaElement | null
|
||||||
const caret = textArea ? (textArea.selectionStart ?? currentText.length) : currentText.length
|
const caret = textArea ? (textArea.selectionStart ?? currentText.length) : currentText.length
|
||||||
return removeAtSymbolAndText(currentText, caret, searchText || '', closeTriggerInfo.position!)
|
return removeAtSymbolAndText(currentText, caret, searchText || '', ctx.triggerInfo?.position!)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
|
import { BreadcrumbItem, Breadcrumbs } from '@heroui/react'
|
||||||
import { loggerService } from '@logger'
|
import { loggerService } from '@logger'
|
||||||
import { NavbarCenter, NavbarHeader, NavbarRight } from '@renderer/components/app/Navbar'
|
import { NavbarCenter, NavbarHeader, NavbarRight } from '@renderer/components/app/Navbar'
|
||||||
import { HStack } from '@renderer/components/Layout'
|
import { HStack } from '@renderer/components/Layout'
|
||||||
import { useActiveNode } from '@renderer/hooks/useNotesQuery'
|
import { useActiveNode } from '@renderer/hooks/useNotesQuery'
|
||||||
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
||||||
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
|
import { useShowWorkspace } from '@renderer/hooks/useShowWorkspace'
|
||||||
import { findNodeInTree } from '@renderer/services/NotesTreeService'
|
import { findNodeByPath, findNodeInTree, updateNodeInTree } from '@renderer/services/NotesTreeService'
|
||||||
import { Breadcrumb, BreadcrumbProps, Dropdown, Tooltip } from 'antd'
|
import { NotesTreeNode } from '@types'
|
||||||
|
import { Dropdown, Tooltip } from 'antd'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
|
import { MoreHorizontal, PanelLeftClose, PanelRightClose, Star } from 'lucide-react'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
@ -19,7 +21,9 @@ const logger = loggerService.withContext('HeaderNavbar')
|
|||||||
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRef, currentContent }) => {
|
const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRef, currentContent }) => {
|
||||||
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
const { showWorkspace, toggleShowWorkspace } = useShowWorkspace()
|
||||||
const { activeNode } = useActiveNode(notesTree)
|
const { activeNode } = useActiveNode(notesTree)
|
||||||
const [breadcrumbItems, setBreadcrumbItems] = useState<Required<BreadcrumbProps>['items']>([])
|
const [breadcrumbItems, setBreadcrumbItems] = useState<
|
||||||
|
Array<{ key: string; title: string; treePath: string; isFolder: boolean }>
|
||||||
|
>([])
|
||||||
const { settings, updateSettings } = useNotesSettings()
|
const { settings, updateSettings } = useNotesSettings()
|
||||||
const canShowStarButton = activeNode?.type === 'file' && onToggleStar
|
const canShowStarButton = activeNode?.type === 'file' && onToggleStar
|
||||||
|
|
||||||
@ -48,6 +52,40 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRe
|
|||||||
}
|
}
|
||||||
}, [getCurrentNoteContent])
|
}, [getCurrentNoteContent])
|
||||||
|
|
||||||
|
const handleBreadcrumbClick = useCallback(
|
||||||
|
async (item: { treePath: string; isFolder: boolean }) => {
|
||||||
|
if (item.isFolder && notesTree) {
|
||||||
|
try {
|
||||||
|
// 获取从根目录到点击目录的所有路径片段
|
||||||
|
const pathParts = item.treePath.split('/').filter(Boolean)
|
||||||
|
const expandPromises: Promise<NotesTreeNode>[] = []
|
||||||
|
|
||||||
|
// 逐级展开从根到目标路径的所有文件夹
|
||||||
|
for (let i = 0; i < pathParts.length; i++) {
|
||||||
|
const currentPath = '/' + pathParts.slice(0, i + 1).join('/')
|
||||||
|
const folderNode = findNodeByPath(notesTree, currentPath)
|
||||||
|
|
||||||
|
if (folderNode && folderNode.type === 'folder' && !folderNode.expanded) {
|
||||||
|
expandPromises.push(updateNodeInTree(notesTree, folderNode.id, { expanded: true }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行执行所有展开操作
|
||||||
|
if (expandPromises.length > 0) {
|
||||||
|
await Promise.all(expandPromises)
|
||||||
|
logger.info('Expanded folder path from breadcrumb:', {
|
||||||
|
targetPath: item.treePath,
|
||||||
|
expandedCount: expandPromises.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to expand folder path from breadcrumb:', error as Error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[notesTree]
|
||||||
|
)
|
||||||
|
|
||||||
const handleExportPDFAction = useCallback(async () => {
|
const handleExportPDFAction = useCallback(async () => {
|
||||||
const menuContext: ExportContext = {
|
const menuContext: ExportContext = {
|
||||||
editorRef,
|
editorRef,
|
||||||
@ -118,9 +156,13 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRe
|
|||||||
|
|
||||||
const pathParts = node.treePath.split('/').filter(Boolean)
|
const pathParts = node.treePath.split('/').filter(Boolean)
|
||||||
const items = pathParts.map((part, index) => {
|
const items = pathParts.map((part, index) => {
|
||||||
|
const currentPath = '/' + pathParts.slice(0, index + 1).join('/')
|
||||||
|
const isLastItem = index === pathParts.length - 1
|
||||||
return {
|
return {
|
||||||
key: `path-${index}`,
|
key: `path-${index}`,
|
||||||
title: part
|
title: part,
|
||||||
|
treePath: currentPath,
|
||||||
|
isFolder: !isLastItem || node.type === 'folder'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -147,8 +189,20 @@ const HeaderNavbar = ({ notesTree, getCurrentNoteContent, onToggleStar, editorRe
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</HStack>
|
</HStack>
|
||||||
<NavbarCenter style={{ flex: 1 }}>
|
<NavbarCenter style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Breadcrumb items={breadcrumbItems} />
|
<BreadcrumbsContainer>
|
||||||
|
<Breadcrumbs>
|
||||||
|
{breadcrumbItems.map((item, index) => (
|
||||||
|
<BreadcrumbItem key={item.key} isCurrent={index === breadcrumbItems.length - 1}>
|
||||||
|
<BreadcrumbTitle
|
||||||
|
onClick={() => handleBreadcrumbClick(item)}
|
||||||
|
$clickable={item.isFolder && index < breadcrumbItems.length - 1}>
|
||||||
|
{item.title}
|
||||||
|
</BreadcrumbTitle>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
))}
|
||||||
|
</Breadcrumbs>
|
||||||
|
</BreadcrumbsContainer>
|
||||||
</NavbarCenter>
|
</NavbarCenter>
|
||||||
<NavbarRight style={{ paddingRight: 0 }}>
|
<NavbarRight style={{ paddingRight: 0 }}>
|
||||||
{canShowStarButton && (
|
{canShowStarButton && (
|
||||||
@ -237,4 +291,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
|
export default HeaderNavbar
|
||||||
|
|||||||
@ -538,6 +538,25 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
[onUploadFiles]
|
[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 (
|
return (
|
||||||
<SidebarContainer
|
<SidebarContainer
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
@ -576,7 +595,7 @@ const NotesSidebar: FC<NotesSidebarProps> = ({
|
|||||||
<NodeIcon>
|
<NodeIcon>
|
||||||
<FilePlus size={16} />
|
<FilePlus size={16} />
|
||||||
</NodeIcon>
|
</NodeIcon>
|
||||||
<DropHintText>{t('notes.drop_markdown_hint')}</DropHintText>
|
<DropHintText onClick={handleClickToSelectFiles}>{t('notes.drop_markdown_hint')}</DropHintText>
|
||||||
</TreeNodeContent>
|
</TreeNodeContent>
|
||||||
</TreeNodeContainer>
|
</TreeNodeContainer>
|
||||||
</DropHintNode>
|
</DropHintNode>
|
||||||
@ -738,12 +757,6 @@ const NodeName = styled.div`
|
|||||||
const EditInput = styled(Input)`
|
const EditInput = styled(Input)`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|
||||||
.ant-input {
|
|
||||||
font-size: 13px;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border: 0.5px solid var(--color-primary);
|
|
||||||
}
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const DragOverIndicator = styled.div`
|
const DragOverIndicator = styled.div`
|
||||||
|
|||||||
@ -69,18 +69,18 @@ const NotesSidebarHeader: FC<NotesSidebarHeaderProps> = ({
|
|||||||
<HeaderActions>
|
<HeaderActions>
|
||||||
{!isShowStarred && !isShowSearch && (
|
{!isShowStarred && !isShowSearch && (
|
||||||
<>
|
<>
|
||||||
<Tooltip title={t('notes.new_folder')} mouseEnterDelay={0.8}>
|
|
||||||
<ActionButton onClick={onCreateFolder}>
|
|
||||||
<FolderPlus size={18} />
|
|
||||||
</ActionButton>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<Tooltip title={t('notes.new_note')} mouseEnterDelay={0.8}>
|
<Tooltip title={t('notes.new_note')} mouseEnterDelay={0.8}>
|
||||||
<ActionButton onClick={onCreateNote}>
|
<ActionButton onClick={onCreateNote}>
|
||||||
<FilePlus2 size={18} />
|
<FilePlus2 size={18} />
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip title={t('notes.new_folder')} mouseEnterDelay={0.8}>
|
||||||
|
<ActionButton onClick={onCreateFolder}>
|
||||||
|
<FolderPlus size={18} />
|
||||||
|
</ActionButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Dropdown
|
<Dropdown
|
||||||
menu={{
|
menu={{
|
||||||
items: sortMenuWithCheck,
|
items: sortMenuWithCheck,
|
||||||
|
|||||||
@ -684,10 +684,15 @@ async function uploadSingleFile(
|
|||||||
const nameWithoutExt = fileName.replace(MARKDOWN_EXT, '')
|
const nameWithoutExt = fileName.replace(MARKDOWN_EXT, '')
|
||||||
|
|
||||||
let actualDirPath = originalDirPath
|
let actualDirPath = originalDirPath
|
||||||
let parentNode: NotesTreeNode | null
|
let parentNode: NotesTreeNode | null = null
|
||||||
|
|
||||||
if (originalDirPath === targetFolderPath) {
|
if (originalDirPath === targetFolderPath) {
|
||||||
parentNode =
|
parentNode =
|
||||||
tree.find((node) => node.externalPath === targetFolderPath) || findNodeByExternalPath(tree, targetFolderPath)
|
tree.find((node) => node.externalPath === targetFolderPath) || findNodeByExternalPath(tree, targetFolderPath)
|
||||||
|
|
||||||
|
if (!parentNode) {
|
||||||
|
logger.debug(`Uploading file ${fileName} to root directory: ${targetFolderPath}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
parentNode = createdFolders.get(originalDirPath) || null
|
parentNode = createdFolders.get(originalDirPath) || null
|
||||||
if (!parentNode) {
|
if (!parentNode) {
|
||||||
@ -696,22 +701,21 @@ async function uploadSingleFile(
|
|||||||
parentNode = findNodeByExternalPath(tree, originalDirPath)
|
parentNode = findNodeByExternalPath(tree, originalDirPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 如果找不到父节点,尝试通过 createdFolders 找到实际路径
|
if (!parentNode) {
|
||||||
if (!parentNode && originalDirPath !== targetFolderPath) {
|
for (const [originalPath, createdNode] of createdFolders.entries()) {
|
||||||
for (const [originalPath, createdNode] of createdFolders.entries()) {
|
if (originalPath === originalDirPath) {
|
||||||
if (originalPath === originalDirPath) {
|
parentNode = createdNode
|
||||||
parentNode = createdNode
|
actualDirPath = createdNode.externalPath
|
||||||
actualDirPath = createdNode.externalPath
|
break
|
||||||
break
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!parentNode) {
|
if (!parentNode) {
|
||||||
logger.error(`Cannot upload file ${fileName}: parent node not found for path ${originalDirPath}`)
|
logger.error(`Cannot upload file ${fileName}: parent node not found for path ${originalDirPath}`)
|
||||||
return null
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { safeName, exists } = await window.api.file.checkFileName(actualDirPath, nameWithoutExt, true)
|
const { safeName, exists } = await window.api.file.checkFileName(actualDirPath, nameWithoutExt, true)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user