import "./folder-icon.less"; import { AFItem, CachedMetadata, FileExplorerView, FolderItem, TAbstractFile, TFile, TFolder, } from "obsidian"; import { dirname } from "path"; import type ALxFolderNote from "../fn-main"; import { afItemMark, isFolder } from "../misc"; import FEHandler_Base from "./base"; export const folderIconMark = "alx-folder-icons"; const folderNoteClass = "alx-folder-note"; const folderClass = "alx-folder-with-note"; const emptyFolderClass = "alx-empty-folder"; export default class FolderMark extends FEHandler_Base { constructor(plugin: ALxFolderNote, fileExplorer: FileExplorerView) { super(plugin, fileExplorer); this.initFolderMark(); if (this.plugin.settings.folderIcon) { this.initFolderIcon(); } if (this.plugin.settings.hideCollapseIndicator) { this.initHideCollapseIndicator(); } } queues = { mark: { queue: new Map<string, [revert: boolean]>(), action: (path: string, revert: boolean) => { const item = this.getAfItem(path); if (!item) { console.warn("no afitem found for path %s, escaping...", path); return; } if (isFolder(item)) { if (revert === !!item.isFolderWithNote) { item.el.toggleClass(folderClass, !revert); item.isFolderWithNote = revert ? undefined : true; if (this.plugin.settings.hideCollapseIndicator) item.el.toggleClass( emptyFolderClass, revert ? false : item.file.children.length === 1, ); } this._updateIcon(path, revert, item); } else if (revert === !!item.isFolderNote) { item.el.toggleClass(folderNoteClass, !revert); item.isFolderNote = revert ? undefined : true; } }, }, changedFolder: { queue: new Set<string>(), action: (path: string) => { let note = this.fncApi.getFolderNote(path); if (note) { (this.getAfItem(path) as FolderItem)?.el.toggleClass( emptyFolderClass, note.parent.children.length === 1, ); } }, }, }; private initFolderMark() { const { vault, metadataCache } = this.app; this.markAll(); //#region folder note events setup [ vault.on("folder-note:create", (note: TFile, folder: TFolder) => { this.setMark(note); this.setMark(folder); }), vault.on("folder-note:delete", (note: TFile, folder: TFolder) => { this.setMark(note, true); this.setMark(folder, true); }), vault.on("folder-note:rename", () => { // fe-item in dom will be reused, do nothing for now }), vault.on("folder-note:cfg-changed", () => { this.markAll(true); window.setTimeout(this.markAll, 200); }), metadataCache.on("changed", (file) => { let folder; if ((folder = this.fncApi.getFolderFromNote(file))) { this.setMark(folder); } }), ].forEach(this.plugin.registerEvent.bind(this.plugin)); } //#region set class mark for folder notes and folders public setMark = ( target: AFItem | TAbstractFile | string, revert = false, ) => { if (!target) return; const { queue } = this.queues.mark; let path: string; if (target instanceof TAbstractFile) { path = target.path; } else if (typeof target === "string") { path = target; } else { path = target.file.path; } queue.set(path, [revert]); this.execQueue("mark"); }; public markAll = (revert = false) => { this.iterateItems((item: AFItem) => { if (isFolder(item) && !revert) { this.markFolderNote(item.file); } else if (revert) { this.setMark(item, true); } }); }; markFolderNote = (af: TAbstractFile): boolean => { if (af instanceof TFolder && af.isRoot()) return false; const { getFolderNote, getFolderFromNote } = this.fncApi; let found: TAbstractFile | null = null; if (af instanceof TFile) found = getFolderFromNote(af); else if (af instanceof TFolder) found = getFolderNote(af); if (found) { this.setMark(found); this.setMark(af); } else { this.setMark(af, true); } return !!found; }; // #endregion //#region folder icon setup private initFolderIcon() { document.body.toggleClass(folderIconMark, this.plugin.settings.folderIcon); const { vault } = this.app; const updateIcon = () => { for (const path of this.foldersWithIcon) { this.setMark(path); } }; [ vault.on("iconsc:initialized", updateIcon), vault.on("iconsc:changed", updateIcon), ].forEach(this.plugin.registerEvent.bind(this.plugin)); } foldersWithIcon = new Set<string>(); private _updateIcon(path: string, revert: boolean, item: afItemMark) { const api = this.plugin.IconSCAPI; if (!api) return; let folderNotePath: string | undefined, metadata: CachedMetadata | undefined; const revertIcon = () => { delete item.el.dataset.icon; delete item.el.dataset["icon-type"]; this.foldersWithIcon.delete(path); item.el.style.removeProperty("--alx-folder-icon-txt"); item.el.style.removeProperty("--alx-folder-icon-url"); }; if (revert) { revertIcon(); } else if ( (folderNotePath = this.fncApi.getFolderNotePath(path)?.path) && (metadata = this.plugin.app.metadataCache.getCache(folderNotePath)) ) { let iconId = metadata.frontmatter?.icon, icon; if ( iconId && typeof iconId === "string" && (icon = api.getIcon(iconId, true)) ) { this.foldersWithIcon.add(path); item.el.dataset.icon = iconId.replace(/^:|:$/g, ""); if (!api.isEmoji(iconId)) { item.el.dataset.iconType = "svg"; item.el.style.setProperty("--alx-folder-icon-url", `url("${icon}")`); item.el.style.setProperty("--alx-folder-icon-txt", '" "'); } else { item.el.dataset.iconType = "emoji"; item.el.style.setProperty("--alx-folder-icon-url", '""'); item.el.style.setProperty("--alx-folder-icon-txt", `"${icon}"`); } } else if (item.el.dataset.icon) { revertIcon(); } } } //#endregion //#region set hide collapse indicator private initHideCollapseIndicator() { if (!this.plugin.settings.hideCollapseIndicator) return; const { vault } = this.app; [ vault.on("create", (file) => this.setChangedFolder(file.parent.path)), vault.on("delete", (file) => { let parent = dirname(file.path); this.setChangedFolder(parent === "." ? "/" : parent); }), vault.on("rename", (file, oldPath) => { this.setChangedFolder(file.parent.path); let parent = dirname(oldPath); this.setChangedFolder(parent === "." ? "/" : parent); }), ].forEach(this.plugin.registerEvent.bind(this.plugin)); } setChangedFolder = (folderPath: string) => { if (!folderPath || folderPath === "/") return; this.queues.changedFolder.queue.add(folderPath); this.execQueue("changedFolder"); }; //#endregion }