Compare commits

...

6 commits
3.4.13 ... main

Author SHA1 Message Date
Devon22
5d93b16e8d 3.4.19 2026-07-21 21:36:38 +08:00
Devon22
ce13321edd 3.4.18 2026-07-09 18:24:02 +08:00
Devon22
f031515fda 3.4.17 2026-07-07 23:07:34 +08:00
Devon22
f448ffb986 3.4.16 2026-07-06 00:13:43 +08:00
Devon22
9602dc2c75 3.4.15 2026-07-04 22:55:19 +08:00
Devon22
8a6a48f280 3.4.14 2026-07-02 23:47:18 +08:00
21 changed files with 586 additions and 243 deletions

View file

@ -1,9 +1,9 @@
{
"id": "gridexplorer",
"name": "GridExplorer",
"version": "3.4.13",
"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
View file

@ -1,12 +1,12 @@
{
"name": "gridexplorer",
"version": "3.4.13",
"version": "3.4.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "gridexplorer",
"version": "3.4.13",
"version": "3.4.19",
"license": "MIT",
"dependencies": {
"jszip": "^3.10.1"

View file

@ -1,7 +1,7 @@
{
"name": "gridexplorer",
"version": "3.4.13",
"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",

View file

@ -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);
// 繼續處理其他檔案
}

View file

@ -1,4 +1,4 @@
import { TFile, Platform, setIcon, setTooltip, MarkdownRenderer, Component, Menu } 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';
@ -104,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;
@ -125,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;
@ -168,7 +186,6 @@ export class GridPreviewManager {
// 只有在進入我們自己定義的拖曳狀態時,才攔截事件與阻止預設行為
if (isHorizontalDragging || isDragging) {
e.stopPropagation();
if (e.cancelable) {
e.preventDefault();
}
@ -196,7 +213,7 @@ export class GridPreviewManager {
};
const handleTouchEnd = (e: TouchEvent) => {
if (isHorizontalDragging || isDragging) {
if (!isFromEdge) {
e.stopPropagation();
}
@ -303,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');
@ -339,8 +361,6 @@ export class GridPreviewManager {
setTooltip(noteTitle, file.basename);
}
const rightBar = topBar.createDiv('ge-note-top-right');
// 編輯按鈕
@ -350,7 +370,6 @@ 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');
@ -526,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);
}
// 創建筆記內容區域
@ -671,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(
@ -680,6 +576,7 @@ export class GridPreviewManager {
file.path,
this.previewComponent!
);
if (!this.view.isShowingNote || this.view.previewedFile !== file) return;
// 加上自訂屬性 data-source-path
noteContentArea
@ -718,16 +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;
this.view.previewedFile = file;
// 廣播事件,讓其他 GridView例如側邊欄的反向連結模式可以跟著更新
this.view.app.workspace.trigger('ge-preview-file-change', file, this.view);
}
// 隱藏筆記顯示
@ -752,6 +643,10 @@ export class GridPreviewManager {
}
if (this.view.noteViewContainer) {
// 中斷所有正在下載的圖片,釋放瀏覽器連線池給下一次載入
this.view.noteViewContainer
.querySelectorAll<HTMLImageElement>('img')
.forEach((img) => { img.removeAttribute('src'); });
this.view.noteViewContainer.remove();
this.view.noteViewContainer = null;
}
@ -839,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');
@ -853,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();
});
@ -943,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 => {
@ -1396,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 ? ' ' : ', ' });
}
});
}
}
}
}

View file

@ -1262,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;
});
@ -1537,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');

View file

@ -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;

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
// 繼續處理其他檔案
}
}
})();
});
});

View file

@ -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);
// 繼續處理其他檔案
}

View file

@ -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();
});
});

View file

@ -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',
};

View 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': '設定管理',

View file

@ -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': '구성 관리',

View 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': 'Конфигурация управления',

View 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': 'Управління конфігурацією',

View file

@ -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': '搬移檔案失敗',
};

View 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': '搬移文件失败',
};

View 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 {

View file

@ -2323,6 +2323,7 @@ a.ge-current-folder:hover {
z-index: 100;
display: flex;
flex-direction: column;
container-type: inline-size;
}
/* 筆記頂部按鈕列 */
@ -2354,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);
}
@ -2457,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,