mirror of
https://github.com/devon22/obsidian-gridexplorer.git
synced 2026-07-22 05:38:16 +00:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d93b16e8d | ||
|
|
ce13321edd | ||
|
|
f031515fda | ||
|
|
f448ffb986 | ||
|
|
9602dc2c75 | ||
|
|
8a6a48f280 | ||
|
|
95b302bff6 | ||
|
|
fc220c5218 |
25 changed files with 965 additions and 321 deletions
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"id": "gridexplorer",
|
||||
"name": "GridExplorer",
|
||||
"version": "3.4.11",
|
||||
"version": "3.4.19",
|
||||
"minAppVersion": "1.8.7",
|
||||
"description": "Browse note files in a grid view.",
|
||||
"description": "Browse and organize note files visually in a flexible grid layout.",
|
||||
"author": "Devon22",
|
||||
"authorUrl": "https://github.com/Devon22",
|
||||
"isDesktopOnly": false
|
||||
|
|
|
|||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "gridexplorer",
|
||||
"version": "3.4.11",
|
||||
"version": "3.4.19",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gridexplorer",
|
||||
"version": "3.4.11",
|
||||
"version": "3.4.19",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jszip": "^3.10.1"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "gridexplorer",
|
||||
"version": "3.4.11",
|
||||
"description": "Browse note files in a grid view.",
|
||||
"version": "3.4.19",
|
||||
"description": "Browse and organize note files visually in a flexible grid layout.",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "node esbuild.config.mjs",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext, ViewStateResult } from 'obsidian';
|
||||
import { TFile, TFolder, WorkspaceLeaf, Menu, setIcon, Platform, normalizePath, ItemView, EventRef, FuzzySuggestModal, parseLinktext, ViewStateResult, Notice } from 'obsidian';
|
||||
import GridExplorerPlugin from './main';
|
||||
import { GridView } from './GridView';
|
||||
import { isFolderIgnored, isImageFile, isVideoFile, isAudioFile, isMediaFile } from './utils/fileUtils';
|
||||
|
|
@ -2225,11 +2225,11 @@ export class ExplorerView extends ItemView {
|
|||
|
||||
let handled = false;
|
||||
for (const line of lines) {
|
||||
let resolvedFile: TFile | null = null;
|
||||
try {
|
||||
let text = line;
|
||||
if (text.startsWith('!')) text = text.substring(1);
|
||||
|
||||
let resolvedFile: TFile | null = null;
|
||||
if (text.startsWith('[[') && text.endsWith(']]')) {
|
||||
const inner = text.slice(2, -2);
|
||||
const parsed = parseLinktext(inner);
|
||||
|
|
@ -2253,6 +2253,7 @@ export class ExplorerView extends ItemView {
|
|||
handled = true;
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolvedFile ? resolvedFile.name : line}`);
|
||||
console.error('An error occurred while moving one of the files (ExplorerView):', error);
|
||||
// 繼續處理其他檔案
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,12 +182,17 @@ export class FileWatcher {
|
|||
// 監聽當前開啟的檔案變更,讀取反向連結
|
||||
this.plugin.registerEvent(
|
||||
this.app.workspace.on('file-open', (file) => {
|
||||
if (file instanceof TFile && this.gridView.searchQuery === '') {
|
||||
const sourceMode = this.gridView.sourceMode;
|
||||
if (this.gridView.searchQuery !== '') return;
|
||||
const sourceMode = this.gridView.sourceMode;
|
||||
|
||||
if (file instanceof TFile) {
|
||||
// 處理反向連結和出向連結
|
||||
if (sourceMode === 'backlinks' || sourceMode === 'outgoinglinks') {
|
||||
this.scheduleRender();
|
||||
// 若分頁已釘選,維持鎖定的目標檔案,不跟著切換(與內建反向連結行為一致)
|
||||
if (!this.gridView.isPinned() && this.gridView.sourcePath !== file.path) {
|
||||
this.gridView.sourcePath = file.path;
|
||||
this.scheduleRender();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -202,16 +207,93 @@ export class FileWatcher {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if (file === null) {
|
||||
// 當 file 為 null,代表切換到非檔案檢視,交由 active-leaf-change 處理
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 監聽 active-leaf-change 來處理切回 GridView 或其他檢視的狀況
|
||||
this.plugin.registerEvent(
|
||||
this.app.workspace.on('active-leaf-change', (leaf) => {
|
||||
if (!leaf || !leaf.view) return;
|
||||
const view = leaf.view;
|
||||
// 忽略自己被設為活動的事件,避免自我循環與自我清空
|
||||
if (view === this.gridView) return;
|
||||
|
||||
// 忽略側邊欄的活動分頁變更,避免切換側邊欄頁籤或點擊側邊欄時,反向連結被清空
|
||||
const isSidebarLeaf = leaf.getRoot() === this.app.workspace.leftSplit ||
|
||||
leaf.getRoot() === this.app.workspace.rightSplit;
|
||||
if (isSidebarLeaf) return;
|
||||
|
||||
if (this.gridView.searchQuery !== '') return;
|
||||
|
||||
const sourceMode = this.gridView.sourceMode;
|
||||
if (sourceMode === 'backlinks' || sourceMode === 'outgoinglinks') {
|
||||
if (this.gridView.isPinned()) return;
|
||||
|
||||
let newSourcePath = '';
|
||||
if (view.getViewType() === 'grid-view') {
|
||||
const activeGridView = view as unknown as Record<string, unknown>;
|
||||
const previewedFile = activeGridView.previewedFile;
|
||||
if (activeGridView.isShowingNote === true && previewedFile instanceof TFile) {
|
||||
newSourcePath = previewedFile.path;
|
||||
}
|
||||
} else if ('file' in view && view.file instanceof TFile) {
|
||||
newSourcePath = view.file.path;
|
||||
}
|
||||
|
||||
// 如果 sourcePath 有改變,就更新並重新渲染
|
||||
if (this.gridView.sourcePath !== newSourcePath) {
|
||||
this.gridView.sourcePath = newSourcePath;
|
||||
this.scheduleRender();
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// 監聽其他 GridView 在網格內預覽筆記的事件(GridPreviewManager.showNoteInGrid/hideNoteInGrid)
|
||||
// 讓側邊欄的反向連結/外部連結模式能跟著預覽的筆記更新,行為與內建反向連結面板一致
|
||||
this.plugin.registerEvent(
|
||||
(this.app.workspace as unknown as {
|
||||
on(name: 'ge-preview-file-change', callback: (file: TFile | null, sourceView: unknown) => void): import('obsidian').EventRef
|
||||
}).on('ge-preview-file-change', (file, sourceView) => {
|
||||
// 忽略自己觸發的事件,避免自我循環
|
||||
if (sourceView === this.gridView) return;
|
||||
if (this.gridView.searchQuery !== '') return;
|
||||
|
||||
const sourceMode = this.gridView.sourceMode;
|
||||
if (sourceMode !== 'backlinks' && sourceMode !== 'outgoinglinks') return;
|
||||
|
||||
// 若分頁已釘選,維持鎖定的目標檔案,不跟著切換
|
||||
if (this.gridView.isPinned()) return;
|
||||
|
||||
if (file instanceof TFile) {
|
||||
this.gridView.sourcePath = file.path;
|
||||
this.scheduleRender();
|
||||
} else if (file === null) {
|
||||
this.gridView.sourcePath = '';
|
||||
this.scheduleRender();
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
public renderPending = false;
|
||||
|
||||
public performPendingRender() {
|
||||
if (this.renderPending) {
|
||||
this.renderPending = false;
|
||||
this.scheduleRender();
|
||||
}
|
||||
}
|
||||
|
||||
// 以 200ms 去抖動的方式排程 render,避免短時間內大量重繪
|
||||
private scheduleRender = (delay: number = 200): void => {
|
||||
// 若分頁被釘選或正在顯示筆記,暫停重新渲染
|
||||
// 若分頁被釘選或正在顯示筆記/ZIP,暫停重新渲染,但標記為待更新
|
||||
if ((this.gridView.isPinned() && this.gridView.sourceMode === 'backlinks') ||
|
||||
this.gridView.isShowingNote) {
|
||||
this.gridView.isShowingNote || this.gridView.isShowingZip) {
|
||||
this.renderPending = true;
|
||||
return;
|
||||
}
|
||||
if(this.gridView.sourceMode === 'recent-files' && this.gridView.containerEl.offsetParent === null) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TFile, Platform, setIcon, setTooltip, MarkdownRenderer } from 'obsidian';
|
||||
import { TFile, Platform, setIcon, setTooltip, MarkdownRenderer, Component, Menu, getFrontMatterInfo, parseYaml } from 'obsidian';
|
||||
import JSZip from 'jszip';
|
||||
import { GridView } from './GridView';
|
||||
import { MediaModal, VirtualMediaFile } from './modal/mediaModal';
|
||||
|
|
@ -8,6 +8,10 @@ interface NoteViewContainerWithKeydownHandler extends HTMLElement {
|
|||
keydownHandler?: (e: KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
interface MenuItemWithWarning {
|
||||
setWarning(warning: boolean): this;
|
||||
}
|
||||
|
||||
interface AppWithInternalPlugins {
|
||||
internalPlugins?: {
|
||||
plugins: {
|
||||
|
|
@ -27,6 +31,18 @@ export class GridPreviewManager {
|
|||
private view: GridView;
|
||||
private previewHistory: TFile[] = [];
|
||||
private previewHistoryIndex: number = -1;
|
||||
private previewComponent: Component | null = null;
|
||||
|
||||
private preparePreviewComponent() {
|
||||
if (this.previewComponent) {
|
||||
this.previewComponent.unload();
|
||||
this.view.removeChild(this.previewComponent);
|
||||
this.previewComponent = null;
|
||||
}
|
||||
this.previewComponent = new Component();
|
||||
this.view.addChild(this.previewComponent);
|
||||
this.previewComponent.load();
|
||||
}
|
||||
|
||||
constructor(view: GridView) {
|
||||
this.view = view;
|
||||
|
|
@ -88,6 +104,7 @@ export class GridPreviewManager {
|
|||
let initialScrollTop = 0;
|
||||
let isAtTop = false;
|
||||
let isAtBottom = false;
|
||||
let isFromEdge = false;
|
||||
|
||||
const handleTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length > 1) return;
|
||||
|
|
@ -109,11 +126,28 @@ export class GridPreviewManager {
|
|||
isDragging = false;
|
||||
isPullingUp = false;
|
||||
isHorizontalDragging = false;
|
||||
|
||||
const edgeThreshold = 24;
|
||||
isFromEdge = startX < edgeThreshold || startX > window.innerWidth - edgeThreshold;
|
||||
|
||||
// Obsidian 自身的 swipe 手勢會在方向判斷完成前就介入,
|
||||
// 所以必須在 touchstart 一開始就 stopPropagation,
|
||||
// 僅保留螢幕邊緣 24px 讓系統返回手勢通過
|
||||
if (!isFromEdge) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: TouchEvent) => {
|
||||
if (e.touches.length > 1) return;
|
||||
|
||||
// 理由同 handleTouchStart,必須盡早攔截以避免 Obsidian 手勢介入
|
||||
if (!isFromEdge) {
|
||||
e.stopPropagation();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
currentY = e.touches[0].clientY;
|
||||
currentX = e.touches[0].clientX;
|
||||
const deltaY = currentY - startY;
|
||||
|
|
@ -152,7 +186,6 @@ export class GridPreviewManager {
|
|||
|
||||
// 只有在進入我們自己定義的拖曳狀態時,才攔截事件與阻止預設行為
|
||||
if (isHorizontalDragging || isDragging) {
|
||||
e.stopPropagation();
|
||||
if (e.cancelable) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
|
@ -180,7 +213,7 @@ export class GridPreviewManager {
|
|||
};
|
||||
|
||||
const handleTouchEnd = (e: TouchEvent) => {
|
||||
if (isHorizontalDragging || isDragging) {
|
||||
if (!isFromEdge) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
|
|
@ -287,6 +320,11 @@ export class GridPreviewManager {
|
|||
this.view.noteViewContainer = this.view.containerEl.createDiv('ge-note-view-container');
|
||||
const noteViewContainer = this.view.noteViewContainer;
|
||||
|
||||
// 立即設定狀態,確保在非同步渲染期間也能正常響應關閉或跳轉
|
||||
this.view.isShowingNote = true;
|
||||
this.view.previewedFile = file;
|
||||
this.view.app.workspace.trigger('ge-preview-file-change', file, this.view);
|
||||
|
||||
// 頂部列 (左右區塊)
|
||||
const topBar = noteViewContainer.createDiv('ge-note-top-bar');
|
||||
const leftBar = topBar.createDiv('ge-note-top-left');
|
||||
|
|
@ -323,8 +361,6 @@ export class GridPreviewManager {
|
|||
setTooltip(noteTitle, file.basename);
|
||||
}
|
||||
|
||||
|
||||
|
||||
const rightBar = topBar.createDiv('ge-note-top-right');
|
||||
|
||||
// 編輯按鈕
|
||||
|
|
@ -334,11 +370,90 @@ export class GridPreviewManager {
|
|||
void this.view.getLeafByMode(file).openFile(file);
|
||||
});
|
||||
|
||||
|
||||
// Metadata 切換按鈕
|
||||
const infoButton = rightBar.createEl('button', { cls: 'clickable-icon ge-note-info-button' });
|
||||
setIcon(infoButton, 'info');
|
||||
|
||||
// 更多選項按鈕
|
||||
const moreOptionsButton = rightBar.createEl('button', { cls: 'clickable-icon ge-note-more-options-button' });
|
||||
setIcon(moreOptionsButton, 'more-vertical');
|
||||
if (Platform.isDesktop) {
|
||||
setTooltip(moreOptionsButton, t('more_options'));
|
||||
}
|
||||
moreOptionsButton.addEventListener('click', (e) => {
|
||||
const menu = new Menu();
|
||||
this.view.app.workspace.trigger('file-menu', menu, file, 'file-explorer');
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t('open_in_new_tab'))
|
||||
.setIcon("external-link")
|
||||
.setSection?.("open")
|
||||
.onClick(() => {
|
||||
void this.view.app.workspace.getLeaf(true).openFile(file);
|
||||
});
|
||||
});
|
||||
|
||||
if (Platform.isMobile) {
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t('add_to_stash'))
|
||||
.setIcon("archive")
|
||||
.onClick(() => {
|
||||
(this.view as unknown as { addFilesToStash(files: TFile[]): void }).addFilesToStash([file]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
menu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t("make_a_copy"))
|
||||
.setIcon("copy")
|
||||
.setSection?.("action")
|
||||
.onClick(async () => {
|
||||
const parentPath = file.parent ? file.parent.path : "";
|
||||
let copyPath = "";
|
||||
if (parentPath && parentPath !== "/") {
|
||||
copyPath = `${parentPath}/${file.basename} 1.${file.extension}`;
|
||||
} else {
|
||||
copyPath = `${file.basename} 1.${file.extension}`;
|
||||
}
|
||||
|
||||
let counter = 1;
|
||||
while (this.view.app.vault.getAbstractFileByPath(copyPath)) {
|
||||
counter++;
|
||||
if (parentPath && parentPath !== "/") {
|
||||
copyPath = `${parentPath}/${file.basename} ${counter}.${file.extension}`;
|
||||
} else {
|
||||
copyPath = `${file.basename} ${counter}.${file.extension}`;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.view.app.vault.copy(file, copyPath);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy file:", err);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
menu.addItem((item) => {
|
||||
(item as unknown as MenuItemWithWarning).setWarning(true);
|
||||
item
|
||||
.setTitle(t('delete_note'))
|
||||
.setIcon('trash')
|
||||
.onClick(async () => {
|
||||
await this.view.app.fileManager.trashFile(file);
|
||||
this.hideNoteInGrid();
|
||||
});
|
||||
});
|
||||
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
// 關閉按鈕
|
||||
const closeButton = rightBar.createEl('button', { cls: 'clickable-icon ge-note-close-button' });
|
||||
setIcon(closeButton, 'x');
|
||||
|
|
@ -363,6 +478,8 @@ export class GridPreviewManager {
|
|||
}
|
||||
|
||||
// 在移動端添加滾動監聽,根據滾動方向控制導航欄顯示/隱藏
|
||||
this.preparePreviewComponent();
|
||||
|
||||
if (Platform.isPhone) {
|
||||
let lastScrollTop = 0;
|
||||
let accumulateScroll = 0;
|
||||
|
|
@ -400,7 +517,7 @@ export class GridPreviewManager {
|
|||
lastScrollTop = currentScrollTop;
|
||||
};
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
this.previewComponent!.registerDomEvent(scrollContainer, 'scroll', handleScroll);
|
||||
|
||||
// 監聽分頁切換事件,當離開當前視圖時恢復導航欄
|
||||
const handleActiveLeafChange = () => {
|
||||
|
|
@ -418,14 +535,9 @@ export class GridPreviewManager {
|
|||
};
|
||||
|
||||
// 註冊事件監聽器
|
||||
this.view.registerEvent(
|
||||
this.previewComponent!.registerEvent(
|
||||
this.view.app.workspace.on('active-leaf-change', handleActiveLeafChange)
|
||||
);
|
||||
|
||||
// 儲存滾動事件清理函數
|
||||
this.view.eventCleanupFunctions.push(() => {
|
||||
scrollContainer.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
}
|
||||
|
||||
// 取得 Metadata (Frontmatter)
|
||||
|
|
@ -433,131 +545,7 @@ export class GridPreviewManager {
|
|||
const frontmatter = fileCache?.frontmatter;
|
||||
|
||||
if (frontmatter) {
|
||||
// 檢查是否除了 position 以外還有其他屬性
|
||||
const keys = Object.keys(frontmatter).filter(k => k !== 'position');
|
||||
if (keys.length > 0) {
|
||||
const metadataContainer = noteContent.createDiv('ge-note-metadata-container');
|
||||
|
||||
// 綁定切換事件
|
||||
infoButton.addEventListener('click', () => {
|
||||
metadataContainer.classList.toggle('is-visible');
|
||||
scrollContainer.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
const metadataContent = metadataContainer.createDiv('ge-note-metadata-content');
|
||||
for (const key of keys) {
|
||||
const item = metadataContent.createDiv('ge-note-metadata-item');
|
||||
item.createSpan({ cls: 'ge-note-metadata-key', text: `${key}: ` });
|
||||
const value: unknown = frontmatter[key] as unknown;
|
||||
const valueSpan = item.createSpan({ cls: 'ge-note-metadata-value' });
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
values.forEach((val, index) => {
|
||||
const valStr = String(val);
|
||||
// 處理內部連結 [[link]] 或 [[link|alias]]
|
||||
const wikilinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
||||
// 處理 URL
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
// 處理 Tag
|
||||
const tagRegex = /#([^\s#]+)/g;
|
||||
|
||||
if (wikilinkRegex.test(valStr)) {
|
||||
wikilinkRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = wikilinkRegex.exec(valStr)) !== null) {
|
||||
// 插入匹配前的文字
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const linkPath = match[1];
|
||||
const linkAlias = match[2] || linkPath;
|
||||
const linkEl = valueSpan.createEl('a', {
|
||||
cls: 'internal-link',
|
||||
text: linkAlias,
|
||||
attr: { 'data-href': linkPath }
|
||||
});
|
||||
linkEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const linkedFile = this.view.app.metadataCache.getFirstLinkpathDest(linkPath, file.path);
|
||||
if (linkedFile) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
void this.view.getLeafByMode(linkedFile).openFile(linkedFile);
|
||||
} else {
|
||||
void this.showNoteInGrid(linkedFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
lastIndex = wikilinkRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
} else if (urlRegex.test(valStr)) {
|
||||
urlRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = urlRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const url = match[1];
|
||||
valueSpan.createEl('a', {
|
||||
cls: 'external-link',
|
||||
text: url,
|
||||
attr: { 'href': url, 'target': '_blank', 'rel': 'noopener' }
|
||||
});
|
||||
lastIndex = urlRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
} else if (key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag' || tagRegex.test(valStr)) {
|
||||
if ((key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag') && !valStr.startsWith('#')) {
|
||||
const tagEl = valueSpan.createEl('a', {
|
||||
cls: 'tag',
|
||||
text: '#' + valStr,
|
||||
attr: { 'href': '#' + valStr }
|
||||
});
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
(this.view.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + valStr);
|
||||
});
|
||||
} else {
|
||||
tagRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = tagRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const tagName = match[1];
|
||||
const tagEl = valueSpan.createEl('a', {
|
||||
cls: 'tag',
|
||||
text: '#' + tagName,
|
||||
attr: { 'href': '#' + tagName }
|
||||
});
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
(this.view.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + tagName);
|
||||
});
|
||||
lastIndex = tagRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
valueSpan.createSpan({ text: valStr });
|
||||
}
|
||||
|
||||
if (index < values.length - 1) {
|
||||
const isTag = key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag';
|
||||
valueSpan.createSpan({ text: isTag ? ' ' : ', ' });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
this.renderMetadata(frontmatter, noteContent, infoButton, scrollContainer, file.path);
|
||||
}
|
||||
|
||||
// 創建筆記內容區域
|
||||
|
|
@ -578,6 +566,7 @@ export class GridPreviewManager {
|
|||
try {
|
||||
// 讀取筆記內容
|
||||
const content = await this.view.app.vault.read(file);
|
||||
if (!this.view.isShowingNote || this.view.previewedFile !== file) return;
|
||||
|
||||
// 使用 Obsidian 的 MarkdownRenderer 渲染內容
|
||||
await MarkdownRenderer.render(
|
||||
|
|
@ -585,8 +574,9 @@ export class GridPreviewManager {
|
|||
content,
|
||||
noteContentArea,
|
||||
file.path,
|
||||
this.view
|
||||
this.previewComponent!
|
||||
);
|
||||
if (!this.view.isShowingNote || this.view.previewedFile !== file) return;
|
||||
|
||||
// 加上自訂屬性 data-source-path
|
||||
noteContentArea
|
||||
|
|
@ -617,7 +607,7 @@ export class GridPreviewManager {
|
|||
};
|
||||
|
||||
// 使用 registerDomEvent 註冊事件
|
||||
this.view.registerDomEvent(noteContentArea, 'click', handleLinkClick);
|
||||
this.previewComponent!.registerDomEvent(noteContentArea, 'click', handleLinkClick);
|
||||
} catch (error) {
|
||||
noteContentArea.textContent = '無法載入筆記內容';
|
||||
console.error('Error loading note content:', error);
|
||||
|
|
@ -625,13 +615,10 @@ export class GridPreviewManager {
|
|||
|
||||
// 渲染反向連結與出站連結區塊
|
||||
await this.renderLinksSection(file, noteContent);
|
||||
|
||||
if (!this.view.isShowingNote || this.view.previewedFile !== file) return;
|
||||
|
||||
// 註冊行動裝置滑動手勢
|
||||
this.registerPreviewTouchEvents(noteViewContainer, scrollContainer, () => this.hideNoteInGrid());
|
||||
|
||||
// 設定狀態
|
||||
this.view.isShowingNote = true;
|
||||
}
|
||||
|
||||
// 隱藏筆記顯示
|
||||
|
|
@ -649,12 +636,28 @@ export class GridPreviewManager {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.previewComponent) {
|
||||
this.previewComponent.unload();
|
||||
this.view.removeChild(this.previewComponent);
|
||||
this.previewComponent = null;
|
||||
}
|
||||
|
||||
if (this.view.noteViewContainer) {
|
||||
// 中斷所有正在下載的圖片,釋放瀏覽器連線池給下一次載入
|
||||
this.view.noteViewContainer
|
||||
.querySelectorAll<HTMLImageElement>('img')
|
||||
.forEach((img) => { img.removeAttribute('src'); });
|
||||
this.view.noteViewContainer.remove();
|
||||
this.view.noteViewContainer = null;
|
||||
}
|
||||
|
||||
this.view.isShowingNote = false;
|
||||
this.view.previewedFile = null;
|
||||
this.view.app.workspace.trigger('ge-preview-file-change', null, this.view);
|
||||
|
||||
if (this.view.fileWatcher) {
|
||||
this.view.fileWatcher.performPendingRender();
|
||||
}
|
||||
|
||||
if (clearHistory) {
|
||||
this.previewHistory = [];
|
||||
|
|
@ -731,6 +734,11 @@ export class GridPreviewManager {
|
|||
|
||||
const rightBar = topBar.createDiv('ge-zip-top-right');
|
||||
|
||||
// 建立 info 按鈕 (先建立,預設隱藏,待 zip 載入完後若有同名 md 則顯示)
|
||||
const infoButton = rightBar.createEl('button', { cls: 'clickable-icon ge-note-info-button' });
|
||||
setIcon(infoButton, 'info');
|
||||
infoButton.setCssProps({ display: 'none' });
|
||||
|
||||
// 開啟按鈕 (在分頁中開啟 ZIP 檔案)
|
||||
const openButton = rightBar.createEl('button', { cls: 'clickable-icon ge-zip-open-button' });
|
||||
setIcon(openButton, 'folder-archive');
|
||||
|
|
@ -745,10 +753,6 @@ export class GridPreviewManager {
|
|||
// 關閉按鈕
|
||||
const closeButton = rightBar.createEl('button', { cls: 'clickable-icon ge-zip-close-button' });
|
||||
setIcon(closeButton, 'x');
|
||||
closeButton.setAttribute('aria-label', t('zip_close_view'));
|
||||
if (Platform.isDesktop) {
|
||||
setTooltip(closeButton, t('zip_close_view'));
|
||||
}
|
||||
closeButton.addEventListener('click', () => {
|
||||
this.hideZipInGrid();
|
||||
});
|
||||
|
|
@ -770,6 +774,8 @@ export class GridPreviewManager {
|
|||
}
|
||||
const zipGridEl = zipContent.createDiv('zip-viewer-grid');
|
||||
|
||||
this.preparePreviewComponent();
|
||||
|
||||
// 在移動端添加滾動監聽,控制導航欄
|
||||
if (Platform.isPhone) {
|
||||
let lastScrollTop = 0;
|
||||
|
|
@ -805,7 +811,7 @@ export class GridPreviewManager {
|
|||
lastScrollTop = currentScrollTop;
|
||||
};
|
||||
|
||||
scrollContainer.addEventListener('scroll', handleScroll);
|
||||
this.previewComponent!.registerDomEvent(scrollContainer, 'scroll', handleScroll);
|
||||
|
||||
const handleActiveLeafChange = () => {
|
||||
const activeView = this.view.app.workspace.getActiveViewOfType(GridView);
|
||||
|
|
@ -820,13 +826,9 @@ export class GridPreviewManager {
|
|||
}
|
||||
};
|
||||
|
||||
this.view.registerEvent(
|
||||
this.previewComponent!.registerEvent(
|
||||
this.view.app.workspace.on('active-leaf-change', handleActiveLeafChange)
|
||||
);
|
||||
|
||||
this.view.eventCleanupFunctions.push(() => {
|
||||
scrollContainer.removeEventListener('scroll', handleScroll);
|
||||
});
|
||||
}
|
||||
|
||||
// 載入與解析 ZIP 檔案
|
||||
|
|
@ -837,6 +839,39 @@ export class GridPreviewManager {
|
|||
const zip = await JSZip.loadAsync(arrayBuffer);
|
||||
this.view.activeZip = zip;
|
||||
|
||||
// 檢查 zip 內是否有同名 md 檔案
|
||||
const zipMdFilename = Object.keys(zip.files).find(name => {
|
||||
const lower = name.toLowerCase();
|
||||
const basename = file.basename.toLowerCase();
|
||||
return !zip.files[name].dir &&
|
||||
(lower === `${basename}.md` || lower.endsWith(`/${basename}.md`));
|
||||
});
|
||||
|
||||
if (zipMdFilename) {
|
||||
try {
|
||||
const mdZipFile = zip.files[zipMdFilename];
|
||||
const mdContent = await mdZipFile.async("string");
|
||||
|
||||
// 取得 Frontmatter 資訊並解析
|
||||
const frontMatterInfo = getFrontMatterInfo(mdContent);
|
||||
if (frontMatterInfo && frontMatterInfo.exists) {
|
||||
const frontmatter = parseYaml(frontMatterInfo.frontmatter) as Record<string, unknown>;
|
||||
if (frontmatter) {
|
||||
this.renderMetadata(frontmatter, scrollContainer, infoButton, scrollContainer, file.path, true, isInSidebar);
|
||||
// 因為 metadataContainer 是在非同步加載後才建立,需將它移動至 scrollContainer 的最上方 (即 zipContent 之前)
|
||||
const metadataEl = scrollContainer.querySelector('.ge-note-metadata-container');
|
||||
if (metadataEl && scrollContainer.firstChild && metadataEl !== scrollContainer.firstChild) {
|
||||
scrollContainer.insertBefore(metadataEl, scrollContainer.firstChild);
|
||||
}
|
||||
// 顯示切換按鈕
|
||||
infoButton.setCssProps({ display: '' });
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to read/render companion md inside ZIP:", err);
|
||||
}
|
||||
}
|
||||
|
||||
const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'];
|
||||
this.view.zipImageFiles = Object.keys(zip.files)
|
||||
.filter(filename => {
|
||||
|
|
@ -1124,6 +1159,12 @@ export class GridPreviewManager {
|
|||
hideZipInGrid(clearHistory = true) {
|
||||
if (!this.view.isShowingZip) return;
|
||||
|
||||
if (this.previewComponent) {
|
||||
this.previewComponent.unload();
|
||||
this.view.removeChild(this.previewComponent);
|
||||
this.previewComponent = null;
|
||||
}
|
||||
|
||||
// 顯示移動端導航欄 (僅在行動裝置上)
|
||||
if (Platform.isPhone) {
|
||||
const mobileNavbar = activeDocument.querySelector('.mobile-navbar') as HTMLElement;
|
||||
|
|
@ -1160,6 +1201,10 @@ export class GridPreviewManager {
|
|||
|
||||
this.view.isShowingZip = false;
|
||||
|
||||
if (this.view.fileWatcher) {
|
||||
this.view.fileWatcher.performPendingRender();
|
||||
}
|
||||
|
||||
if (clearHistory) {
|
||||
this.previewHistory = [];
|
||||
this.previewHistoryIndex = -1;
|
||||
|
|
@ -1250,6 +1295,8 @@ export class GridPreviewManager {
|
|||
text: file.basename
|
||||
});
|
||||
|
||||
this.preparePreviewComponent();
|
||||
|
||||
// 3. 讀取與渲染
|
||||
const content = await this.view.app.vault.read(file);
|
||||
await MarkdownRenderer.render(
|
||||
|
|
@ -1257,7 +1304,7 @@ export class GridPreviewManager {
|
|||
content,
|
||||
noteContentArea as HTMLElement,
|
||||
file.path,
|
||||
this.view
|
||||
this.previewComponent!
|
||||
);
|
||||
|
||||
// 4. 加上圖片的 data-source-path
|
||||
|
|
@ -1278,6 +1325,152 @@ export class GridPreviewManager {
|
|||
console.error('Error updating current preview note:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private renderMetadata(
|
||||
frontmatter: Record<string, unknown>,
|
||||
parentEl: HTMLElement,
|
||||
infoButton: HTMLButtonElement,
|
||||
scrollContainer: HTMLElement,
|
||||
filePath: string,
|
||||
isZip = false,
|
||||
isInSidebar = false
|
||||
) {
|
||||
const keys = Object.keys(frontmatter).filter(k => k !== 'position');
|
||||
if (keys.length > 0) {
|
||||
const metadataContainer = parentEl.createDiv('ge-note-metadata-container');
|
||||
|
||||
// 如果是在 zip 中渲染,需要加上 margin 外間距使其跟 showNoteInGrid 左右對齊
|
||||
if (isZip) {
|
||||
metadataContainer.setCssProps({
|
||||
margin: isInSidebar ? '15px 15px 0 15px' : '20px 20px 0 20px'
|
||||
});
|
||||
}
|
||||
|
||||
// 綁定切換事件
|
||||
infoButton.addEventListener('click', () => {
|
||||
metadataContainer.classList.toggle('is-visible');
|
||||
scrollContainer.scrollTo(0, 0);
|
||||
});
|
||||
|
||||
const metadataContent = metadataContainer.createDiv('ge-note-metadata-content');
|
||||
for (const key of keys) {
|
||||
const item = metadataContent.createDiv('ge-note-metadata-item');
|
||||
item.createSpan({ cls: 'ge-note-metadata-key', text: `${key}: ` });
|
||||
const value = frontmatter[key];
|
||||
const valueSpan = item.createSpan({ cls: 'ge-note-metadata-value' });
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
values.forEach((val, index) => {
|
||||
let valStr = '';
|
||||
if (val instanceof Date) {
|
||||
const yyyy = val.getFullYear();
|
||||
const mm = String(val.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(val.getDate()).padStart(2, '0');
|
||||
valStr = `${yyyy}-${mm}-${dd}`;
|
||||
} else {
|
||||
valStr = String(val);
|
||||
}
|
||||
const wikilinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const tagRegex = /#([^\s#]+)/g;
|
||||
|
||||
if (wikilinkRegex.test(valStr)) {
|
||||
wikilinkRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = wikilinkRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const linkPath = match[1];
|
||||
const linkAlias = match[2] || linkPath;
|
||||
const linkEl = valueSpan.createEl('a', {
|
||||
cls: 'internal-link',
|
||||
text: linkAlias,
|
||||
attr: { 'data-href': linkPath }
|
||||
});
|
||||
linkEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const linkedFile = this.view.app.metadataCache.getFirstLinkpathDest(linkPath, filePath);
|
||||
if (linkedFile) {
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
void this.view.getLeafByMode(linkedFile).openFile(linkedFile);
|
||||
} else {
|
||||
void this.showNoteInGrid(linkedFile);
|
||||
}
|
||||
}
|
||||
});
|
||||
lastIndex = wikilinkRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
} else if (urlRegex.test(valStr)) {
|
||||
urlRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = urlRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const url = match[1];
|
||||
valueSpan.createEl('a', {
|
||||
cls: 'external-link',
|
||||
text: url,
|
||||
attr: { 'href': url, 'target': '_blank', 'rel': 'noopener' }
|
||||
});
|
||||
lastIndex = urlRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
} else if (key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag' || tagRegex.test(valStr)) {
|
||||
if ((key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag') && !valStr.startsWith('#')) {
|
||||
const tagEl = valueSpan.createEl('a', {
|
||||
cls: 'tag',
|
||||
text: '#' + valStr,
|
||||
attr: { 'href': '#' + valStr }
|
||||
});
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
(this.view.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + valStr);
|
||||
});
|
||||
} else {
|
||||
tagRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = tagRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const tagName = match[1];
|
||||
const tagEl = valueSpan.createEl('a', {
|
||||
cls: 'tag',
|
||||
text: '#' + tagName,
|
||||
attr: { 'href': '#' + tagName }
|
||||
});
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
(this.view.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + tagName);
|
||||
});
|
||||
lastIndex = tagRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
valueSpan.createSpan({ text: valStr });
|
||||
}
|
||||
|
||||
if (index < values.length - 1) {
|
||||
const isTag = key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag';
|
||||
valueSpan.createSpan({ text: isTag ? ' ' : ', ' });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -110,6 +110,8 @@ interface GridViewStateData {
|
|||
showIgnoredItems?: boolean;
|
||||
baseCardLayout?: 'horizontal' | 'vertical';
|
||||
cardLayout?: 'horizontal' | 'vertical';
|
||||
baseShowNoteInGrid?: boolean;
|
||||
showNoteInGridState?: boolean;
|
||||
hideHeaderElements?: boolean;
|
||||
showFileNameFilter?: boolean;
|
||||
baseShowDateDividers?: boolean;
|
||||
|
|
@ -188,9 +190,12 @@ export class GridView extends ItemView {
|
|||
customOptionIndex: number = -1; // 自訂模式選項索引
|
||||
baseCardLayout: 'horizontal' | 'vertical' = 'horizontal'; // 使用者在設定或 UI 中選擇的基礎卡片樣式(不受資料夾臨時覆蓋影響)
|
||||
cardLayout: 'horizontal' | 'vertical' = 'horizontal'; // 目前實際使用的卡片樣式(可能被資料夾 metadata 臨時覆蓋)
|
||||
baseShowNoteInGrid: boolean = false; // 基礎在網格中顯示筆記(不受資料夾臨時覆蓋影響)
|
||||
showNoteInGridState: boolean = false; // 目前實際使用的在網格中顯示筆記狀態(可能被資料夾 metadata 臨時覆蓋)
|
||||
renderToken: number = 0; // 用於取消尚未完成之批次排程的遞增令牌
|
||||
isShowingNote: boolean = false; // 是否正在顯示筆記
|
||||
noteViewContainer: HTMLElement | null = null; // 筆記檢視容器
|
||||
previewedFile: TFile | null = null; // 目前在網格內預覽顯示的筆記檔案(供其他 GridView 的反向連結/外部連結模式跟蹤)
|
||||
isShowingZip: boolean = false; // 是否正在顯示 ZIP 預覽
|
||||
zipViewContainer: HTMLElement | null = null; // ZIP 檢視容器
|
||||
zipThumbnailUrls: Map<number, string> = new Map(); // 暫存縮圖 Blob URL
|
||||
|
|
@ -211,6 +216,8 @@ export class GridView extends ItemView {
|
|||
this.sortType = this.baseSortType;
|
||||
this.baseCardLayout = this.plugin.settings.cardLayout;
|
||||
this.cardLayout = this.baseCardLayout;
|
||||
this.baseShowNoteInGrid = this.plugin.settings.showNoteInGrid;
|
||||
this.showNoteInGridState = this.baseShowNoteInGrid;
|
||||
this.baseMinMode = false;
|
||||
this.minMode = this.baseMinMode;
|
||||
this.baseShowDateDividers = this.plugin.settings.dateDividerMode !== 'none';
|
||||
|
|
@ -635,6 +642,7 @@ export class GridView extends ItemView {
|
|||
let folderSort: string | undefined;
|
||||
let tempMinMode: boolean = this.baseMinMode;
|
||||
let tempShowDateDividers: boolean = this.baseShowDateDividers;
|
||||
let tempShowNoteInGrid: boolean = this.baseShowNoteInGrid;
|
||||
|
||||
if (mdFile instanceof TFile) {
|
||||
const metadata = this.app.metadataCache.getFileCache(mdFile)?.frontmatter;
|
||||
|
|
@ -652,10 +660,16 @@ export class GridView extends ItemView {
|
|||
} else if (metadata?.showDateDividers === false || metadata?.showDateDividers === 'false') {
|
||||
tempShowDateDividers = false;
|
||||
}
|
||||
if (metadata?.showNoteInGrid === true || metadata?.showNoteInGrid === 'true') {
|
||||
tempShowNoteInGrid = true;
|
||||
} else if (metadata?.showNoteInGrid === false || metadata?.showNoteInGrid === 'false') {
|
||||
tempShowNoteInGrid = false;
|
||||
}
|
||||
}
|
||||
this.cardLayout = tempLayout;
|
||||
this.minMode = tempMinMode;
|
||||
this.showDateDividers = tempShowDateDividers;
|
||||
this.showNoteInGridState = tempShowNoteInGrid;
|
||||
|
||||
// 根據資料夾 frontmatter 的 sort 覆蓋實際排序,否則使用 baseSortType
|
||||
this.sortType = folderSort && typeof folderSort === 'string' && folderSort.trim() !== ''
|
||||
|
|
@ -666,6 +680,7 @@ export class GridView extends ItemView {
|
|||
this.cardLayout = this.baseCardLayout; // 回復基礎卡片排列
|
||||
this.minMode = this.baseMinMode; // 回復基礎最小化模式
|
||||
this.showDateDividers = this.baseShowDateDividers; // 回復基礎日期分隔器模式
|
||||
this.showNoteInGridState = this.baseShowNoteInGrid; // 回復基礎在網格中顯示筆記狀態
|
||||
this.sourcePath = '/'; // 強制設定路徑為根目錄 (創建筆記用)
|
||||
|
||||
// 切換到自訂模式時:重設選項索引,並將排序設為 'none'
|
||||
|
|
@ -1071,8 +1086,8 @@ export class GridView extends ItemView {
|
|||
return;
|
||||
}
|
||||
|
||||
// 如果是反向連結模式,但沒有活動中的檔案
|
||||
if (this.sourceMode === 'backlinks' && !this.app.workspace.getActiveFile()) {
|
||||
// 如果是反向連結模式,但沒有有效的目標檔案
|
||||
if (this.sourceMode === 'backlinks' && !(this.app.vault.getAbstractFileByPath(this.sourcePath) instanceof TFile)) {
|
||||
const noFilesDiv = container.createDiv('ge-no-files');
|
||||
noFilesDiv.setText(t('no_backlinks'));
|
||||
if (this.plugin.statusBarItem) {
|
||||
|
|
@ -1247,15 +1262,19 @@ export class GridView extends ItemView {
|
|||
// 刪除註解及連結
|
||||
contentWithoutMediaLinks = contentWithoutMediaLinks
|
||||
.replace(/<!--[\s\S]*?-->/g, '')
|
||||
.replace(/!?\[([^\]]*)\]\([^)]+\)|!?\[\[([^\]]+)\]\]/g, (_match: string, p1?: string, p2?: string) => {
|
||||
const rawLinkText = p1 || p2 || '';
|
||||
if (!rawLinkText) return '';
|
||||
.replace(/(?:!?\[([^\]]*)\]\(([^)]+)\))|(?:!?\[\[([^\]]+)\]\])/g, (_match: string, p1?: string, p2?: string, p3?: string) => {
|
||||
if (p3) {
|
||||
// WikiLink
|
||||
const wikiLinkParts = p3.split('|');
|
||||
const linkTarget = wikiLinkParts[0];
|
||||
const linkText = wikiLinkParts.length > 1 ? wikiLinkParts.slice(1).join('|') : p3;
|
||||
const extension = linkTarget.split('.').pop()?.toLowerCase() || '';
|
||||
return (IMAGE_EXTENSIONS.has(extension) || VIDEO_EXTENSIONS.has(extension)) ? '' : linkText;
|
||||
}
|
||||
|
||||
const wikiLinkParts = p2 ? rawLinkText.split('|') : [];
|
||||
const linkTarget = p2 ? wikiLinkParts[0] : rawLinkText;
|
||||
const linkText = p2 && wikiLinkParts.length > 1 ? wikiLinkParts.slice(1).join('|') : rawLinkText;
|
||||
|
||||
// 獲取副檔名並檢查是否為圖片或影片
|
||||
// Standard Markdown link/image
|
||||
const linkText = p1 || '';
|
||||
const linkTarget = (p2 || '').split('?')[0];
|
||||
const extension = linkTarget.split('.').pop()?.toLowerCase() || '';
|
||||
return (IMAGE_EXTENSIONS.has(extension) || VIDEO_EXTENSIONS.has(extension)) ? '' : linkText;
|
||||
});
|
||||
|
|
@ -1522,6 +1541,7 @@ export class GridView extends ItemView {
|
|||
// Markdown 檔案尋找內部圖片
|
||||
if (imageUrl) {
|
||||
const img = imageArea.createEl('img');
|
||||
img.referrerPolicy = 'no-referrer';
|
||||
img.src = imageUrl;
|
||||
img.draggable = false;
|
||||
imageArea.setAttribute('data-loaded', 'true');
|
||||
|
|
@ -1851,7 +1871,7 @@ export class GridView extends ItemView {
|
|||
}*/
|
||||
|
||||
// 滑鼠懸停在項目上時,按 Ctrl 鍵直接顯示筆記或 ZIP 圖片網格
|
||||
if (Platform.isDesktop && (file.extension === 'md' || file.extension === 'zip') && !this.plugin.settings.showNoteInGrid) {
|
||||
if (Platform.isDesktop && (file.extension === 'md' || file.extension === 'zip') && !this.showNoteInGridState) {
|
||||
let triggeredInHover = false;
|
||||
let isHovering = false;
|
||||
let isMouseDown = false; // 追蹤滑鼠按下狀態
|
||||
|
|
@ -1977,7 +1997,7 @@ export class GridView extends ItemView {
|
|||
this.hasKeyboardFocus = true;
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if (this.plugin.settings.showNoteInGrid) {
|
||||
} else if (this.showNoteInGridState) {
|
||||
this.selectItem(index);
|
||||
this.hasKeyboardFocus = true;
|
||||
|
||||
|
|
@ -2645,6 +2665,8 @@ export class GridView extends ItemView {
|
|||
showIgnoredItems: this.showIgnoredItems,
|
||||
baseCardLayout: this.baseCardLayout,
|
||||
cardLayout: this.cardLayout,
|
||||
baseShowNoteInGrid: this.baseShowNoteInGrid,
|
||||
showNoteInGridState: this.showNoteInGridState,
|
||||
hideHeaderElements: this.hideHeaderElements,
|
||||
showFileNameFilter: this.showFileNameFilter,
|
||||
baseShowDateDividers: this.baseShowDateDividers,
|
||||
|
|
@ -2675,6 +2697,8 @@ export class GridView extends ItemView {
|
|||
this.showIgnoredItems = state.state.showIgnoredItems ?? false;
|
||||
this.baseCardLayout = state.state.baseCardLayout ?? 'horizontal';
|
||||
this.cardLayout = state.state.cardLayout ?? this.baseCardLayout;
|
||||
this.baseShowNoteInGrid = state.state.baseShowNoteInGrid ?? state.state.showNoteInGridState ?? this.plugin.settings.showNoteInGrid;
|
||||
this.showNoteInGridState = state.state.showNoteInGridState ?? this.baseShowNoteInGrid;
|
||||
this.hideHeaderElements = state.state.hideHeaderElements ?? false;
|
||||
this.showFileNameFilter = state.state.showFileNameFilter ?? this.plugin.settings.showFileNameFilter;
|
||||
this.baseShowDateDividers = state.state.baseShowDateDividers ?? state.state.showDateDividers ?? (this.plugin.settings.dateDividerMode !== 'none');
|
||||
|
|
|
|||
169
src/ZipView.ts
169
src/ZipView.ts
|
|
@ -1,4 +1,4 @@
|
|||
import { FileView, WorkspaceLeaf, TFile } from 'obsidian';
|
||||
import { FileView, WorkspaceLeaf, TFile, getFrontMatterInfo, parseYaml } from 'obsidian';
|
||||
import JSZip from 'jszip';
|
||||
import { MediaModal, VirtualMediaFile } from './modal/mediaModal';
|
||||
|
||||
|
|
@ -124,6 +124,173 @@ export class ZipView extends FileView {
|
|||
return;
|
||||
}
|
||||
|
||||
// 移除可能殘留的 metadata container
|
||||
const oldMeta = this.containerEl.querySelector('.ge-note-metadata-container');
|
||||
if (oldMeta) oldMeta.remove();
|
||||
|
||||
// 檢查 zip 內是否有同名 md 檔案
|
||||
const zipMdFilename = Object.keys(zip.files).find(name => {
|
||||
const lower = name.toLowerCase();
|
||||
const basename = file.basename.toLowerCase();
|
||||
return !zip.files[name].dir &&
|
||||
(lower === `${basename}.md` || lower.endsWith(`/${basename}.md`));
|
||||
});
|
||||
|
||||
if (zipMdFilename) {
|
||||
try {
|
||||
const mdZipFile = zip.files[zipMdFilename];
|
||||
const mdContent = await mdZipFile.async("string");
|
||||
const frontMatterInfo = getFrontMatterInfo(mdContent);
|
||||
|
||||
if (frontMatterInfo && frontMatterInfo.exists) {
|
||||
const frontmatter = parseYaml(frontMatterInfo.frontmatter) as Record<string, unknown>;
|
||||
if (frontmatter) {
|
||||
const keys = Object.keys(frontmatter).filter(k => k !== 'position');
|
||||
if (keys.length > 0) {
|
||||
const gridContainer = this.gridContainer;
|
||||
if (gridContainer) {
|
||||
const metadataContainer = gridContainer.createDiv('ge-note-metadata-container');
|
||||
metadataContainer.addClass('is-visible');
|
||||
metadataContainer.setCssProps({
|
||||
margin: '0 0 20px 0'
|
||||
});
|
||||
const metadataContent = metadataContainer.createDiv('ge-note-metadata-content');
|
||||
|
||||
for (const key of keys) {
|
||||
const item = metadataContent.createDiv('ge-note-metadata-item');
|
||||
item.createSpan({ cls: 'ge-note-metadata-key', text: `${key}: ` });
|
||||
const value = frontmatter[key];
|
||||
const valueSpan = item.createSpan({ cls: 'ge-note-metadata-value' });
|
||||
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
values.forEach((val, idx) => {
|
||||
let valStr = '';
|
||||
if (val instanceof Date) {
|
||||
const yyyy = val.getFullYear();
|
||||
const mm = String(val.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(val.getDate()).padStart(2, '0');
|
||||
valStr = `${yyyy}-${mm}-${dd}`;
|
||||
} else {
|
||||
valStr = String(val);
|
||||
}
|
||||
const wikilinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const tagRegex = /#([^\s#]+)/g;
|
||||
|
||||
if (wikilinkRegex.test(valStr)) {
|
||||
wikilinkRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = wikilinkRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const linkPath = match[1];
|
||||
const linkAlias = match[2] || linkPath;
|
||||
const linkEl = valueSpan.createEl('a', {
|
||||
cls: 'internal-link',
|
||||
text: linkAlias,
|
||||
attr: { 'data-href': linkPath }
|
||||
});
|
||||
linkEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const linkedFile = this.app.metadataCache.getFirstLinkpathDest(linkPath, file.path);
|
||||
if (linkedFile) {
|
||||
void this.app.workspace.getLeaf(true).openFile(linkedFile);
|
||||
}
|
||||
});
|
||||
lastIndex = wikilinkRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
} else if (urlRegex.test(valStr)) {
|
||||
urlRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = urlRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const url = match[1];
|
||||
valueSpan.createEl('a', {
|
||||
cls: 'external-link',
|
||||
text: url,
|
||||
attr: { 'href': url, 'target': '_blank', 'rel': 'noopener' }
|
||||
});
|
||||
lastIndex = urlRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
} else if (key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag' || tagRegex.test(valStr)) {
|
||||
interface AppWithInternalPlugins {
|
||||
internalPlugins?: {
|
||||
getPluginById?: (id: string) => {
|
||||
instance?: {
|
||||
openGlobalSearch?: (query: string) => void;
|
||||
};
|
||||
} | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
if ((key.toLowerCase() === 'tags' || key.toLowerCase() === 'tag') && !valStr.startsWith('#')) {
|
||||
const tagEl = valueSpan.createEl('a', {
|
||||
cls: 'tag',
|
||||
text: '#' + valStr,
|
||||
attr: { 'href': '#' + valStr }
|
||||
});
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
(this.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + valStr);
|
||||
});
|
||||
} else {
|
||||
tagRegex.lastIndex = 0;
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = tagRegex.exec(valStr)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex, match.index) });
|
||||
}
|
||||
const tagName = match[1];
|
||||
const tagEl = valueSpan.createEl('a', {
|
||||
cls: 'tag',
|
||||
text: '#' + tagName,
|
||||
attr: { 'href': '#' + tagName }
|
||||
});
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
(this.app as AppWithInternalPlugins).internalPlugins?.getPluginById?.('global-search')?.instance?.openGlobalSearch?.('tag:#' + tagName);
|
||||
});
|
||||
lastIndex = tagRegex.lastIndex;
|
||||
}
|
||||
if (lastIndex < valStr.length) {
|
||||
valueSpan.createSpan({ text: valStr.substring(lastIndex) });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
valueSpan.createSpan({ text: valStr });
|
||||
}
|
||||
|
||||
if (idx < values.length - 1) {
|
||||
valueSpan.createSpan({ text: ', ' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 將 metadataContainer 置於 gridContainer 頂部 (滾動區最上方,即 gridEl 之前)
|
||||
if (gridContainer.firstChild && metadataContainer !== gridContainer.firstChild) {
|
||||
gridContainer.insertBefore(metadataContainer, gridContainer.firstChild);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to parse companion metadata inside ZIP:", err);
|
||||
}
|
||||
}
|
||||
|
||||
this.initGrid();
|
||||
|
||||
this.currentIndex = 0;
|
||||
|
|
|
|||
45
src/main.ts
45
src/main.ts
|
|
@ -145,7 +145,7 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
if (activeFile) {
|
||||
const view = await this.activateView();
|
||||
if (view instanceof GridView) {
|
||||
await view.setSource('backlinks');
|
||||
await view.setSource('backlinks', activeFile.path);
|
||||
}
|
||||
} else {
|
||||
// 如果沒有當前筆記,則打開根目錄
|
||||
|
|
@ -163,7 +163,7 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
if (activeFile) {
|
||||
const view = await this.activateView();
|
||||
if (view instanceof GridView) {
|
||||
await view.setSource('outgoinglinks');
|
||||
await view.setSource('outgoinglinks', activeFile.path);
|
||||
}
|
||||
} else {
|
||||
// 如果沒有當前筆記,則打開根目錄
|
||||
|
|
@ -282,7 +282,7 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
await this.app.workspace.getLeaf().openFile(file);
|
||||
const view = await this.activateView();
|
||||
if (view instanceof GridView) {
|
||||
await view.setSource('backlinks');
|
||||
await view.setSource('backlinks', file.path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -294,7 +294,7 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
await this.app.workspace.getLeaf().openFile(file);
|
||||
const view = await this.activateView();
|
||||
if (view instanceof GridView) {
|
||||
await view.setSource('outgoinglinks');
|
||||
await view.setSource('outgoinglinks', file.path);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -398,8 +398,13 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
|
||||
// 攔截所有tag點擊事件
|
||||
this.registerDomEvent(activeDocument, 'click', async (evt: MouseEvent) => {
|
||||
// 如果未啟用攔截所有tag點擊事件,則跳過
|
||||
if (!this.settings.interceptAllTagClicks) return;
|
||||
// 判斷是否符合攔截條件
|
||||
const tagMode = this.settings.interceptAllTagClicks;
|
||||
if (tagMode === 'disabled') return;
|
||||
if (tagMode === 'alt' && !evt.altKey) return;
|
||||
if (tagMode === 'ctrl' && !(evt.ctrlKey || evt.metaKey)) return;
|
||||
if (tagMode === 'shift' && !evt.shiftKey) return;
|
||||
|
||||
// 只處理左鍵
|
||||
if (evt.button !== 0) return;
|
||||
|
||||
|
|
@ -462,11 +467,18 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
|
||||
// 攔截Breadcrumb導航點擊事件
|
||||
this.registerDomEvent(activeDocument, 'click', async (evt: MouseEvent) => {
|
||||
// 如果未啟用攔截Breadcrumb導航點擊事件,則跳過
|
||||
if (!this.settings.interceptBreadcrumbClicks) return;
|
||||
|
||||
//如果有按著Ctrl鍵,則跳過
|
||||
if (evt.ctrlKey || evt.metaKey) return;
|
||||
// 判斷是否符合攔截條件
|
||||
const crumbMode = this.settings.interceptBreadcrumbClicks;
|
||||
if (crumbMode === 'disabled') return;
|
||||
if (crumbMode === 'enabled') {
|
||||
if (evt.ctrlKey || evt.metaKey) return; // 預設功能,有按 Ctrl 鍵則跳過
|
||||
} else if (crumbMode === 'alt') {
|
||||
if (!evt.altKey) return;
|
||||
} else if (crumbMode === 'ctrl') {
|
||||
if (!(evt.ctrlKey || evt.metaKey)) return;
|
||||
} else if (crumbMode === 'shift') {
|
||||
if (!evt.shiftKey) return;
|
||||
}
|
||||
|
||||
const target = evt.target as HTMLElement;
|
||||
const breadcrumbEl = target.closest('.view-header-breadcrumb');
|
||||
|
|
@ -535,7 +547,7 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
};
|
||||
};
|
||||
if (appWithPlugins.plugins?.plugins) {
|
||||
appWithPlugins.plugins.plugins['obsidian-gridexplorer'] = this;
|
||||
appWithPlugins.plugins.plugins['gridexplorer'] = this;
|
||||
}
|
||||
|
||||
// Override new tab behavior (for useQuickAccessAsNewTabMode setting)
|
||||
|
|
@ -838,6 +850,15 @@ export default class GridExplorerPlugin extends Plugin {
|
|||
async loadSettings() {
|
||||
const loadedSettings = await this.loadData() as Partial<GallerySettings> | null;
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings ?? {});
|
||||
|
||||
// 相容舊版 boolean 設定
|
||||
if (typeof this.settings.interceptAllTagClicks === 'boolean') {
|
||||
this.settings.interceptAllTagClicks = this.settings.interceptAllTagClicks ? 'enabled' : 'disabled';
|
||||
}
|
||||
if (typeof this.settings.interceptBreadcrumbClicks === 'boolean') {
|
||||
this.settings.interceptBreadcrumbClicks = this.settings.interceptBreadcrumbClicks ? 'enabled' : 'disabled';
|
||||
}
|
||||
|
||||
migrateDataviewCodeToQuery(this.settings);
|
||||
updateCustomDocumentExtensions(this.settings);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,11 +179,11 @@ export class FolderSelectionModal extends Modal {
|
|||
backlinksOption.addEventListener('click', () => {
|
||||
void (async () => {
|
||||
if (this.activeView) {
|
||||
await this.activeView.setSource('backlinks');
|
||||
await this.activeView.setSource('backlinks', activeFile.path);
|
||||
} else {
|
||||
const view = await this.plugin.activateView();
|
||||
if (view instanceof GridView) {
|
||||
await view.setSource('backlinks');
|
||||
await view.setSource('backlinks', activeFile.path);
|
||||
}
|
||||
}
|
||||
this.close();
|
||||
|
|
@ -206,11 +206,11 @@ export class FolderSelectionModal extends Modal {
|
|||
outgoinglinksOption.addEventListener('click', () => {
|
||||
void (async () => {
|
||||
if (this.activeView) {
|
||||
await this.activeView.setSource('outgoinglinks');
|
||||
await this.activeView.setSource('outgoinglinks', activeFile.path);
|
||||
} else {
|
||||
const view = await this.plugin.activateView();
|
||||
if (view instanceof GridView) {
|
||||
await view.setSource('outgoinglinks');
|
||||
await view.setSource('outgoinglinks', activeFile.path);
|
||||
}
|
||||
}
|
||||
this.close();
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class MediaModal extends Modal {
|
|||
plugins?: Record<string, { activeMediaModal?: unknown }>;
|
||||
};
|
||||
};
|
||||
const plugin = appWithPlugins.plugins?.plugins?.['obsidian-gridexplorer'];
|
||||
const plugin = appWithPlugins.plugins?.plugins?.['gridexplorer'];
|
||||
if (plugin) {
|
||||
plugin.activeMediaModal = this;
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ export class MediaModal extends Modal {
|
|||
plugins?: Record<string, { activeMediaModal?: unknown }>;
|
||||
};
|
||||
};
|
||||
const plugin = appWithPlugins.plugins?.plugins?.['obsidian-gridexplorer'];
|
||||
const plugin = appWithPlugins.plugins?.plugins?.['gridexplorer'];
|
||||
if (plugin && plugin.activeMediaModal === this) {
|
||||
plugin.activeMediaModal = null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TFolder, TFile, normalizePath, Platform, setIcon, setTooltip, Menu, parseLinktext } from 'obsidian';
|
||||
import { TFolder, TFile, normalizePath, Platform, setIcon, setTooltip, Menu, parseLinktext, Notice } from 'obsidian';
|
||||
import { GridView } from './GridView';
|
||||
import { isFolderIgnored } from './utils/fileUtils';
|
||||
import { extractObsidianPathsFromDT } from './utils/dragUtils';
|
||||
|
|
@ -93,6 +93,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
|
|||
await gridView.app.fileManager.renameFile(resolved, newPath);
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolved.name}`);
|
||||
console.error(`An error occurred while moving the file ${p}:`, error);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -117,11 +118,11 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
|
|||
.filter((v: string): v is string => v.length > 0);
|
||||
|
||||
for (const line of lines) {
|
||||
let resolvedFile: TFile | null = null;
|
||||
try {
|
||||
let text = line;
|
||||
if (text.startsWith('!')) text = text.substring(1);
|
||||
|
||||
let resolvedFile: TFile | null = null;
|
||||
if (text.startsWith('[[') && text.endsWith(']]')) {
|
||||
const inner = text.slice(2, -2);
|
||||
const parsed = parseLinktext(inner);
|
||||
|
|
@ -144,6 +145,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolvedFile ? resolvedFile.name : line}`);
|
||||
console.error('An error occurred while moving one of the files (container):', error);
|
||||
// 繼續處理其他檔案
|
||||
}
|
||||
|
|
@ -525,6 +527,7 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
|
|||
await gridView.app.fileManager.renameFile(resolved, newPath);
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolved.name}`);
|
||||
console.error(`An error occurred while moving the file ${p}:`, error);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -555,37 +558,38 @@ export async function renderFolder(gridView: GridView, container: HTMLElement) {
|
|||
.map((s: string) => s.trim())
|
||||
.filter((v: string): v is string => v.length > 0);
|
||||
for (const line of lines) {
|
||||
try {
|
||||
let text = line;
|
||||
if (text.startsWith('!')) text = text.substring(1); // 去除 '!'
|
||||
|
||||
let resolvedFile: TFile | null = null;
|
||||
if (text.startsWith('[[') && text.endsWith(']]')) {
|
||||
const inner = text.slice(2, -2);
|
||||
const parsed = parseLinktext(inner);
|
||||
const dest = gridView.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
|
||||
if (dest instanceof TFile) resolvedFile = dest;
|
||||
} else {
|
||||
const direct = gridView.app.vault.getAbstractFileByPath(text);
|
||||
if (direct instanceof TFile) {
|
||||
resolvedFile = direct;
|
||||
} else {
|
||||
const dest = gridView.app.metadataCache.getFirstLinkpathDest(text, srcPath);
|
||||
if (dest instanceof TFile) resolvedFile = dest;
|
||||
}
|
||||
}
|
||||
try {
|
||||
let text = line;
|
||||
if (text.startsWith('!')) text = text.substring(1); // 去除 '!'
|
||||
|
||||
if (resolvedFile instanceof TFile) {
|
||||
const newPath = normalizePath(`${folderPath}/${resolvedFile.name}`);
|
||||
if (resolvedFile.path !== newPath) {
|
||||
await gridView.app.fileManager.renameFile(resolvedFile, newPath);
|
||||
if (text.startsWith('[[') && text.endsWith(']]')) {
|
||||
const inner = text.slice(2, -2);
|
||||
const parsed = parseLinktext(inner);
|
||||
const dest = gridView.app.metadataCache.getFirstLinkpathDest(parsed.path, srcPath);
|
||||
if (dest instanceof TFile) resolvedFile = dest;
|
||||
} else {
|
||||
const direct = gridView.app.vault.getAbstractFileByPath(text);
|
||||
if (direct instanceof TFile) {
|
||||
resolvedFile = direct;
|
||||
} else {
|
||||
const dest = gridView.app.metadataCache.getFirstLinkpathDest(text, srcPath);
|
||||
if (dest instanceof TFile) resolvedFile = dest;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedFile instanceof TFile) {
|
||||
const newPath = normalizePath(`${folderPath}/${resolvedFile.name}`);
|
||||
if (resolvedFile.path !== newPath) {
|
||||
await gridView.app.fileManager.renameFile(resolvedFile, newPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolvedFile ? resolvedFile.name : line}`);
|
||||
console.error('An error occurred while moving one of the files:', error);
|
||||
// 繼續處理其他檔案
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('An error occurred while moving one of the files:', error);
|
||||
// 繼續處理其他檔案
|
||||
}
|
||||
}
|
||||
})();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -529,6 +529,19 @@ export function renderHeaderButton(gridView: GridView) {
|
|||
void gridView.render();
|
||||
});
|
||||
});
|
||||
// 預設在網格中顯示筆記
|
||||
moreMenu.addItem((item) => {
|
||||
item
|
||||
.setTitle(t('show_note_in_grid'))
|
||||
.setIcon('book-open')
|
||||
.setChecked(gridView.showNoteInGridState)
|
||||
.onClick(() => {
|
||||
gridView.baseShowNoteInGrid = !gridView.showNoteInGridState;
|
||||
gridView.showNoteInGridState = gridView.baseShowNoteInGrid;
|
||||
gridView.app.workspace.requestSaveLayout();
|
||||
void gridView.render();
|
||||
});
|
||||
});
|
||||
// 顯示忽略的資料夾及檔案選項
|
||||
moreMenu.addItem((item) => {
|
||||
item
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { TFolder, TFile, Menu, Platform, setIcon, normalizePath, setTooltip, parseLinktext } from 'obsidian';
|
||||
import { TFolder, TFile, Menu, Platform, setIcon, normalizePath, setTooltip, parseLinktext, Notice } from 'obsidian';
|
||||
import { GridView } from './GridView';
|
||||
import { isFolderIgnored } from './utils/fileUtils';
|
||||
import { extractObsidianPathsFromDT } from './utils/dragUtils';
|
||||
|
|
@ -312,10 +312,9 @@ export function renderModePath(gridView: GridView) {
|
|||
// 處理 obsidian:// URI 格式(單檔/多檔)
|
||||
const obsidianPaths = await extractObsidianPathsFromDT(event.dataTransfer);
|
||||
if (obsidianPaths.length > 0) {
|
||||
try {
|
||||
for (const filePath of obsidianPaths) {
|
||||
let resolved: TFile | null = null;
|
||||
|
||||
for (const filePath of obsidianPaths) {
|
||||
let resolved: TFile | null = null;
|
||||
try {
|
||||
// 1) 直接以路徑查找
|
||||
const direct = gridView.app.vault.getAbstractFileByPath(filePath);
|
||||
if (direct instanceof TFile) {
|
||||
|
|
@ -341,9 +340,10 @@ export function renderModePath(gridView: GridView) {
|
|||
await gridView.app.fileManager.renameFile(resolved, newPath);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolved ? resolved.name : filePath}`);
|
||||
console.error('An error occurred while moving multiple files to folder:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('An error occurred while moving multiple files to folder:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -359,11 +359,11 @@ export function renderModePath(gridView: GridView) {
|
|||
.filter((v: string): v is string => v.length > 0);
|
||||
|
||||
for (const line of lines) {
|
||||
let resolvedFile: TFile | null = null;
|
||||
try {
|
||||
let text = line;
|
||||
if (text.startsWith('!')) text = text.substring(1);
|
||||
|
||||
let resolvedFile: TFile | null = null;
|
||||
if (text.startsWith('[[') && text.endsWith(']]')) {
|
||||
const inner = text.slice(2, -2);
|
||||
const parsed = parseLinktext(inner);
|
||||
|
|
@ -386,6 +386,7 @@ export function renderModePath(gridView: GridView) {
|
|||
}
|
||||
}
|
||||
} catch (error) {
|
||||
new Notice(`${t('failed_to_move_file')}: ${resolvedFile ? resolvedFile.name : line}`);
|
||||
console.error('An error occurred while moving one of the files to folder:', error);
|
||||
// 繼續處理其他檔案
|
||||
}
|
||||
|
|
@ -670,18 +671,18 @@ export function renderModePath(gridView: GridView) {
|
|||
case 'backlinks': {
|
||||
modeIcon = '🔗';
|
||||
modeName = t('backlinks_mode');
|
||||
const activeFile = gridView.app.workspace.getActiveFile();
|
||||
if (activeFile) {
|
||||
modeName += `: ${activeFile.basename}`;
|
||||
const targetFile = gridView.app.vault.getAbstractFileByPath(gridView.sourcePath);
|
||||
if (targetFile instanceof TFile) {
|
||||
modeName += `: ${targetFile.basename}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'outgoinglinks': {
|
||||
modeIcon = '🔗';
|
||||
modeName = t('outgoinglinks_mode');
|
||||
const currentFile = gridView.app.workspace.getActiveFile();
|
||||
if (currentFile) {
|
||||
modeName += `: ${currentFile.basename}`;
|
||||
const targetFile = gridView.app.vault.getAbstractFileByPath(gridView.sourcePath);
|
||||
if (targetFile instanceof TFile) {
|
||||
modeName += `: ${targetFile.basename}`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ function isCustomMode(value: unknown): value is CustomMode {
|
|||
);
|
||||
}
|
||||
|
||||
export type InterceptMode = 'enabled' | 'alt' | 'ctrl' | 'shift' | 'disabled';
|
||||
|
||||
export interface GallerySettings {
|
||||
ignoredFolders: string[]; // 要忽略的資料夾路徑
|
||||
ignoredFolderPatterns: string[]; // 要忽略的資料夾模式
|
||||
|
|
@ -81,8 +83,8 @@ export interface GallerySettings {
|
|||
dateDividerMode: string; // 日期分隔器模式:none, year, month, day
|
||||
showCodeBlocksInSummary: boolean; // 是否在摘要中顯示程式碼區塊
|
||||
folderNoteDisplaySettings: string; // 資料夾筆記設定
|
||||
interceptAllTagClicks: boolean; // 攔截所有tag點擊事件
|
||||
interceptBreadcrumbClicks: boolean; // 攔截Breadcrumb點擊事件
|
||||
interceptAllTagClicks: InterceptMode; // 攔截所有tag點擊事件
|
||||
interceptBreadcrumbClicks: InterceptMode; // 攔截Breadcrumb點擊事件
|
||||
customModes: CustomMode[]; // 自訂模式
|
||||
quickAccessCommandPath: string; // Path used by "Open quick access folder" command
|
||||
quickAccessModeType: string; // View types used by "Open quick access view" command
|
||||
|
|
@ -141,8 +143,8 @@ export const DEFAULT_SETTINGS: GallerySettings = {
|
|||
dateDividerMode: 'none', // 預設不使用日期分隔器
|
||||
showCodeBlocksInSummary: false, // 預設不在摘要中顯示程式碼區塊
|
||||
folderNoteDisplaySettings: 'default', // 預設不處理資料夾筆記
|
||||
interceptAllTagClicks: true, // 預設攔截所有tag點擊事件
|
||||
interceptBreadcrumbClicks: true, // 預設攔截Breadcrumb點擊事件
|
||||
interceptAllTagClicks: 'enabled', // 預設攔截所有tag點擊事件
|
||||
interceptBreadcrumbClicks: 'enabled', // 預設攔截Breadcrumb點擊事件
|
||||
customModes: [
|
||||
{
|
||||
internalName: 'custom-1750837329297',
|
||||
|
|
@ -881,26 +883,34 @@ export class GridExplorerSettingTab extends PluginSettingTab {
|
|||
new Setting(sectionEl)
|
||||
.setName(t('intercept_all_tag_clicks'))
|
||||
.setDesc(t('intercept_all_tag_clicks_desc'))
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.interceptAllTagClicks)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.interceptAllTagClicks = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
.addDropdown(drop => {
|
||||
drop.addOption('enabled', t('intercept_mode_enabled'));
|
||||
drop.addOption('alt', t('intercept_mode_alt'));
|
||||
drop.addOption('ctrl', t('intercept_mode_ctrl'));
|
||||
drop.addOption('shift', t('intercept_mode_shift'));
|
||||
drop.addOption('disabled', t('intercept_mode_disabled'));
|
||||
drop.setValue(this.plugin.settings.interceptAllTagClicks);
|
||||
drop.onChange(async (value) => {
|
||||
this.plugin.settings.interceptAllTagClicks = value as InterceptMode;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
// 攔截Breadcrumb點擊事件
|
||||
new Setting(sectionEl)
|
||||
.setName(t('intercept_breadcrumb_clicks'))
|
||||
.setDesc(t('intercept_breadcrumb_clicks_desc'))
|
||||
.addToggle(toggle => {
|
||||
toggle
|
||||
.setValue(this.plugin.settings.interceptBreadcrumbClicks)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.interceptBreadcrumbClicks = value;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
.addDropdown(drop => {
|
||||
drop.addOption('enabled', t('intercept_mode_enabled'));
|
||||
drop.addOption('alt', t('intercept_mode_alt'));
|
||||
drop.addOption('ctrl', t('intercept_mode_ctrl'));
|
||||
drop.addOption('shift', t('intercept_mode_shift'));
|
||||
drop.addOption('disabled', t('intercept_mode_disabled'));
|
||||
drop.setValue(this.plugin.settings.interceptBreadcrumbClicks);
|
||||
drop.onChange(async (value) => {
|
||||
this.plugin.settings.interceptBreadcrumbClicks = value as InterceptMode;
|
||||
await this.plugin.saveSettings();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -140,9 +140,14 @@ export default {
|
|||
'enable_file_watcher': 'Enable file watcher',
|
||||
'enable_file_watcher_desc': 'When enabled, the view will automatically update when files change. If disabled, you need to click the refresh button manually',
|
||||
'intercept_all_tag_clicks': 'Intercept all tag clicks',
|
||||
'intercept_all_tag_clicks_desc': 'When enabled, all tag clicks will be intercepted and opened in the grid view',
|
||||
'intercept_all_tag_clicks_desc': 'Configure how tag click interception is triggered to open tags in grid view',
|
||||
'intercept_breadcrumb_clicks': 'Intercept breadcrumb clicks',
|
||||
'intercept_breadcrumb_clicks_desc': 'When enabled, breadcrumb clicks will be intercepted and the path will be opened in the grid view',
|
||||
'intercept_breadcrumb_clicks_desc': 'Configure how breadcrumb click interception is triggered to open the folder path in grid view',
|
||||
'intercept_mode_enabled': 'Intercept click',
|
||||
'intercept_mode_alt': 'Click with Alt',
|
||||
'intercept_mode_ctrl': 'Click with Ctrl',
|
||||
'intercept_mode_shift': 'Click with Shift',
|
||||
'intercept_mode_disabled': 'No intercept',
|
||||
'reset_to_default': 'Reset to default',
|
||||
'settings_reset_notice': 'Settings have been reset to default values',
|
||||
'config_management': 'Config management',
|
||||
|
|
@ -333,4 +338,5 @@ export default {
|
|||
'add_to_stash': 'Add to stash',
|
||||
'added_to_stash': 'Added to stash',
|
||||
'import_fail_notice': 'Import failed',
|
||||
'failed_to_move_file': 'Failed to move file',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -139,9 +139,14 @@ export default {
|
|||
'enable_file_watcher': 'ファイル監視を有効にする',
|
||||
'enable_file_watcher_desc': '有効にすると、ファイルの変更を自動的に検出してビューを更新します。無効の場合は、更新ボタンを手動でクリックする必要があります',
|
||||
'intercept_all_tag_clicks': 'すべてのタグクリックをインターセプト',
|
||||
'intercept_all_tag_clicks_desc': '有効にすると、すべてのタグクリックがインターセプトされ、グリッドビューで開かれます',
|
||||
'intercept_all_tag_clicks_desc': 'タグクリックのインターセプトをトリガーする方法を設定し、グリッドビューで開きます',
|
||||
'intercept_breadcrumb_clicks': 'パンくずリストのクリックをインターセプト',
|
||||
'intercept_breadcrumb_clicks_desc': '有効にすると、パンくずリストのクリックがインターセプトされ、パスがグリッドビューで開かれます',
|
||||
'intercept_breadcrumb_clicks_desc': 'パンくずリストのクリックのインターセプトをトリガーする方法を設定し、グリッドビューで開きます',
|
||||
'intercept_mode_enabled': 'クリックをインターセプト',
|
||||
'intercept_mode_alt': 'Altを押しながらクリック',
|
||||
'intercept_mode_ctrl': 'Ctrlを押しながらクリック',
|
||||
'intercept_mode_shift': 'Shiftを押しながらクリック',
|
||||
'intercept_mode_disabled': 'インターセプトしない',
|
||||
'reset_to_default': 'デフォルトに戻す',
|
||||
'settings_reset_notice': '設定値がデフォルト値にリセットされました',
|
||||
'config_management': '設定管理',
|
||||
|
|
|
|||
|
|
@ -140,9 +140,14 @@ export default {
|
|||
'enable_file_watcher': '파일 감시 활성화',
|
||||
'enable_file_watcher_desc': '활성화 시 파일 변경 시 뷰가 자동으로 업데이트됩니다. 비활성화 시 새로고침 버튼을 수동으로 클릭해야 합니다',
|
||||
'intercept_all_tag_clicks': '모든 태그 클릭 가로채기',
|
||||
'intercept_all_tag_clicks_desc': '활성화 시 모든 태그 클릭이 가로채져 그리드 뷰에서 열립니다',
|
||||
'intercept_all_tag_clicks_desc': '태그 클릭 가로채기 트리거 방식을 설정하여 그리드 뷰에서 엽니다',
|
||||
'intercept_breadcrumb_clicks': '브레드크럼 클릭 가로채기',
|
||||
'intercept_breadcrumb_clicks_desc': '활성화 시 브레드크럼 클릭이 가로채져 경로가 그리드 뷰에서 열립니다',
|
||||
'intercept_breadcrumb_clicks_desc': '브레드크럼 클릭 가로채기 트리거 방식을 설정하여 경로를 그리드 뷰에서 엽니다',
|
||||
'intercept_mode_enabled': '클릭 가로채기',
|
||||
'intercept_mode_alt': 'Alt 누르고 클릭',
|
||||
'intercept_mode_ctrl': 'Ctrl 누르고 클릭',
|
||||
'intercept_mode_shift': 'Shift 누르고 클릭',
|
||||
'intercept_mode_disabled': '가로채지 않음',
|
||||
'reset_to_default': '기본값으로 재설정',
|
||||
'settings_reset_notice': '설정이 기본값으로 재설정되었습니다',
|
||||
'config_management': '구성 관리',
|
||||
|
|
|
|||
|
|
@ -139,9 +139,14 @@ export default {
|
|||
'enable_file_watcher': 'Включить наблюдение за файлами',
|
||||
'enable_file_watcher_desc': 'При включении вид будет автоматически обновляться при изменении файлов. Если отключено, нужно вручную нажимать кнопку обновления',
|
||||
'intercept_all_tag_clicks': 'Перехватывать все клики по тегам',
|
||||
'intercept_all_tag_clicks_desc': 'При включении все клики по тегам будут перехватываться и открываться в виде сетки',
|
||||
'intercept_all_tag_clicks_desc': 'Настройте способ перехвата кликов по тегам для открытия в виде сетки',
|
||||
'intercept_breadcrumb_clicks': 'Перехватывать клики по навигационной цепочке',
|
||||
'intercept_breadcrumb_clicks_desc': 'При включении клики по навигационной цепочке будут перехватываться, и путь будет открываться в виде сетки',
|
||||
'intercept_breadcrumb_clicks_desc': 'Настройте способ перехвата кликов по навигационной цепочке для открытия пути в виде сетки',
|
||||
'intercept_mode_enabled': 'Перехватывать клик',
|
||||
'intercept_mode_alt': 'Клик с Alt',
|
||||
'intercept_mode_ctrl': 'Клик с Ctrl',
|
||||
'intercept_mode_shift': 'Клик с Shift',
|
||||
'intercept_mode_disabled': 'Не перехватывать',
|
||||
'reset_to_default': 'Сбросить на значения по умолчанию',
|
||||
'settings_reset_notice': 'Настройки сброшены на значения по умолчанию',
|
||||
'config_management': 'Конфигурация управления',
|
||||
|
|
|
|||
|
|
@ -139,9 +139,14 @@ export default {
|
|||
'enable_file_watcher': 'Увімкнути спостереження за файлами',
|
||||
'enable_file_watcher_desc': 'Коли увімкнено, вигляд автоматично оновлюватиметься при зміні файлів. Якщо вимкнено, потрібно вручну натискати кнопку оновлення',
|
||||
'intercept_all_tag_clicks': 'Перехоплювати всі кліки по тегах',
|
||||
'intercept_all_tag_clicks_desc': 'Коли увімкнено, всі кліки по тегах перехоплюватимуться і відкриватимуться в сітковому вигляді',
|
||||
'intercept_all_tag_clicks_desc': 'Налаштуйте спосіб перехоплення кліків по тегах для відкриття в сітковому вигляді',
|
||||
'intercept_breadcrumb_clicks': 'Перехоплювати кліки по навігаційному ланцюжку',
|
||||
'intercept_breadcrumb_clicks_desc': 'Коли увімкнено, кліки по навігаційному ланцюжку перехоплюватимуться, і шлях відкриватиметься в сітковому вигляді',
|
||||
'intercept_breadcrumb_clicks_desc': 'Налаштуйте спосіб перехоплення кліків по навігаційному ланцюжку для відкриття шляху в сітковому вигляді',
|
||||
'intercept_mode_enabled': 'Перехоплювати клік',
|
||||
'intercept_mode_alt': 'Клік з Alt',
|
||||
'intercept_mode_ctrl': 'Клік з Ctrl',
|
||||
'intercept_mode_shift': 'Клік з Shift',
|
||||
'intercept_mode_disabled': 'Не перехоплювати',
|
||||
'reset_to_default': 'Скинути до значень за замовчуванням',
|
||||
'settings_reset_notice': 'Налаштування скинуто до значень за замовчуванням',
|
||||
'config_management': 'Управління конфігурацією',
|
||||
|
|
|
|||
|
|
@ -140,9 +140,14 @@ export default {
|
|||
'enable_file_watcher': '啟用檔案監控',
|
||||
'enable_file_watcher_desc': '啟用後會自動偵測檔案變更並更新視圖,關閉後需手動點擊重新整理按鈕',
|
||||
'intercept_all_tag_clicks': '攔截所有標籤點擊事件',
|
||||
'intercept_all_tag_clicks_desc': '啟用後會攔截所有標籤點擊事件,並在網格視圖中打開標籤',
|
||||
'intercept_all_tag_clicks_desc': '設定攔截標籤點擊事件的觸發方式,並在網格視圖中打開標籤',
|
||||
'intercept_breadcrumb_clicks': '攔截筆記導航列點擊事件',
|
||||
'intercept_breadcrumb_clicks_desc': '啟用後會攔截筆記導航列點擊事件,並在網格視圖中打開路徑',
|
||||
'intercept_breadcrumb_clicks_desc': '設定攔截筆記導航列點擊事件的觸發方式,並在網格視圖中打開路徑',
|
||||
'intercept_mode_enabled': '攔截點擊',
|
||||
'intercept_mode_alt': '按著Alt點擊',
|
||||
'intercept_mode_ctrl': '按著Ctrl點擊',
|
||||
'intercept_mode_shift': '按著Shift點擊',
|
||||
'intercept_mode_disabled': '不攔截',
|
||||
'reset_to_default': '重置為預設值',
|
||||
'settings_reset_notice': '設定值已重置為預設值',
|
||||
'config_management': '設定檔管理',
|
||||
|
|
@ -333,4 +338,5 @@ export default {
|
|||
'add_to_stash': '加入暫存區',
|
||||
'added_to_stash': '已添加到暫存區',
|
||||
'import_fail_notice': '匯入失敗',
|
||||
'failed_to_move_file': '搬移檔案失敗',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -139,9 +139,14 @@ export default {
|
|||
'enable_file_watcher': '启用文件监控',
|
||||
'enable_file_watcher_desc': '启用后会自动检测文件变更并更新视图,关闭后需手动点击刷新按钮',
|
||||
'intercept_all_tag_clicks': '拦截所有标签点击事件',
|
||||
'intercept_all_tag_clicks_desc': '启用后会拦截所有标签点击事件,改为在网格视图中打开标签',
|
||||
'intercept_all_tag_clicks_desc': '设置拦截标签点击事件的触发方式,并在网格视图中打开标签',
|
||||
'intercept_breadcrumb_clicks': '拦截笔记导航栏点击事件',
|
||||
'intercept_breadcrumb_clicks_desc': '启用后会拦截笔记导航栏点击事件,并在网格视图中打开路径',
|
||||
'intercept_breadcrumb_clicks_desc': '设置拦截笔记导航栏点击事件的触发方式,并在网格视图中打开路径',
|
||||
'intercept_mode_enabled': '拦截点击',
|
||||
'intercept_mode_alt': '按住Alt点击',
|
||||
'intercept_mode_ctrl': '按住Ctrl点击',
|
||||
'intercept_mode_shift': '按住Shift点击',
|
||||
'intercept_mode_disabled': '不拦截',
|
||||
'reset_to_default': '重置为默认值',
|
||||
'settings_reset_notice': '设置值已重置为默认值',
|
||||
'config_management': '配置管理',
|
||||
|
|
@ -332,4 +337,5 @@ export default {
|
|||
'add_to_stash': '加入暂存区',
|
||||
'added_to_stash': '已添加到暂存区',
|
||||
'import_fail_notice': '导入失败',
|
||||
'failed_to_move_file': '搬移文件失败',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -365,17 +365,20 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
return [];
|
||||
} else if (sourceMode === 'backlinks') {
|
||||
// 反向連結模式:找出所有引用當前筆記的檔案
|
||||
const activeFile = app.workspace.getActiveFile();
|
||||
if (!activeFile) {
|
||||
// 注意:不可使用 app.workspace.getActiveFile(),因為當 GridView 本身成為
|
||||
// active leaf 時,getActiveFile() 可能回傳 null 或不可靠的結果。
|
||||
// 目標檔案應在切換到此模式時記錄於 gridView.sourcePath。
|
||||
const targetFile = app.vault.getAbstractFileByPath(gridView.sourcePath);
|
||||
if (!(targetFile instanceof TFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const backlinks = new Set();
|
||||
// 使用 resolvedLinks 來找出反向連結
|
||||
const resolvedLinks = app.metadataCache.resolvedLinks;
|
||||
for (const [sourcePath, links] of Object.entries(resolvedLinks)) {
|
||||
if (Object.keys(links).includes(activeFile.path)) {
|
||||
const sourceFile = app.vault.getAbstractFileByPath(sourcePath);
|
||||
for (const [resolvedSourcePath, links] of Object.entries(resolvedLinks)) {
|
||||
if (Object.keys(links).includes(targetFile.path)) {
|
||||
const sourceFile = app.vault.getAbstractFileByPath(resolvedSourcePath);
|
||||
if (sourceFile instanceof TFile) {
|
||||
backlinks.add(sourceFile);
|
||||
}
|
||||
|
|
@ -385,22 +388,23 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
return sortFiles(Array.from(backlinks) as TFile[], gridView);
|
||||
} else if (sourceMode === 'outgoinglinks') {
|
||||
// 外部連結模式:找出當前筆記所引用的檔案,並包含所有媒體連結
|
||||
const activeFile = app.workspace.getActiveFile();
|
||||
if (!activeFile) {
|
||||
// 同樣改用 gridView.sourcePath,理由同上
|
||||
const targetFile = app.vault.getAbstractFileByPath(gridView.sourcePath);
|
||||
if (!(targetFile instanceof TFile)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const outgoingLinks = new Set<TFile>();
|
||||
// 使用 resolvedLinks 來找出外部連結
|
||||
const resolvedLinks = app.metadataCache.resolvedLinks;
|
||||
const fileLinks = resolvedLinks[activeFile.path];
|
||||
const fileLinks = resolvedLinks[targetFile.path];
|
||||
|
||||
if (fileLinks) {
|
||||
for (const targetPath of Object.keys(fileLinks)) {
|
||||
const targetFile = app.vault.getAbstractFileByPath(targetPath);
|
||||
if (targetFile instanceof TFile && (isDocumentFile(targetFile) ||
|
||||
(settings.showMediaFiles && isMediaFile(targetFile)))) {
|
||||
outgoingLinks.add(targetFile);
|
||||
const linkedFile = app.vault.getAbstractFileByPath(targetPath);
|
||||
if (linkedFile instanceof TFile && (isDocumentFile(linkedFile) ||
|
||||
(settings.showMediaFiles && isMediaFile(linkedFile)))) {
|
||||
outgoingLinks.add(linkedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -408,7 +412,7 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
// 讀取目前筆記內容,找出所有媒體連結
|
||||
if (settings.showMediaFiles) {
|
||||
try {
|
||||
const content = await app.vault.cachedRead(activeFile);
|
||||
const content = await app.vault.cachedRead(targetFile);
|
||||
// 去除 frontmatter
|
||||
const frontMatterInfo = getFrontMatterInfo(content);
|
||||
const contentWithoutFrontmatter = content.substring(frontMatterInfo.contentStart);
|
||||
|
|
@ -422,7 +426,7 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
// 處理內部連結路徑(去除 | 之後的部分)
|
||||
mediaPath = mediaPath.split('|')[0].trim();
|
||||
// 取得對應的 TFile
|
||||
const mediaFile = app.metadataCache.getFirstLinkpathDest(mediaPath, activeFile.path);
|
||||
const mediaFile = app.metadataCache.getFirstLinkpathDest(mediaPath, targetFile.path);
|
||||
if (mediaFile && isMediaFile(mediaFile)) {
|
||||
outgoingLinks.add(mediaFile);
|
||||
}
|
||||
|
|
@ -580,7 +584,12 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
// 所有筆記模式
|
||||
const allVaultFiles = app.vault.getFiles().filter(file => {
|
||||
if (includeMediaFiles === 'media-only') {
|
||||
return settings.showMediaFiles && isMediaFile(file);
|
||||
if (settings.showMediaFiles) {
|
||||
return isMediaFile(file);
|
||||
} else {
|
||||
// 當不顯示媒體但模式被切換為「只顯示媒體檔案」時,回退顯示筆記檔案
|
||||
return isDocumentFile(file);
|
||||
}
|
||||
}
|
||||
// 根據設定決定是否包含媒體檔案
|
||||
if (isDocumentFile(file) ||
|
||||
|
|
@ -594,7 +603,12 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
// 最近檔案模式
|
||||
const recentFiles = app.vault.getFiles().filter(file => {
|
||||
if (includeMediaFiles === 'media-only') {
|
||||
return settings.showMediaFiles && isMediaFile(file);
|
||||
if (settings.showMediaFiles) {
|
||||
return isMediaFile(file);
|
||||
} else {
|
||||
// 當不顯示媒體但模式被切換為「只顯示媒體檔案」時,回退顯示筆記檔案
|
||||
return isDocumentFile(file);
|
||||
}
|
||||
}
|
||||
// 根據設定決定是否包含媒體檔案
|
||||
if (isDocumentFile(file) ||
|
||||
|
|
@ -609,7 +623,12 @@ export async function getFiles(gridView: GridView, includeMediaFiles: boolean |
|
|||
// 隨機筆記模式,從所有筆記中隨機選取
|
||||
const randomFiles = app.vault.getFiles().filter(file => {
|
||||
if (includeMediaFiles === 'media-only') {
|
||||
return settings.showMediaFiles && isMediaFile(file);
|
||||
if (settings.showMediaFiles) {
|
||||
return isMediaFile(file);
|
||||
} else {
|
||||
// 當不顯示媒體但模式被切換為「只顯示媒體檔案」時,回退顯示筆記檔案
|
||||
return isDocumentFile(file);
|
||||
}
|
||||
}
|
||||
// 根據設定決定是否包含媒體檔案
|
||||
if (isDocumentFile(file) ||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export async function getFirstImageFromZip(app: App, file: TFile): Promise<strin
|
|||
export async function findFirstImageInNote(app: App, content: string): Promise<string | null> {
|
||||
try {
|
||||
const pluginAccess = app as unknown as AppPluginAccess;
|
||||
const customDocumentExtensions = pluginAccess.plugins?.plugins?.['obsidian-gridexplorer']?.settings?.customDocumentExtensions;
|
||||
const customDocumentExtensions = pluginAccess.plugins?.plugins?.['gridexplorer']?.settings?.customDocumentExtensions;
|
||||
const isZipEnabled = customDocumentExtensions
|
||||
?.split(',')
|
||||
.map((ext: string) => ext.trim().toLowerCase())
|
||||
|
|
@ -186,21 +186,24 @@ async function resolveUrl(app: App, url: string): Promise<string | null> {
|
|||
}
|
||||
|
||||
async function validateRemoteImage(url: string): Promise<string | null> {
|
||||
const headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
|
||||
};
|
||||
try {
|
||||
const headResponse = await requestUrl({ url, method: 'HEAD' });
|
||||
const headResponse = await requestUrl({ url, method: 'HEAD', headers });
|
||||
if (isSuccessfulStatus(headResponse)) {
|
||||
return url;
|
||||
}
|
||||
|
||||
if (headResponse.status === 405 || headResponse.status === 501) {
|
||||
const getResponse = await requestUrl({ url });
|
||||
const getResponse = await requestUrl({ url, headers });
|
||||
if (isSuccessfulStatus(getResponse)) {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
const getResponse = await requestUrl({ url });
|
||||
const getResponse = await requestUrl({ url, headers });
|
||||
if (isSuccessfulStatus(getResponse)) {
|
||||
return url;
|
||||
}
|
||||
|
|
@ -209,7 +212,8 @@ async function validateRemoteImage(url: string): Promise<string | null> {
|
|||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
// 即使驗證失敗,只要 URL 格式符合圖片規格,仍傳回原網址讓瀏覽器嘗試載入
|
||||
return url;
|
||||
}
|
||||
|
||||
function isSuccessfulStatus(response: RequestUrlResponse): boolean {
|
||||
|
|
|
|||
119
styles.css
119
styles.css
|
|
@ -12,7 +12,7 @@
|
|||
.ge-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(var(--grid-item-width, 300px), 1fr));
|
||||
gap: 10px;
|
||||
gap: 11px;
|
||||
align-items: start;
|
||||
align-content: start;
|
||||
flex: 1;
|
||||
|
|
@ -20,6 +20,10 @@
|
|||
}
|
||||
|
||||
.ge-grid-view-container .ge-grid-container {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
:is(.mod-left-split, .mod-right-split) .ge-grid-view-container .ge-grid-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
|
@ -150,8 +154,8 @@
|
|||
.ge-title.ge-multiline-title {
|
||||
white-space: normal;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
line-height: 1.3;
|
||||
overflow: hidden;
|
||||
|
|
@ -171,6 +175,13 @@
|
|||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.mod-left-split .ge-grid-container .ge-grid-item .ge-content-area p,
|
||||
.mod-right-split .ge-grid-container .ge-grid-item .ge-content-area p,
|
||||
.workspace-drawer .ge-grid-container .ge-grid-item .ge-content-area p {
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
}
|
||||
|
||||
.ge-content-area p:empty {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -228,7 +239,7 @@
|
|||
}
|
||||
|
||||
.ge-non-doc-icon.ge-zip {
|
||||
color: var(--color-folder); /* obsidian color or fallback */
|
||||
color: var(--color-yellow);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -457,53 +468,77 @@
|
|||
}
|
||||
|
||||
/* 資料夾和筆記顏色 Folder & Note Colors */
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-red,
|
||||
.ge-grid-item.ge-note-color-red {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-red {
|
||||
background-color: rgba(var(--color-red-rgb), 0.2);
|
||||
border-color: rgba(var(--color-red-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-red {
|
||||
background-color: rgba(var(--color-red-rgb), 0.2);
|
||||
border-color: rgba(var(--color-red-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-orange,
|
||||
.ge-grid-item.ge-note-color-orange {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-orange {
|
||||
background-color: rgba(var(--color-orange-rgb), 0.2);
|
||||
border-color: rgba(var(--color-orange-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-orange {
|
||||
background-color: rgba(var(--color-orange-rgb), 0.2);
|
||||
border-color: rgba(var(--color-orange-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-yellow,
|
||||
.ge-grid-item.ge-note-color-yellow {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-yellow {
|
||||
background-color: rgba(var(--color-yellow-rgb), 0.2);
|
||||
border-color: rgba(var(--color-yellow-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-yellow {
|
||||
background-color: rgba(var(--color-yellow-rgb), 0.2);
|
||||
border-color: rgba(var(--color-yellow-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-green,
|
||||
.ge-grid-item.ge-note-color-green {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-green {
|
||||
background-color: rgba(var(--color-green-rgb), 0.2);
|
||||
border-color: rgba(var(--color-green-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-green {
|
||||
background-color: rgba(var(--color-green-rgb), 0.2);
|
||||
border-color: rgba(var(--color-green-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-cyan,
|
||||
.ge-grid-item.ge-note-color-cyan {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-cyan {
|
||||
background-color: rgba(var(--color-cyan-rgb), 0.2);
|
||||
border-color: rgba(var(--color-cyan-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-cyan {
|
||||
background-color: rgba(var(--color-cyan-rgb), 0.2);
|
||||
border-color: rgba(var(--color-cyan-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-blue,
|
||||
.ge-grid-item.ge-note-color-blue {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-blue {
|
||||
background-color: rgba(var(--color-blue-rgb), 0.2);
|
||||
border-color: rgba(var(--color-blue-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-blue {
|
||||
background-color: rgba(var(--color-blue-rgb), 0.2);
|
||||
border-color: rgba(var(--color-blue-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-purple,
|
||||
.ge-grid-item.ge-note-color-purple {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-purple {
|
||||
background-color: rgba(var(--color-purple-rgb), 0.2);
|
||||
border-color: rgba(var(--color-purple-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-purple {
|
||||
background-color: rgba(var(--color-purple-rgb), 0.2);
|
||||
border-color: rgba(var(--color-purple-rgb), 0.25);
|
||||
}
|
||||
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-pink,
|
||||
.ge-grid-item.ge-note-color-pink {
|
||||
.ge-grid-item.ge-folder-item.ge-folder-color-pink {
|
||||
background-color: rgba(var(--color-pink-rgb), 0.2);
|
||||
border-color: rgba(var(--color-pink-rgb), 0.5);
|
||||
}
|
||||
.ge-grid-item.ge-note-color-pink {
|
||||
background-color: rgba(var(--color-pink-rgb), 0.2);
|
||||
border-color: rgba(var(--color-pink-rgb), 0.25);
|
||||
}
|
||||
|
||||
/* 筆記文字顏色 Note Text Color */
|
||||
.ge-content-area p.ge-note-color-red-text {
|
||||
|
|
@ -2288,6 +2323,7 @@ a.ge-current-folder:hover {
|
|||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
/* 筆記頂部按鈕列 */
|
||||
|
|
@ -2319,7 +2355,8 @@ a.ge-current-folder:hover {
|
|||
/* 捲動區域 */
|
||||
.ge-note-scroll-container {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--background-primary);
|
||||
font-size: var(--font-text-size);
|
||||
}
|
||||
|
|
@ -2375,7 +2412,15 @@ a.ge-current-folder:hover {
|
|||
min-height: 100%;
|
||||
}
|
||||
|
||||
.is-mobile .ge-note-content-container {
|
||||
:is(.mod-left-split, .mod-right-split) .ge-note-content-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.is-tablet .ge-note-content-container {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.is-phone .ge-note-content-container {
|
||||
padding-top: 15px;
|
||||
}
|
||||
|
||||
|
|
@ -2414,6 +2459,21 @@ a.ge-current-folder:hover {
|
|||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 讓圖片延展到包含卷軸的介面寬度 (僅限行動裝置,且排除程式碼區塊、表格、清單等特殊容器內的圖片) */
|
||||
.is-mobile .ge-note-content img:not(pre *, code *, [class*="block-language-"] *, .callout *, table *, ul *, ol *, blockquote *),
|
||||
.is-mobile .ge-note-content .internal-embed:has(img):not(pre *, code *, [class*="block-language-"] *, .callout *, table *, ul *, ol *, blockquote *) {
|
||||
width: 100cqw;
|
||||
max-width: 100cqw;
|
||||
margin-left: calc(50% - 50cqw);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.is-mobile .ge-note-content .internal-embed:has(img):not(pre *, code *, [class*="block-language-"] *, .callout *, table *, ul *, ol *, blockquote *) img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.ge-note-content h1,
|
||||
.ge-note-content h2,
|
||||
.ge-note-content h3,
|
||||
|
|
@ -3037,15 +3097,22 @@ a.ge-current-folder:hover {
|
|||
/* --------------------------------------------------------------------------- */
|
||||
/* 手機版頂部邊距適配 (針對 Obsidian 更新後的介面調整) */
|
||||
|
||||
.is-phone .ge-grid-view-container,
|
||||
.is-phone .ge-note-view-container {
|
||||
/* 讓整個插件容器避開頂部安全區域 (狀態列/瀏海) */
|
||||
/* Obsidian 更新後將此變數用於處理頂部間距 */
|
||||
.is-phone .ge-grid-view-container:not(.mod-left-split *, .mod-right-split *, .workspace-drawer *),
|
||||
.is-phone .ge-note-view-container:not(.mod-left-split *, .mod-right-split *, .workspace-drawer *) {
|
||||
/* 讓非側邊欄的主工作區插件容器避開頂部安全區域 (狀態列/瀏海) */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
|
||||
/* 平板上的右側邊欄或底部/側邊抽屜加上頂部 5px 邊距 */
|
||||
.is-mobile .mod-right-split .ge-grid-view-container,
|
||||
.is-mobile .mod-right-split .ge-note-view-container,
|
||||
.is-mobile .workspace-drawer .ge-grid-view-container,
|
||||
.is-mobile .workspace-drawer .ge-note-view-container {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.is-phone .ge-grid-view-container .ge-grid-container {
|
||||
padding-bottom: calc(var(--view-bottom-spacing) + var(--size-4-3));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue