devon22_obsidian-gridexplorer/src/renderFiles.ts
2025-08-02 01:44:27 +08:00

260 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { TFile } from 'obsidian';
import { GridView } from './GridView';
import { isDocumentFile, isMediaFile, sortFiles, ignoredFiles, getFiles } from './fileUtils';
import { t } from './translations';
export async function renderFiles(gridView: GridView, container: HTMLElement) {
let loadingDiv: HTMLElement | null = null;
if (gridView.searchQuery || gridView.sourceMode === 'tasks') {
// 顯示搜尋中的提示
loadingDiv = container.createDiv({ text: t('searching'), cls: 'ge-loading-indicator' });
}
let files: TFile[] = [];
// 使用 Map 來記錄原始順序
let fileIndexMap = new Map<TFile, number>();
if (gridView.searchQuery) {
// 取得 vault 中所有支援的檔案
let allFiles: TFile[] = [];
if (gridView.searchAllFiles) {
// 全部檔案
allFiles = gridView.app.vault.getFiles().filter(file =>
isDocumentFile(file) || (isMediaFile(file) && gridView.searchMediaFiles)
);
} else {
// 當前位置檔案
allFiles = await getFiles(gridView, gridView.searchMediaFiles);
if (gridView.sourceMode === 'bookmarks') {
allFiles = allFiles.filter(file =>
isDocumentFile(file) || (isMediaFile(file) && gridView.searchMediaFiles)
);
// 使用 Map 來記錄原始順序
allFiles.forEach((file, index) => {
fileIndexMap.set(file, index);
});
} else if (gridView.sourceMode === 'search') {
allFiles = allFiles.filter(file =>
isDocumentFile(file) || (isMediaFile(file) && gridView.searchMediaFiles)
);
} else if (gridView.sourceMode === 'recent-files') {
// 搜尋"最近檔案"的當前位置時先作忽略檔案和只取前n筆
allFiles = ignoredFiles(allFiles, gridView).slice(0, gridView.plugin.settings.recentFilesCount);
} else if (gridView.sourceMode.startsWith('custom-')) {
// 使用 Map 來記錄原始順序
allFiles.forEach((file, index) => {
fileIndexMap.set(file, index);
});
}
}
// 根據搜尋關鍵字進行過濾(不分大小寫)
const searchTerms = gridView.searchQuery.toLowerCase().split(/\s+/).filter(term => term.trim() !== '');
// 分離標籤搜尋、一般搜尋和連結搜尋
const tagTerms: string[] = [];
const linkTerms: string[] = [];
const normalTerms: string[] = [];
for (const term of searchTerms) {
if (term.startsWith('#')) {
tagTerms.push(term.substring(1));
} else if (term.startsWith('[[') && term.endsWith(']]')) {
linkTerms.push(term.slice(2, -2)); // 移除 [[ 和 ]]
} else {
normalTerms.push(term);
}
}
// 檢查是否有 Obsidian 連結格式 [[ ]]
let linkTargetFileSets: Set<string>[] = [];
const hasLinkSearch = linkTerms.length > 0;
if (hasLinkSearch) {
for (const linkTarget of linkTerms) {
// 尋找目標檔案
let targetFile = gridView.app.vault.getAbstractFileByPath(linkTarget + '.md');
if (!targetFile) {
targetFile = gridView.app.metadataCache.getFirstLinkpathDest(linkTarget, '');
}
if (targetFile && 'extension' in targetFile) {
// 使用 Obsidian 的 getBacklinksForFile API
const backlinks = (gridView.app.metadataCache as any).getBacklinksForFile(targetFile);
const currentLinkFiles = new Set<string>();
if (backlinks) {
// 收集所有反向連結的檔案路徑
for (const [filePath, links] of backlinks.data.entries()) {
currentLinkFiles.add(filePath);
}
}
linkTargetFileSets.push(currentLinkFiles);
} else {
// 如果找不到目標檔案,加入空集合
linkTargetFileSets.push(new Set<string>());
}
}
}
// 使用 Promise.all 來非同步地讀取所有檔案內容,順序可能會跟之前不同
await Promise.all(
allFiles.map(async file => {
// 檢查連結搜尋條件:檔案必須在所有連結的反向連結中
if (hasLinkSearch) {
const isInAllLinkTargets = linkTargetFileSets.every(linkSet => linkSet.has(file.path));
if (!isInAllLinkTargets) {
return; // 如果檔案不在所有連結的反向連結中,直接跳過
}
}
const fileName = file.name.toLowerCase();
let matchesNormalTerms = true;
let matchesTags = true;
// 檢查一般搜尋詞
if (normalTerms.length > 0) {
if (gridView.searchFilesNameOnly) {
// 僅檢查檔名
matchesNormalTerms = normalTerms.every(term => fileName.includes(term));
} else {
// 檢查每個一般搜尋字串是否存在於檔名或(若為 Markdown檔案內容中
matchesNormalTerms = true;
let contentLower: string | null = null;
for (const term of normalTerms) {
if (fileName.includes(term)) {
continue; // 此關鍵字已在檔名中找到
}
if (file.extension === 'md') {
// 延遲讀取內容,避免不必要的 IO
if (contentLower === null) {
contentLower = (await gridView.app.vault.cachedRead(file)).toLowerCase();
}
if (contentLower.includes(term)) {
continue; // 此關鍵字在內容中找到
}
}
// 若此關鍵字既不在檔名也不在內容中,則不符合
matchesNormalTerms = false;
break;
}
}
}
// 檢查標籤搜尋詞
if (tagTerms.length > 0) {
if (file.extension !== 'md') {
matchesTags = false; // 非 Markdown 檔案不能匹配標籤
} else {
const fileCache = gridView.app.metadataCache.getFileCache(file);
matchesTags = false;
if (fileCache) {
const collectedTags: string[] = [];
// 內文標籤
if (Array.isArray(fileCache.tags)) {
for (const t of fileCache.tags) {
if (t && t.tag) {
const clean = t.tag.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
// frontmatter 標籤
if (fileCache.frontmatter && fileCache.frontmatter.tags) {
const fmTags = fileCache.frontmatter.tags;
if (typeof fmTags === 'string') {
collectedTags.push(
...fmTags.split(/[,\s]+/)
.map(t => t.toLowerCase().replace(/^#/, ''))
.filter(t => t.trim() !== '')
);
} else if (Array.isArray(fmTags)) {
for (const t of fmTags) {
if (typeof t === 'string') {
const clean = t.toLowerCase().replace(/^#/, '');
collectedTags.push(...clean.split(/\s+/).filter(st => st.trim() !== ''));
}
}
}
}
matchesTags = tagTerms.every(tag => collectedTags.includes(tag));
}
}
}
// 只有當所有條件都符合時才加入結果
if (matchesNormalTerms && matchesTags) {
files.push(file);
}
})
);
// 排序檔案
if (gridView.searchAllFiles) {
// 搜尋所有檔案時
files = sortFiles(files, gridView);
} else {
// 非搜尋所有檔案時
if (gridView.sourceMode === 'bookmarks') {
// 保持原始順序
files.sort((a, b) => {
const indexA = fileIndexMap.get(a) ?? Number.MAX_SAFE_INTEGER;
const indexB = fileIndexMap.get(b) ?? Number.MAX_SAFE_INTEGER;
return indexA - indexB;
});
} else if (gridView.sourceMode === 'recent-files') {
// 臨時的排序類型
const sortType = gridView.sortType;
gridView.sortType = 'mtime-desc';
files = sortFiles(files, gridView);
gridView.sortType = sortType;
} else if (gridView.sourceMode === 'random-note') {
// 臨時的排序類型
const sortType = gridView.sortType;
gridView.sortType = 'random';
files = sortFiles(files, gridView);
gridView.sortType = sortType;
} else if (gridView.sourceMode.startsWith('custom-')) {
// 保持原始順序
files.sort((a, b) => {
const indexA = fileIndexMap.get(a) ?? Number.MAX_SAFE_INTEGER;
const indexB = fileIndexMap.get(b) ?? Number.MAX_SAFE_INTEGER;
return indexA - indexB;
});
} else {
files = sortFiles(files, gridView);
}
}
// 忽略檔案
files = ignoredFiles(files, gridView);
} else {
// 無搜尋關鍵字的情況
files = await getFiles(gridView, gridView.randomNoteIncludeMedia);
// 忽略檔案
files = ignoredFiles(files, gridView)
// 最近檔案模式只取前n筆
if (gridView.sourceMode === 'recent-files') {
files = files.slice(0, gridView.plugin.settings.recentFilesCount);
}
// 隨機筆記模式只取前n筆
if (gridView.sourceMode === 'random-note') {
files = files.slice(0, gridView.plugin.settings.randomNoteCount);
}
}
if (loadingDiv) {
loadingDiv.remove();
}
return files;
}