From 47037a90cedfd0c69e456ab891c5a66eb15acfe4 Mon Sep 17 00:00:00 2001 From: Devon22 Date: Sat, 21 Feb 2026 19:25:57 +0800 Subject: [PATCH] 3.1.4 --- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/GridView.ts | 128 +++++++++++++++++++++++++++++++++------------- src/main.ts | 48 +---------------- styles.css | 40 ++++++++++----- versions.json | 2 +- 7 files changed, 126 insertions(+), 100 deletions(-) diff --git a/manifest.json b/manifest.json index 09210b1..6a24c6a 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "gridexplorer", "name": "GridExplorer", - "version": "3.1.3", + "version": "3.1.4", "minAppVersion": "1.1.0", "description": "Browse note files in a grid view.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index c76de2d..4b15da2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gridexplorer", - "version": "3.1.3", + "version": "3.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gridexplorer", - "version": "3.1.3", + "version": "3.1.4", "license": "MIT", "dependencies": { "@esbuild/linux-x64": "0.17.3", diff --git a/package.json b/package.json index fd3100d..751d46b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gridexplorer", - "version": "3.1.3", + "version": "3.1.4", "description": "Browse note files in a grid view.", "main": "main.js", "scripts": { diff --git a/src/GridView.ts b/src/GridView.ts index e0adafb..c1a5148 100644 --- a/src/GridView.ts +++ b/src/GridView.ts @@ -67,6 +67,7 @@ export class GridView extends ItemView { isShowingNote: boolean = false; // 是否正在顯示筆記 noteViewContainer: HTMLElement | null = null; // 筆記檢視容器 eventCleanupFunctions: (() => void)[] = []; // 存儲事件清理函數 + targetFocusPath: string | null = null; // 指定下次渲染要對焦的檔案路徑 constructor(leaf: WorkspaceLeaf, plugin: GridExplorerPlugin) { super(leaf); @@ -1063,45 +1064,102 @@ export class GridView extends ItemView { const state: DividerState = { lastDateString: lastDateString, pinDividerAdded: pinDividerAdded, blankDividerAdded: blankDividerAdded }; const paramsBase: FileRenderParams = { container, observer, files, dateDividerMode, sortType, shouldShowDateDividers, state }; const selfRef = this; - - if (Platform.isIosApp) { - // iOS 專用:以 time-slice 方式分批,避免阻塞點擊事件 - const TIME_BUDGET_MS = 6; // 每幀最多執行 6ms - const processChunk = (start: number) => { - if (currentToken !== this.renderToken) return; + const batchSize = 50; + let currentIndex = 0; + + // 建立哨兵元素,用於觸發後續載入 + const sentinel = container.createDiv('ge-load-more-sentinel'); + sentinel.style.height = '1px'; + sentinel.style.width = '100%'; + sentinel.style.flexShrink = '0'; + + let isLoading = false; + + const loadMore = (entries?: IntersectionObserverEntry[]) => { + if (currentToken !== selfRef.renderToken) return; + + if (entries && entries[0] && !entries[0].isIntersecting) { + return; + } + + if (isLoading) return; + isLoading = true; + + // 暫時移除哨兵 + sentinel.remove(); + + let targetMaxEnd = currentIndex + batchSize; + if (selfRef.targetFocusPath) { + const tIndex = files.findIndex(f => f.path === selfRef.targetFocusPath); + if (tIndex >= currentIndex) { + targetMaxEnd = Math.max(targetMaxEnd, tIndex + 1); + } + } + const end = Math.min(targetMaxEnd, files.length); + const TIME_BUDGET_MS = Platform.isIosApp ? 6 : 16; + + const renderChunk = () => { + if (currentToken !== selfRef.renderToken) return; + const startTime = performance.now(); - let i = start; - for (; i < files.length; i++) { - selfRef.processFile(files[i], paramsBase); - if (performance.now() - startTime > TIME_BUDGET_MS) { - break; // 超過時間預算,讓出主執行緒 - } + while (currentIndex < end && (performance.now() - startTime) < TIME_BUDGET_MS) { + selfRef.processFile(files[currentIndex], paramsBase); + currentIndex++; } - if (i < files.length) { - requestAnimationFrame(() => processChunk(i)); // 下一幀繼續 + + if (currentIndex < end) { + requestAnimationFrame(renderChunk); + } else { + // 這一批次處理完畢 + if (selfRef.targetFocusPath) { + // 尋找剛剛載入的目標項目並捲動到該位置 + const gridItem = Array.from(container.querySelectorAll('.ge-grid-item')).find( + item => (item as HTMLElement).dataset.filePath === selfRef.targetFocusPath + ) as HTMLElement; + + if (gridItem) { + gridItem.scrollIntoView({ block: 'nearest' }); + const itemIndex = selfRef.gridItems.indexOf(gridItem); + if (itemIndex >= 0) { + selfRef.selectItem(itemIndex); + } + selfRef.targetFocusPath = null; // 處理完清除目標 + } + } + + if (currentIndex < files.length) { + // 還有剩餘檔案,將哨兵加回底部 + container.appendChild(sentinel); + + // 主動判斷是否仍在可視範圍內(解決捲動跳轉後 IntersectionObserver 未觸發的問題) + setTimeout(() => { + if (currentToken !== selfRef.renderToken) return; + // 由於不一定是在 root container,也可以透過 getBoundingClientRect 簡單測算 + const rect = sentinel.getBoundingClientRect(); + const containerRect = container.getBoundingClientRect(); + if (rect.top - containerRect.bottom < 400) { + loadMore(); + } + }, 50); + } + + isLoading = false; } }; - processChunk(0); - } else { - // 其他平台維持原本固定 batchSize 的邏輯 - const batchSize = 50; - const processBatch = (start: number) => { - if (currentToken !== this.renderToken) return; - const end = Math.min(start + batchSize, files.length); - for (let i = start; i < end; i++) { - selfRef.processFile(files[i], paramsBase); - } - if (end < files.length) { - const cb = () => processBatch(end); - if (typeof (window as any).requestIdleCallback === 'function') { - (window as any).requestIdleCallback(cb); - } else { - setTimeout(cb, 0); - } - } - }; - processBatch(0); - } + + renderChunk(); + }; + + const sentinelObserver = new IntersectionObserver(loadMore, { + root: container, + rootMargin: '400px', // 提早 400px 載入 + threshold: 0 + }); + + sentinelObserver.observe(sentinel); + + // 初始載入 + loadMore(); } if (this.plugin.statusBarItem) { diff --git a/src/main.ts b/src/main.ts index 0efd78a..0513b42 100644 --- a/src/main.ts +++ b/src/main.ts @@ -629,30 +629,8 @@ export default class GridExplorerPlugin extends Plugin { async openNoteInRecentFiles(file: TFile) { const view = await this.activateView(); if (view instanceof GridView) { + view.targetFocusPath = file instanceof TFile ? file.path : null; await view.setSource('recent-files'); - // 如果是文件,等待視圖渲染完成後捲動到該文件位置 - if (file instanceof TFile) { - // 等待下一個事件循環以確保視圖已完全渲染 - requestAnimationFrame(() => { - const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement; - if (!gridContainer) return; - - // 找到對應的網格項目 - const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find( - item => (item as HTMLElement).dataset.filePath === file.path - ) as HTMLElement; - - if (gridItem) { - // 捲動到該項目的位置 - gridItem.scrollIntoView({ block: 'nearest' }); - // 選中該項目 - const itemIndex = view.gridItems.indexOf(gridItem); - if (itemIndex >= 0) { - view.selectItem(itemIndex); - } - } - }); - } } } @@ -662,30 +640,8 @@ export default class GridExplorerPlugin extends Plugin { const folderPath = file ? (file instanceof TFile ? file.parent?.path : file.path) : "/"; const view = await this.activateView(); if (view instanceof GridView) { + view.targetFocusPath = file instanceof TFile ? file.path : null; await view.setSource('folder', folderPath); - // 如果是文件,等待視圖渲染完成後捲動到該文件位置 - if (file instanceof TFile) { - // 等待下一個事件循環以確保視圖已完全渲染 - requestAnimationFrame(() => { - const gridContainer = view.containerEl.querySelector('.ge-grid-container') as HTMLElement; - if (!gridContainer) return; - - // 找到對應的網格項目 - const gridItem = Array.from(gridContainer.querySelectorAll('.ge-grid-item')).find( - item => (item as HTMLElement).dataset.filePath === file.path - ) as HTMLElement; - - if (gridItem) { - // 捲動到該項目的位置 - gridItem.scrollIntoView({ block: 'nearest' }); - // 選中該項目 - const itemIndex = view.gridItems.indexOf(gridItem); - if (itemIndex >= 0) { - view.selectItem(itemIndex); - } - } - }); - } } } diff --git a/styles.css b/styles.css index 068bd7e..31267b1 100644 --- a/styles.css +++ b/styles.css @@ -40,14 +40,20 @@ height: var(--grid-item-height); } -.ge-grid-item:hover { - transform: translateY(-2px); - background-color: var(--text-selection) !important; - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +@media (hover: hover) { + .ge-grid-item:hover { + transform: translateY(-2px); + background-color: var(--text-selection) !important; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + + .ge-grid-item:hover p { + color: var(--text-muted); + } } -.ge-grid-item:hover p { - color: var(--text-muted); +.ge-grid-item { + -webkit-tap-highlight-color: transparent; } /* 右上角懸浮開啟筆記按鈕 Hover Open-in-Grid Button */ @@ -287,8 +293,10 @@ } /* 鼠標懸停效果:整張卡片微抬升 */ -.ge-vertical-card .ge-grid-item:hover { - transform: translateY(-2px); +@media (hover: hover) { + .ge-vertical-card .ge-grid-item:hover { + transform: translateY(-2px); + } } /* 僅圖片/影片的直向卡片 Media Card */ @@ -391,10 +399,12 @@ height: 100%; } -.ge-grid-item.ge-folder-item:hover { - background-color: var(--background-modifier-hover); - transform: translateY(-1px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +@media (hover: hover) { + .ge-grid-item.ge-folder-item:hover { + background-color: var(--background-modifier-hover); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } } /* 直向卡片模式下的資料夾項目補正 */ @@ -525,8 +535,10 @@ background-color: var(--background-modifier-border); } -.ge-grid-item.ge-note-hidden:hover { - opacity: 0.7; +@media (hover: hover) { + .ge-grid-item.ge-note-hidden:hover { + opacity: 0.7; + } } .ge-grid-item.ge-note-hidden .ge-title { diff --git a/versions.json b/versions.json index 55ec1dc..db150af 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "3.1.3": "1.1.0" + "3.1.4": "1.1.0" } \ No newline at end of file