From 9d81fd0f11124f252528ede793186ae65f3624a8 Mon Sep 17 00:00:00 2001 From: Devon22 Date: Mon, 29 Jun 2026 23:55:42 +0800 Subject: [PATCH] 3.4.11 --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/GridPreviewManager.ts | 423 +++++++++++++++++--------------------- src/GridView.ts | 88 +++++++- src/translations/en.ts | 1 + src/translations/ja.ts | 1 + src/translations/ko.ts | 1 + src/translations/ru.ts | 1 + src/translations/uk.ts | 1 + src/translations/zh-TW.ts | 1 + src/translations/zh.ts | 1 + styles.css | 8 +- 13 files changed, 283 insertions(+), 251 deletions(-) diff --git a/manifest.json b/manifest.json index ed22f17..e97823c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "3.4.10", + "version": "3.4.11", "minAppVersion": "1.8.7", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index dfe78d7..21587cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "3.4.10", + "version": "3.4.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "3.4.10", + "version": "3.4.11", "license": "MIT", "dependencies": { "jszip": "^3.10.1" diff --git a/package.json b/package.json index e282281..c8b4802 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "3.4.10", + "version": "3.4.11", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/GridPreviewManager.ts b/src/GridPreviewManager.ts index dc10a02..672f4e4 100644 --- a/src/GridPreviewManager.ts +++ b/src/GridPreviewManager.ts @@ -68,7 +68,190 @@ export class GridPreviewManager { } } + // 註冊預覽視窗的行動裝置滑動手勢(包含上下拖曳關閉與左右滑動切換歷史記錄) + private registerPreviewTouchEvents( + container: HTMLElement, + scrollContainer: HTMLElement, + onClose: () => void + ) { + if (!Platform.isMobile) return; + container.setCssProps({ touchAction: 'pan-y' }); + + let startY = 0; + let startX = 0; + let currentY = 0; + let currentX = 0; + let isPullingUp = false; + let isDragging = false; + let isHorizontalDragging = false; + let initialScrollTop = 0; + let isAtTop = false; + let isAtBottom = false; + + const handleTouchStart = (e: TouchEvent) => { + if (e.touches.length > 1) return; + + const target = e.target as HTMLElement; + if (target.closest('input') || target.closest('textarea')) { + return; + } + + initialScrollTop = scrollContainer.scrollTop; + isAtTop = initialScrollTop <= 0; + isAtBottom = initialScrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 1; + + startY = e.touches[0].clientY; + currentY = startY; + startX = e.touches[0].clientX; + currentX = startX; + + isDragging = false; + isPullingUp = false; + isHorizontalDragging = false; + }; + + const handleTouchMove = (e: TouchEvent) => { + if (e.touches.length > 1) return; + + currentY = e.touches[0].clientY; + currentX = e.touches[0].clientX; + const deltaY = currentY - startY; + const deltaX = currentX - startX; + + // 決定滑動方向 + if (!isDragging && !isHorizontalDragging) { + const threshold = 10; + if (Math.abs(deltaX) > threshold || Math.abs(deltaY) > threshold) { + if (Math.abs(deltaX) > Math.abs(deltaY)) { + // 左右滑動 + const hasForwardHistory = this.previewHistoryIndex < this.previewHistory.length - 1; + if (deltaX < 0 && !hasForwardHistory) { + // 禁用該方向,不設為 dragging + } else { + isHorizontalDragging = true; + container.setCssProps({ transition: 'none' }); + } + } else { + // 上下滑動 (原有拉動關閉功能) + if (isAtTop || isAtBottom) { + const pullDownStartThreshold = 24; + const pullUpStartThreshold = 36; + const canPullDown = isAtTop && deltaY > pullDownStartThreshold; + const canPullUp = isAtBottom && deltaY < -pullUpStartThreshold; + + if (canPullDown || canPullUp) { + isDragging = true; + isPullingUp = canPullUp && deltaY < 0; + container.setCssProps({ transition: 'none' }); + } + } + } + } + } + + // 只有在進入我們自己定義的拖曳狀態時,才攔截事件與阻止預設行為 + if (isHorizontalDragging || isDragging) { + e.stopPropagation(); + if (e.cancelable) { + e.preventDefault(); + } + } + + // 處理左右滑動視覺反饋 + if (isHorizontalDragging) { + const hasForwardHistory = this.previewHistoryIndex < this.previewHistory.length - 1; + let translateX = deltaX * 0.6; + if (!hasForwardHistory && translateX < 0) { + translateX = 0; + } + + if (translateX !== 0) { + container.setCssProps({ transform: `translateX(${translateX}px)` }); + } + } + + // 處理上下滑動視覺反饋 + if (isDragging) { + const resistance = 0.5; + const translateY = deltaY * resistance; + container.setCssProps({ transform: `translateY(${translateY}px)` }); + } + }; + + const handleTouchEnd = (e: TouchEvent) => { + if (isHorizontalDragging || isDragging) { + e.stopPropagation(); + } + + const deltaX = currentX - startX; + const deltaY = currentY - startY; + + if (isHorizontalDragging) { + isHorizontalDragging = false; + const swipeThreshold = 80; + + const hasForwardHistory = this.previewHistoryIndex < this.previewHistory.length - 1; + const isValidSwipe = !(deltaX < 0 && !hasForwardHistory); + + if (isValidSwipe && Math.abs(deltaX) > swipeThreshold) { + if (deltaX > 0) { + void this.navigatePreviewBack(); + } else { + void this.navigatePreviewForward(); + } + container.setCssProps({ + transform: '', + transition: '', + }); + } else { + container.setCssProps({ + transition: 'transform 0.3s ease-out', + transform: 'translateX(0)', + }); + window.setTimeout(() => { + container.setCssProps({ + transform: '', + transition: '', + }); + }, 300); + } + } else if (isDragging) { + isDragging = false; + + const closeThreshold = isPullingUp ? 170 : 110; + if ((!isPullingUp && deltaY > closeThreshold) || (isPullingUp && deltaY < -closeThreshold)) { + const targetY = isPullingUp ? '-100vh' : '100vh'; + container.setCssProps({ + transition: 'transform 0.2s ease-out', + transform: `translateY(${targetY})`, + }); + window.setTimeout(() => { + onClose(); + container.setCssProps({ + transform: '', + transition: '', + }); + }, 200); + } else { + container.setCssProps({ + transition: 'transform 0.3s ease-out', + transform: 'translateY(0)', + }); + window.setTimeout(() => { + container.setCssProps({ + transform: '', + transition: '', + }); + }, 300); + } + } + }; + + container.addEventListener('touchstart', handleTouchStart, { capture: true, passive: false }); + container.addEventListener('touchmove', handleTouchMove, { capture: true, passive: false }); + container.addEventListener('touchend', handleTouchEnd, { capture: true }); + } // 在網格視圖中直接顯示筆記 async showNoteInGrid(file: TFile, isHistoryNavigation = false) { @@ -444,124 +627,8 @@ export class GridPreviewManager { await this.renderLinksSection(file, noteContent); - // 行動裝置下拉或上拉關閉筆記 - if (Platform.isMobile && noteViewContainer) { - let startY = 0; - let startX = 0; - let currentY = 0; - let isPulling = false; - let isPullingUp = false; - let isDragging = false; - let initialScrollTop = 0; - let isAtTop = false; - let isAtBottom = false; - - const handleTouchStart = (e: TouchEvent) => { - const target = e.target as HTMLElement; - if (target.closest('button') || target.closest('a') || target.closest('input') || target.closest('textarea')) { - return; - } - - initialScrollTop = scrollContainer.scrollTop; - isAtTop = initialScrollTop <= 0; - isAtBottom = initialScrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 1; - - if (isAtTop || isAtBottom) { - startY = e.touches[0].clientY; - currentY = startY; - startX = e.touches[0].clientX; - isPulling = true; - isDragging = false; - isPullingUp = false; - } - }; - - const handleTouchMove = (e: TouchEvent) => { - if (!isPulling || !noteViewContainer) return; - - currentY = e.touches[0].clientY; - const currentX = e.touches[0].clientX; - const deltaY = currentY - startY; - const deltaX = currentX - startX; - - if (!isDragging) { - if (Math.abs(deltaX) > Math.abs(deltaY)) { - isPulling = false; - return; - } - - const pullDownStartThreshold = 24; - const pullUpStartThreshold = 36; - const canPullDown = isAtTop && deltaY > pullDownStartThreshold; - const canPullUp = isAtBottom && deltaY < -pullUpStartThreshold; - - if (canPullDown || canPullUp) { - isDragging = true; - isPullingUp = canPullUp && deltaY < 0; - if (noteViewContainer) { - noteViewContainer.setCssProps({ transition: 'none' }); - } - } else if ((isAtTop && !isAtBottom && deltaY < 0) || (isAtBottom && !isAtTop && deltaY > 0)) { - isPulling = false; - return; - } - } - - if (isDragging) { - if (e.cancelable) { - e.preventDefault(); - } - const resistance = 0.5; - const translateY = deltaY * resistance; - noteViewContainer.setCssProps({ transform: `translateY(${translateY}px)` }); - } - }; - - const handleTouchEnd = () => { - if (!isPulling || !noteViewContainer) return; - isPulling = false; - - if (!isDragging) return; - isDragging = false; - - const deltaY = currentY - startY; - - const closeThreshold = isPullingUp ? 170 : 110; - if ((!isPullingUp && deltaY > closeThreshold) || (isPullingUp && deltaY < -closeThreshold)) { - const targetY = isPullingUp ? '-100vh' : '100vh'; - noteViewContainer.setCssProps({ - transition: 'transform 0.2s ease-out', - transform: `translateY(${targetY})`, - }); - window.setTimeout(() => { - this.hideNoteInGrid(); - if (noteViewContainer) { - noteViewContainer.setCssProps({ - transform: '', - transition: '', - }); - } - }, 200); - } else { - noteViewContainer.setCssProps({ - transition: 'transform 0.3s ease-out', - transform: 'translateY(0)', - }); - window.setTimeout(() => { - if (noteViewContainer) { - noteViewContainer.setCssProps({ - transform: '', - transition: '', - }); - } - }, 300); - } - }; - - noteViewContainer.addEventListener('touchstart', handleTouchStart, { passive: true }); - noteViewContainer.addEventListener('touchmove', handleTouchMove, { passive: false }); - noteViewContainer.addEventListener('touchend', handleTouchEnd); - } + // 註冊行動裝置滑動手勢 + this.registerPreviewTouchEvents(noteViewContainer, scrollContainer, () => this.hideNoteInGrid()); // 設定狀態 this.view.isShowingNote = true; @@ -866,124 +933,8 @@ export class GridPreviewManager { console.error('Error loading ZIP content in grid:', error); } - // 行動裝置下拉關閉 - if (Platform.isMobile && zipViewContainer) { - let startY = 0; - let startX = 0; - let currentY = 0; - let isPulling = false; - let isPullingUp = false; - let isDragging = false; - let initialScrollTop = 0; - let isAtTop = false; - let isAtBottom = false; - - const handleTouchStart = (e: TouchEvent) => { - const target = e.target as HTMLElement; - if (target.closest('button') || target.closest('a') || target.closest('input') || target.closest('textarea')) { - return; - } - - initialScrollTop = scrollContainer.scrollTop; - isAtTop = initialScrollTop <= 0; - isAtBottom = initialScrollTop + scrollContainer.clientHeight >= scrollContainer.scrollHeight - 1; - - if (isAtTop || isAtBottom) { - startY = e.touches[0].clientY; - currentY = startY; - startX = e.touches[0].clientX; - isPulling = true; - isDragging = false; - isPullingUp = false; - } - }; - - const handleTouchMove = (e: TouchEvent) => { - if (!isPulling || !zipViewContainer) return; - - currentY = e.touches[0].clientY; - const currentX = e.touches[0].clientX; - const deltaY = currentY - startY; - const deltaX = currentX - startX; - - if (!isDragging) { - if (Math.abs(deltaX) > Math.abs(deltaY)) { - isPulling = false; - return; - } - - const pullDownStartThreshold = 24; - const pullUpStartThreshold = 36; - const canPullDown = isAtTop && deltaY > pullDownStartThreshold; - const canPullUp = isAtBottom && deltaY < -pullUpStartThreshold; - - if (canPullDown || canPullUp) { - isDragging = true; - isPullingUp = canPullUp && deltaY < 0; - if (zipViewContainer) { - zipViewContainer.setCssProps({ transition: 'none' }); - } - } else if ((isAtTop && !isAtBottom && deltaY < 0) || (isAtBottom && !isAtTop && deltaY > 0)) { - isPulling = false; - return; - } - } - - if (isDragging) { - if (e.cancelable) { - e.preventDefault(); - } - const resistance = 0.5; - const translateY = deltaY * resistance; - zipViewContainer.setCssProps({ transform: `translateY(${translateY}px)` }); - } - }; - - const handleTouchEnd = () => { - if (!isPulling || !zipViewContainer) return; - isPulling = false; - - if (!isDragging) return; - isDragging = false; - - const deltaY = currentY - startY; - - const closeThreshold = isPullingUp ? 170 : 110; - if ((!isPullingUp && deltaY > closeThreshold) || (isPullingUp && deltaY < -closeThreshold)) { - const targetY = isPullingUp ? '-100vh' : '100vh'; - zipViewContainer.setCssProps({ - transition: 'transform 0.2s ease-out', - transform: `translateY(${targetY})`, - }); - window.setTimeout(() => { - this.hideZipInGrid(); - if (zipViewContainer) { - zipViewContainer.setCssProps({ - transform: '', - transition: '', - }); - } - }, 200); - } else { - zipViewContainer.setCssProps({ - transition: 'transform 0.3s ease-out', - transform: 'translateY(0)', - }); - window.setTimeout(() => { - if (zipViewContainer) { - zipViewContainer.setCssProps({ - transform: '', - transition: '', - }); - } - }, 300); - } - }; - - zipViewContainer.addEventListener('touchstart', handleTouchStart, { passive: true }); - zipViewContainer.addEventListener('touchmove', handleTouchMove, { passive: false }); - zipViewContainer.addEventListener('touchend', handleTouchEnd); - } + // 註冊行動裝置滑動手勢 + this.registerPreviewTouchEvents(zipViewContainer, scrollContainer, () => this.hideZipInGrid()); // 設定狀態 this.view.isShowingZip = true; diff --git a/src/GridView.ts b/src/GridView.ts index 351a403..a15a20a 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -243,12 +243,12 @@ export class GridView extends ItemView { if (this.app.workspace.getActiveViewOfType(GridView) !== this) return; // 有 modal 時不處理 if (activeDocument.querySelector('.modal-container')) return; - + // 阻止內建快捷鍵與其他監聽器 event.preventDefault(); // 停止後續所有監聽器(包含 Obsidian 內建 hotkey) event.stopImmediatePropagation(); - + if (this.isShowingNote || this.isShowingZip) { void this.previewManager.navigatePreviewBack(); } else { @@ -260,12 +260,12 @@ export class GridView extends ItemView { if (this.app.workspace.getActiveViewOfType(GridView) !== this) return; // 有 modal 時不處理 if (activeDocument.querySelector('.modal-container')) return; - + // 阻止內建快捷鍵與其他監聽器 event.preventDefault(); // 停止後續所有監聽器(包含 Obsidian 內建 hotkey) event.stopImmediatePropagation(); - + if (this.isShowingNote || this.isShowingZip) { void this.previewManager.navigatePreviewForward(); } else { @@ -277,7 +277,7 @@ export class GridView extends ItemView { if (this.app.workspace.getActiveViewOfType(GridView) !== this) return; // 有 modal 時不處理 if (activeDocument.querySelector('.modal-container')) return; - + // 若正在顯示預覽,則關閉預覽 if (this.isShowingNote || this.isShowingZip) { event.preventDefault(); @@ -325,6 +325,80 @@ export class GridView extends ItemView { }) ); } + + // 監聽外部檔案拖曳進來並複製到目前資料夾 + this.registerDomEvent(this.containerEl, 'dragover', (event: DragEvent) => { + if (this.sourceMode === 'folder') { + const types = event.dataTransfer?.types || []; + // 確保為外部系統檔案拖曳,排除內部元素拖曳 (內部拖曳通常含有 text/html 或 text/plain) + const isExternalFile = types.includes('Files') && !types.includes('text/html') && !types.includes('text/plain'); + if (isExternalFile) { + event.preventDefault(); + if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; + this.containerEl.addClass('ge-dragover'); + } + } + }); + + this.registerDomEvent(this.containerEl, 'dragleave', () => { + this.containerEl.removeClass('ge-dragover'); + }); + + this.registerDomEvent(this.containerEl, 'drop', async (event: DragEvent) => { + if (this.sourceMode !== 'folder') return; + this.containerEl.removeClass('ge-dragover'); + + if (!event.dataTransfer) return; + + const types = event.dataTransfer.types || []; + const isExternalFile = types.includes('Files') && !types.includes('text/html') && !types.includes('text/plain'); + + // 如果不是外部檔案,直接返回(交給其他已經寫好搬移邏輯的處理器處理,不攔截) + if (!isExternalFile) return; + + // 處理外部檔案的複製匯入 (Copy/Import) + event.preventDefault(); + event.stopPropagation(); + + const files = event.dataTransfer.files; + if (!files || files.length === 0) return; + + let importedCount = 0; + for (let i = 0; i < files.length; i++) { + const file = files[i]; + try { + const arrayBuffer = await file.arrayBuffer(); + + // 處理檔名重複 + let baseName = file.name; + let extension = ''; + const lastDotIndex = file.name.lastIndexOf('.'); + if (lastDotIndex !== -1) { + baseName = file.name.substring(0, lastDotIndex); + extension = file.name.substring(lastDotIndex); + } + + let targetPath = normalizePath(`${this.sourcePath}/${file.name}`); + let counter = 1; + while (this.app.vault.getAbstractFileByPath(targetPath)) { + targetPath = normalizePath(`${this.sourcePath}/${baseName}_${counter}${extension}`); + counter++; + } + + await this.app.vault.createBinary(targetPath, arrayBuffer); + importedCount++; + } catch (err) { + console.error(`GridExplorer: Failed to import file ${file.name}`, err); + new Notice(t('import_fail_notice')); + } + } + + if (importedCount > 0) { + if (!this.fileWatcher) { + await this.render(); + } + } + }); } getViewType() { @@ -2162,7 +2236,7 @@ export class GridView extends ItemView { } else { copyPath = `${f.basename} 1.${f.extension}`; } - + let counter = 1; while (this.app.vault.getAbstractFileByPath(copyPath)) { counter++; @@ -2172,7 +2246,7 @@ export class GridView extends ItemView { copyPath = `${f.basename} ${counter}.${f.extension}`; } } - + try { await this.app.vault.copy(f, copyPath); } catch (err) { diff --git a/src/translations/en.ts b/src/translations/en.ts index a499672..e37831d 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -332,4 +332,5 @@ export default { 'save_stash_as_markdown': 'Save stash as Markdown', 'add_to_stash': 'Add to stash', 'added_to_stash': 'Added to stash', + 'import_fail_notice': 'Import failed', }; diff --git a/src/translations/ja.ts b/src/translations/ja.ts index b4bec08..10bd068 100644 --- a/src/translations/ja.ts +++ b/src/translations/ja.ts @@ -331,4 +331,5 @@ export default { 'save_stash_as_markdown': '一時保存をMarkdownとして保存', 'add_to_stash': '一時保存に追加', 'added_to_stash': '一時保存に追加されました', + 'import_fail_notice': 'インポートに失敗しました', }; diff --git a/src/translations/ko.ts b/src/translations/ko.ts index 40de01f..53cfbab 100644 --- a/src/translations/ko.ts +++ b/src/translations/ko.ts @@ -332,4 +332,5 @@ export default { 'save_stash_as_markdown': '보관함을 Markdown으로 저장', 'add_to_stash': '보관함에 추가', 'added_to_stash': '보관함에 추가됨', + 'import_fail_notice': '가져오기 실패', }; diff --git a/src/translations/ru.ts b/src/translations/ru.ts index eebd6c1..b568b7d 100644 --- a/src/translations/ru.ts +++ b/src/translations/ru.ts @@ -331,4 +331,5 @@ export default { 'save_stash_as_markdown': 'Сохранить хранилище как Markdown', 'add_to_stash': 'Добавить в хранилище', 'added_to_stash': 'Добавлено в хранилище', + 'import_fail_notice': 'Ошибка импорта', }; diff --git a/src/translations/uk.ts b/src/translations/uk.ts index 12ffe55..d95af6c 100644 --- a/src/translations/uk.ts +++ b/src/translations/uk.ts @@ -331,4 +331,5 @@ export default { 'save_stash_as_markdown': 'Зберегти сховище як Markdown', 'add_to_stash': 'Додати до сховища', 'added_to_stash': 'Додано до сховища', + 'import_fail_notice': 'Помилка імпорту', }; diff --git a/src/translations/zh-TW.ts b/src/translations/zh-TW.ts index b5b7b3e..c0c2266 100644 --- a/src/translations/zh-TW.ts +++ b/src/translations/zh-TW.ts @@ -332,4 +332,5 @@ export default { 'save_stash_as_markdown': '將暫存區存為 Markdown', 'add_to_stash': '加入暫存區', 'added_to_stash': '已添加到暫存區', + 'import_fail_notice': '匯入失敗', }; diff --git a/src/translations/zh.ts b/src/translations/zh.ts index d795620..5a38de7 100644 --- a/src/translations/zh.ts +++ b/src/translations/zh.ts @@ -331,4 +331,5 @@ export default { 'save_stash_as_markdown': '将暂存区保存为 Markdown', 'add_to_stash': '加入暂存区', 'added_to_stash': '已添加到暂存区', + 'import_fail_notice': '导入失败', }; diff --git a/styles.css b/styles.css index 938f2e3..6d31d3e 100644 --- a/styles.css +++ b/styles.css @@ -49,11 +49,11 @@ } @media (hover: hover) { - .ge-grid-view-container .ge-grid-container .ge-grid-item:hover { + body .ge-grid-view-container .ge-grid-container .ge-grid-item:hover { transform: translateY(-2px); background-color: var(--text-selection); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-color: var(--interactive-accent) !important; + border-color: var(--interactive-accent); } .ge-grid-item:hover p { @@ -3428,11 +3428,11 @@ a.ge-current-folder:hover { } @media (hover: hover) { - .ge-folder-button:hover { + body .ge-folder-button:hover { background-color: var(--text-selection); transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); - border-color: var(--interactive-accent) !important; + border-color: var(--interactive-accent); } }