diff --git a/src/commands.ts b/src/commands.ts index 307a087..d2cd74d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,4 +1,4 @@ -import { App, TFolder, Menu, TAbstractFile, Notice, TFile, Editor, MarkdownView, Platform } from 'obsidian'; +import { App, TFolder, Menu, TAbstractFile, Notice, TFile, Editor, MarkdownView, Platform, stringifyYaml } from 'obsidian'; import FolderNotesPlugin from './main'; import { getFolderNote, createFolderNote, deleteFolderNote, turnIntoFolderNote, openFolderNote, extractFolderName } from './functions/folderNoteFunctions'; import { ExcludedFolder } from './excludedFolder'; @@ -10,65 +10,25 @@ export class Commands { this.app = app; } registerCommands() { - this.plugin.registerEvent(this.plugin.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => { - const text = editor.getSelection().trim(); - menu.addItem((item) => { - item.setTitle('Create folder overview') - .setIcon('edit') - .onClick(() => { - editor.replaceSelection('```folder-overview\n```\n'); - }); - }); - if (!text || text.trim() === '') return; - menu.addItem((item) => { - item.setTitle('Create folder note') - .setIcon('edit') - .onClick(() => { - const file = view.file; - if (!(file instanceof TFile)) return; - const blacklist = ['*', '\\', '"', '/', '<', '>', '?', '|', ':']; - for (const char of blacklist) { - if (text.includes(char)) { - new Notice('File name cannot contain any of the following characters: * " \\ / < > : | ?'); - return; - } - } - if (text.endsWith('.')) { - new Notice('File name cannot end with a dot'); - return; - } - let folder: TAbstractFile | null; - const folderPath = this.plugin.getFolderPathFromString(file.path); - if (folderPath === '') { - folder = this.plugin.app.vault.getAbstractFileByPath(text); - if (folder instanceof TFolder) { - return new Notice('Folder note already exists'); - } else { - this.plugin.app.vault.createFolder(text); - createFolderNote(this.plugin, text, false); - } - } else { - folder = this.plugin.app.vault.getAbstractFileByPath(folderPath + '/' + text); - if (folder instanceof TFolder) { - return new Notice('Folder note already exists'); - } - if (this.plugin.settings.storageLocation === 'parentFolder') { - if (this.app.vault.getAbstractFileByPath(folderPath + '/' + text + this.plugin.settings.folderNoteType)) { - return new Notice('File already exists'); - } - } - this.plugin.app.vault.createFolder(folderPath + '/' + text); - createFolderNote(this.plugin, folderPath + '/' + text, false); - } - const fileName = this.plugin.settings.folderNoteName.replace('{{folder_name}}', text); - if (fileName === text) { - editor.replaceSelection(`[[${fileName}]]`); - } else { - editor.replaceSelection(`[[${fileName}|${text}]]`); - } - }); - }); - })); + this.editorCommands(); + this.fileCommands(); + this.regularCommands(); + } + regularCommands() { + this.plugin.addCommand({ + id: 'turn-into-folder-note', + name: 'Make current active note a folder note', + callback: () => { + const file = this.app.workspace.getActiveFile(); + if (!(file instanceof TFile)) return; + const folder = file.parent; + if (!(folder instanceof TFolder)) return; + const folderNote = getFolderNote(this.plugin, folder.path); + turnIntoFolderNote(this.plugin, file, folder, folderNote); + } + }); + } + fileCommands() { this.plugin.registerEvent(this.app.workspace.on('file-menu', (menu: Menu, file: TAbstractFile) => { let folder: TAbstractFile | TFolder | null = file.parent; if (file instanceof TFile) { @@ -190,4 +150,71 @@ export class Commands { }); })); } + editorCommands() { + this.plugin.registerEvent(this.plugin.app.workspace.on('editor-menu', (menu: Menu, editor: Editor, view: MarkdownView) => { + const text = editor.getSelection().trim(); + const fileContent = editor.getValue().trim(); + const line = editor.getCursor().line; + const lineText = editor.getLine(line); + if (!fileContent.includes('```folder-overview') && lineText.trim() == '') { + menu.addItem((item) => { + item.setTitle('Create folder overview') + .setIcon('edit') + .onClick(() => { + const yaml = stringifyYaml(this.plugin.settings.defaultOverview) + editor.replaceSelection(`\`\`\`folder-overview\n${yaml}\`\`\`\n`); + }); + }); + } + if (!text || text.trim() === '') return; + menu.addItem((item) => { + item.setTitle('Create folder note') + .setIcon('edit') + .onClick(() => { + const file = view.file; + if (!(file instanceof TFile)) return; + const blacklist = ['*', '\\', '"', '/', '<', '>', '?', '|', ':']; + for (const char of blacklist) { + if (text.includes(char)) { + new Notice('File name cannot contain any of the following characters: * " \\ / < > : | ?'); + return; + } + } + if (text.endsWith('.')) { + new Notice('File name cannot end with a dot'); + return; + } + let folder: TAbstractFile | null; + const folderPath = this.plugin.getFolderPathFromString(file.path); + if (folderPath === '') { + folder = this.plugin.app.vault.getAbstractFileByPath(text); + if (folder instanceof TFolder) { + return new Notice('Folder note already exists'); + } else { + this.plugin.app.vault.createFolder(text); + createFolderNote(this.plugin, text, false); + } + } else { + folder = this.plugin.app.vault.getAbstractFileByPath(folderPath + '/' + text); + if (folder instanceof TFolder) { + return new Notice('Folder note already exists'); + } + if (this.plugin.settings.storageLocation === 'parentFolder') { + if (this.app.vault.getAbstractFileByPath(folderPath + '/' + text + this.plugin.settings.folderNoteType)) { + return new Notice('File already exists'); + } + } + this.plugin.app.vault.createFolder(folderPath + '/' + text); + createFolderNote(this.plugin, folderPath + '/' + text, false); + } + const fileName = this.plugin.settings.folderNoteName.replace('{{folder_name}}', text); + if (fileName === text) { + editor.replaceSelection(`[[${fileName}]]`); + } else { + editor.replaceSelection(`[[${fileName}|${text}]]`); + } + }); + }); + })); + } } diff --git a/src/folderOverview.ts b/src/folderOverview.ts deleted file mode 100644 index b43ecb6..0000000 --- a/src/folderOverview.ts +++ /dev/null @@ -1,380 +0,0 @@ -import { MarkdownPostProcessorContext, parseYaml, TAbstractFile, TFolder, TFile } from 'obsidian'; -import { extractFolderName, getFolderNote } from './functions/folderNoteFunctions'; -import FolderNotesPlugin from './main'; -import { FolderOverviewSettings } from './modals/folderOverview'; -import { getExcludedFolder } from './excludedFolder'; -export type yamlSettings = { - title?: string; - disableTitle?: boolean; - depth?: number; - type?: 'folder' | 'markdown' | 'canvas' | 'other' | 'pdf' | 'images' | 'audio' | 'video' | 'all'; - includeTypes?: string[]; - style?: 'list' | 'grid'; - disableFileTag?: boolean; - sortBy?: 'name' | 'created' | 'modified' | 'nameAsc' | 'createdAsc' | 'modifiedAsc'; - showEmptyFolders?: boolean; - onlyIncludeSubfolders?: boolean; -}; - -export function createCanvasOverview() { - // console.log('test'); -} - -export function createOverview(plugin: FolderNotesPlugin, source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { - // parse source to yaml - let yaml: yamlSettings = parseYaml(source); - if (!yaml) { - yaml = { - title: plugin.settings.defaultOverview.title, - disableTitle: plugin.settings.defaultOverview.disableTitle, - depth: plugin.settings.defaultOverview.depth, - type: plugin.settings.defaultOverview.type, - includeTypes: plugin.settings.defaultOverview.includeTypes, - style: plugin.settings.defaultOverview.style, - disableFileTag: plugin.settings.defaultOverview.disableFileTag, - sortBy: plugin.settings.defaultOverview.sortBy, - showEmptyFolders: plugin.settings.defaultOverview.showEmptyFolders, - onlyIncludeSubfolders: plugin.settings.defaultOverview.onlyIncludeSubfolders, - }; - } - const depth = yaml?.depth || 1; - let title = yaml?.title || plugin.settings.defaultOverview.title || '{{folderName}} overview'; - const folderName = extractFolderName(plugin.settings.folderNoteName, plugin.removeExtension(plugin.getFolderNameFromPathString(ctx.sourcePath))); - title = title.replaceAll('{{folderName}}', folderName || ''); - const disableTitle = yaml?.disableTitle || plugin.settings.defaultOverview.disableTitle || false; - let includeTypes: string[] = yaml?.includeTypes || plugin.settings.defaultOverview.includeTypes || ['folder', 'markdown']; - includeTypes = includeTypes.map((type) => type.toLowerCase()); - const style: 'list' | 'grid' = yaml?.style || 'list'; - const disableFileTag = yaml?.disableFileTag || plugin.settings.defaultOverview.disableFileTag || false; - const showEmptyFolders = yaml?.showEmptyFolders || plugin.settings.defaultOverview.showEmptyFolders || false; - const pathBlacklist: string[] = []; - - - el.parentElement?.classList.add('folder-overview-container'); - const root = el.createEl('div', { cls: 'folder-overview' }); - const titleEl = root.createEl('h1', { cls: 'folder-overview-title' }); - const ul = root.createEl('ul', { cls: 'folder-overview-list' }); - if (!disableTitle) { - titleEl.innerText = title; - } - if (includeTypes.length === 0) { return; } - let files: TAbstractFile[] = []; - const sourceFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); - if (!sourceFile) return; - const sourceFolderPath = plugin.getFolderPathFromString(ctx.sourcePath); - let sourceFolder: TFolder | undefined; - if (sourceFile.parent instanceof TFolder) { - sourceFolder = sourceFile.parent; - } else { - sourceFolder = plugin.app.vault.getAbstractFileByPath(plugin.getFolderPathFromString(ctx.sourcePath)) as TFolder; - } - - files = sourceFolder.children; - - files = files.filter((file) => { - const folderPath = plugin.getFolderPathFromString(file.path); - if (!folderPath.startsWith(sourceFolderPath)) { return false; } - const excludedFolder = getExcludedFolder(plugin, file.path); - if (excludedFolder?.excludeFromFolderOverview) { return false; } - if (file.path === ctx.sourcePath) { return false; } - if ((file.path.split('/').length - sourceFolderPath.split('/').length) - 1 < depth) { - return true; - } - }); - if (!includeTypes.includes('folder')) { - files = getAllFiles(files, sourceFolderPath, depth); - } - if (files.length === 0) { - // create button to edit the overview - const editButton = root.createEl('button', { cls: 'folder-overview-edit-button' }); - editButton.innerText = 'Edit overview'; - editButton.addEventListener('click', (e) => { - e.stopImmediatePropagation(); - e.preventDefault(); - e.stopPropagation(); - new FolderOverviewSettings(plugin.app, plugin, parseYaml(source), ctx, el).open(); - }, { capture: true }); - } - files = sortFiles(files, yaml, plugin); - - if (style === 'grid') { - const grid = root.createEl('div', { cls: 'folder-overview-grid' }); - files.forEach(async (file) => { - const gridItem = grid.createEl('div', { cls: 'folder-overview-grid-item' }); - const gridArticle = gridItem.createEl('article', { cls: 'folder-overview-grid-item-article' }); - if (file instanceof TFile) { - const fileContent = await plugin.app.vault.read(file); - // skip --- yaml - const descriptionEl = gridArticle.createEl('p', { cls: 'folder-overview-grid-item-description' }); - let description = fileContent.split('\n')[0]; - if (description.length > 64) { - description = description.slice(0, 64) + '...'; - } - descriptionEl.innerText = description; - const link = gridArticle.createEl('a', { cls: 'folder-overview-grid-item-link internal-link' }); - const title = link.createEl('h1', { cls: 'folder-overview-grid-item-link-title' }); - title.innerText = file.name.replace('.md', '').replace('.canvas', ''); - link.href = file.path; - } else if (file instanceof TFolder) { - const folderItem = gridArticle.createEl('div', { cls: 'folder-overview-grid-item-folder' }); - const folderName = folderItem.createEl('h1', { cls: 'folder-overview-grid-item-folder-name' }); - folderName.innerText = file.name; - } - }); - } else if (style === 'list') { - files.forEach((file) => { - if (file instanceof TFolder) { - const folderItem = addFolderList(plugin, ul, pathBlacklist, file); - goThroughFolders(plugin, folderItem, file, depth, sourceFolderPath, ctx, yaml, pathBlacklist, includeTypes, disableFileTag); - } else if (file instanceof TFile) { - addFileList(plugin, ul, pathBlacklist, file, includeTypes, disableFileTag); - } - }); - } else if (style === 'explorer') { - } else if (style === 'testing') { - cloneFileExplorerView(plugin, ctx, root, yaml, pathBlacklist); - } - if (includeTypes.length > 1 && style !== 'grid' && (!showEmptyFolders || yaml.onlyIncludeSubfolders)) { - removeEmptyFolders(ul, 1, yaml); - } -} -function cloneFileExplorerView(plugin: FolderNotesPlugin, ctx: MarkdownPostProcessorContext, root: HTMLElement, yaml: yamlSettings, pathBlacklist: string[]) { - const folder = plugin.getEL(plugin.getFolderPathFromString(ctx.sourcePath)) - const folderElement = folder?.parentElement; - console.log(folderElement) - if (!folderElement) return; - const newFolderElement = folderElement.cloneNode(true) as HTMLElement; - newFolderElement.querySelectorAll('div.tree-item-icon').forEach((el) => { - if (el instanceof HTMLElement) { - el.onclick = () => { - handleCollapseClick(el, plugin, yaml, pathBlacklist); - } - } - }); - root.appendChild(newFolderElement); -} -function handleCollapseClick(el: HTMLElement, plugin: FolderNotesPlugin, yaml: yamlSettings, pathBlacklist: string[]) { - el.classList.toggle('is-collapsed'); - if (el.classList.contains('is-collapsed')) { - console.log(el.parentElement?.parentElement); - console.log(el.parentElement?.parentElement?.childNodes[1]) - console.log(el.parentElement?.parentElement?.querySelector('tree-item-children')) - el.parentElement?.parentElement?.childNodes[1]?.remove(); - } else { - console.log(el.parentElement) - const path = el.parentElement?.getAttribute('data-path'); - if (!path) return; - const folder = plugin.app.vault.getAbstractFileByPath(path); - if (!(folder instanceof TFolder)) return; - const folderElement = el.parentElement?.parentElement; - if (!folderElement) return; - console.log('folderElement', folderElement); - const childrenElement = folderElement.createDiv({ cls: 'tree-item-children nav-folder-children' }); - let files = sortFiles(folder.children, {}, plugin); - files = filterFiles(files, plugin, path, yaml.depth || 1, pathBlacklist); - files.forEach((child) => { - if (child instanceof TFolder) { - const svg = ''; - const folderElement = childrenElement.createDiv({ - cls: 'tree-item nav-folder', - }); - const folderTitle = folderElement.createDiv({ - cls: 'tree-item-self is-clickable nav-folder-title', - attr: { - 'data-path': child.path, - 'draggable': 'true' - }, - }) - const collapseIcon = folderTitle.createDiv({ - cls: 'tree-item-icon collapse-icon nav-folder-collapse-indicator', - }); - collapseIcon.innerHTML = svg; - collapseIcon.onclick = () => { - handleCollapseClick(collapseIcon, plugin, yaml, pathBlacklist); - } - folderTitle.createDiv({ - cls: 'tree-item-inner nav-folder-title-content', - text: child.name, - }); - } else if (child instanceof TFile) { - const fileElement = childrenElement.createDiv({ - cls: 'tree-item nav-file', - }); - const fileTitle = fileElement.createDiv({ - cls: 'tree-item-self is-clickable nav-file-title', - attr: { - 'data-path': child.path, - 'draggable': 'true' - }, - }) - fileTitle.createDiv({ - cls: 'tree-item-inner nav-file-title-content', - text: child.basename, - }); - if (child.extension !== 'md') { - fileTitle.createDiv({ - cls: 'nav-file-tag', - text: child.extension - }); - } - } - }); - // move folders above files - } -} - - -function goThroughFolders(plugin: FolderNotesPlugin, list: HTMLLIElement | HTMLUListElement, folder: TFolder, - depth: number, sourceFolderPath: string, ctx: MarkdownPostProcessorContext, yaml: yamlSettings, pathBlacklist: string[], includeTypes: string[], disableFileTag: boolean) { - if (sourceFolderPath === '') { - depth--; - } - let files = filterFiles(folder.children, plugin, sourceFolderPath, depth, pathBlacklist); - files = sortFiles(files, yaml, plugin); - const ul = list.createEl('ul', { cls: 'folder-overview-list' }); - files.forEach((file) => { - if (file instanceof TFolder) { - const folderItem = addFolderList(plugin, ul, pathBlacklist, file); - goThroughFolders(plugin, folderItem, file, depth, sourceFolderPath, ctx, yaml, pathBlacklist, includeTypes, disableFileTag); - } else if (file instanceof TFile) { - addFileList(plugin, ul, pathBlacklist, file, includeTypes, disableFileTag); - } - }); -} - -function filterFiles(files: TAbstractFile[], plugin: FolderNotesPlugin, sourceFolderPath: string, depth: number, pathBlacklist: string[]) { - return files.filter((file) => { - console.log('pathBlacklist', pathBlacklist); - if (pathBlacklist.includes(file.path)) { return false; } - const folderPath = plugin.getFolderPathFromString(file.path); - if (!folderPath.startsWith(sourceFolderPath)) { return false; } - const excludedFolder = getExcludedFolder(plugin, file.path); - if (excludedFolder?.excludeFromFolderOverview) { return false; } - if ((file.path.split('/').length - sourceFolderPath.split('/').length) - 1 < depth) { - return true; - } - }); -} - -function sortFiles(files: TAbstractFile[], yaml: yamlSettings, plugin: FolderNotesPlugin) { - if (!yaml?.sortBy) { - yaml.sortBy = plugin.settings.defaultOverview.sortBy || 'name'; - } - return files.sort((a, b) => { - // sort by folder first - if (a instanceof TFolder && !(b instanceof TFolder)) { - return -1; - } - if (!(a instanceof TFolder) && b instanceof TFolder) { - return 1; - } - if (!(a instanceof TFile) || !(b instanceof TFile)) { return -1; } - if (yaml.sortBy === 'created') { - if (a.stat.ctime > b.stat.ctime) { - return -1; - } else if (a.stat.ctime < b.stat.ctime) { - return 1; - } - } else if (yaml.sortBy === 'createdAsc') { - if (a.stat.ctime < b.stat.ctime) { - return -1; - } else if (a.stat.ctime > b.stat.ctime) { - return 1; - } - } else if (yaml.sortBy === 'modified') { - if (a.stat.mtime > b.stat.mtime) { - return -1; - } else if (a.stat.mtime < b.stat.mtime) { - return 1; - } - } else if (yaml.sortBy === 'modifiedAsc') { - if (a.stat.mtime < b.stat.mtime) { - return -1; - } else if (a.stat.mtime > b.stat.mtime) { - return 1; - } - } - // sort by name - if (a.name < b.name && yaml.sortBy === 'name') { - return -1; - } else if (a.name > b.name && yaml.sortBy === 'nameAsc') { - return -1; - } - - if (a.name > b.name && yaml.sortBy === 'name') { - return 1; - } else if (a.name < b.name && yaml.sortBy === 'nameAsc') { - return 1; - } - return 0; - }); -} - -function removeEmptyFolders(ul: HTMLUListElement | HTMLLIElement, depth: number, yaml: yamlSettings) { - const childrensToRemove: ChildNode[] = []; - ul.childNodes.forEach((el) => { - const childrens = (el as Element).querySelector('ul'); - if (!childrens || childrens === null) { return; } - if (childrens && !childrens?.hasChildNodes() && !(el instanceof HTMLUListElement)) { - childrensToRemove.push(el); - } else if (el instanceof HTMLUListElement || el instanceof HTMLLIElement) { - removeEmptyFolders(el, depth + 1, yaml); - } - }); - childrensToRemove.forEach((el) => { - if (yaml.onlyIncludeSubfolders && depth === 1) { return; } - el.remove(); - }); -} - -function addFolderList(plugin: FolderNotesPlugin, list: HTMLUListElement | HTMLLIElement, pathBlacklist: string[], folder: TFolder) { - const folderItem = list.createEl('li', { cls: 'folder-overview-list folder-list' }); - const folderNote = getFolderNote(plugin, folder.path); - if (folderNote instanceof TFile) { - const folderNoteLink = folderItem.createEl('a', { cls: 'folder-overview-list-item folder-name-item internal-link', href: folderNote.path }); - folderNoteLink.innerText = folder.name; - pathBlacklist.push(folderNote.path); - } else { - const folderName = folderItem.createEl('span', { cls: 'folder-overview-list-item folder-name-item' }); - folderName.innerText = folder.name; - } - return folderItem; -} - -function addFileList(plugin: FolderNotesPlugin, list: HTMLUListElement | HTMLLIElement, pathBlacklist: string[], file: TFile, includeTypes: string[], disableFileTag: boolean) { - if (includeTypes.length > 0 && !includeTypes.includes('all')) { - if (file.extension === 'md' && !includeTypes.includes('markdown')) return; - if (file.extension === 'canvas' && !includeTypes.includes('canvas')) return; - if (file.extension === 'pdf' && !includeTypes.includes('pdf')) return; - const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp']; - if (imageTypes.includes(file.extension) && !includeTypes.includes('image')) return; - const videoTypes = ['mp4', 'webm', 'ogv', 'mov', 'mkv']; - if (videoTypes.includes(file.extension) && !includeTypes.includes('video')) return; - const audioTypes = ['mp3', 'wav', 'm4a', '3gp', 'flac', 'ogg', 'oga', 'opus']; - if (audioTypes.includes(file.extension) && includeTypes.includes('audio')) return; - const allTypes = ['md', 'canvas', 'pdf', ...imageTypes, ...videoTypes, ...audioTypes]; - if (!allTypes.includes(file.extension) && !includeTypes.includes('other')) return; - } - if (pathBlacklist.includes(file.path)) return; - const listItem = list.createEl('li', { cls: 'folder-overview-list file-link' }); - const nameItem = listItem.createEl('div', { cls: 'folder-overview-list-item' }); - const link = nameItem.createEl('a', { cls: 'internal-link', href: file.path }); - link.innerText = file.basename; - if (file.extension !== 'md' && !disableFileTag) { - nameItem.createDiv({ cls: 'nav-file-tag' }).innerText = file.extension; - } -} - -function getAllFiles(files: TAbstractFile[], sourceFolderPath: string, depth: number) { - const allFiles: TAbstractFile[] = []; - files.forEach((file) => { - if (file instanceof TFolder) { - if ((file.path.split('/').length - sourceFolderPath.split('/').length) - 1 < depth - 1) { - allFiles.push(...getAllFiles(file.children, sourceFolderPath, depth)); - } - } else { - allFiles.push(file); - } - }); - return allFiles; -} \ No newline at end of file diff --git a/src/folderOverviewClass.ts b/src/folderOverview/FolderOverview.ts similarity index 73% rename from src/folderOverviewClass.ts rename to src/folderOverview/FolderOverview.ts index d7211d3..c5d8854 100644 --- a/src/folderOverviewClass.ts +++ b/src/folderOverview/FolderOverview.ts @@ -1,19 +1,22 @@ -import { MarkdownPostProcessorContext, parseYaml, TAbstractFile, TFolder, TFile } from 'obsidian'; -import { extractFolderName, getFolderNote } from './functions/folderNoteFunctions'; -import FolderNotesPlugin from './main'; -import { FolderOverviewSettings } from './modals/folderOverview'; -import { getExcludedFolder } from './excludedFolder'; +import { MarkdownPostProcessorContext, parseYaml, TAbstractFile, TFolder, TFile, stringifyYaml, Notice } from 'obsidian'; +import { extractFolderName, getFolderNote } from '../functions/folderNoteFunctions'; +import FolderNotesPlugin from '../main'; +import { FolderOverviewSettings } from './modalSettings'; +import { getExcludedFolder } from '../excludedFolder'; +export type includeTypes = 'folder' | 'markdown' | 'canvas' | 'other' | 'pdf' | 'images' | 'audio' | 'video' | 'all'; export type yamlSettings = { + folderPath: string; title: string; disableTitle: boolean; depth: number; - type: 'folder' | 'markdown' | 'canvas' | 'other' | 'pdf' | 'images' | 'audio' | 'video' | 'all'; - includeTypes: string[]; - style: 'list' | 'grid'; + includeTypes: includeTypes[]; + style: 'list' | 'grid' | 'explorer'; disableFileTag: boolean; - sortBy: 'name' | 'created' | 'modified' | 'nameAsc' | 'createdAsc' | 'modifiedAsc'; + sortBy: 'name' | 'created' | 'modified'; + sortByAsc: boolean; showEmptyFolders: boolean; onlyIncludeSubfolders: boolean; + storeFolderCondition: boolean; }; export class FolderOverview { yaml: yamlSettings; @@ -26,68 +29,77 @@ export class FolderOverview { folders: TFolder[] = []; constructor(plugin: FolderNotesPlugin, ctx: MarkdownPostProcessorContext, source: string, el: HTMLElement) { let yaml: yamlSettings = parseYaml(source); + if (!yaml) { yaml = {} as yamlSettings; } const includeTypes = yaml?.includeTypes || plugin.settings.defaultOverview.includeTypes || ['folder', 'markdown']; this.plugin = plugin; this.ctx = ctx; this.source = source; this.el = el; this.yaml = { - title: yaml?.title || plugin.settings.defaultOverview.title || '{{folderName}} overview', - disableTitle: yaml?.disableTitle || plugin.settings.defaultOverview.disableTitle || false, - depth: yaml?.depth || plugin.settings.defaultOverview.depth || 1, + folderPath: yaml?.folderPath || plugin.getFolderPathFromString(ctx.sourcePath), + title: yaml?.title || plugin.settings.defaultOverview.title, + disableTitle: yaml?.disableTitle || plugin.settings.defaultOverview.disableTitle, + depth: yaml?.depth || plugin.settings.defaultOverview.depth, style: yaml?.style || 'list', - includeTypes: includeTypes.map((type) => type.toLowerCase()), - disableFileTag: yaml?.disableFileTag || plugin.settings.defaultOverview.disableFileTag || false, - sortBy: yaml?.sortBy || plugin.settings.defaultOverview.sortBy || 'name', - showEmptyFolders: yaml?.showEmptyFolders || plugin.settings.defaultOverview.showEmptyFolders || false, - onlyIncludeSubfolders: yaml?.onlyIncludeSubfolders || plugin.settings.defaultOverview.onlyIncludeSubfolders || false, - type: yaml?.type || plugin.settings.defaultOverview.type || 'folder', + includeTypes: includeTypes.map((type) => type.toLowerCase()) as includeTypes[], + disableFileTag: yaml?.disableFileTag || plugin.settings.defaultOverview.disableFileTag, + sortBy: yaml?.sortBy || plugin.settings.defaultOverview.sortBy, + sortByAsc: yaml?.sortByAsc || plugin.settings.defaultOverview.sortByAsc, + showEmptyFolders: yaml?.showEmptyFolders || plugin.settings.defaultOverview.showEmptyFolders, + onlyIncludeSubfolders: yaml?.onlyIncludeSubfolders || plugin.settings.defaultOverview.onlyIncludeSubfolders, + storeFolderCondition: yaml?.storeFolderCondition || plugin.settings.defaultOverview.storeFolderCondition } } create(plugin: FolderNotesPlugin, source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { + el.empty(); el.parentElement?.classList.add('folder-overview-container'); const root = el.createEl('div', { cls: 'folder-overview' }); const titleEl = root.createEl('h1', { cls: 'folder-overview-title' }); const ul = root.createEl('ul', { cls: 'folder-overview-list' }); - if (!this.yaml.disableTitle) { - titleEl.innerText = this.yaml.title || ''; - } if (this.yaml.includeTypes.length === 0) { return; } let files: TAbstractFile[] = []; const sourceFile = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); if (!sourceFile) return; - const sourceFolderPath = plugin.getFolderPathFromString(ctx.sourcePath); + const sourceFolderPath = this.yaml.folderPath || plugin.getFolderPathFromString(ctx.sourcePath); let sourceFolder: TFolder | undefined; - if (sourceFile.parent instanceof TFolder) { - sourceFolder = sourceFile.parent; - } else { - sourceFolder = plugin.app.vault.getAbstractFileByPath(plugin.getFolderPathFromString(ctx.sourcePath)) as TFolder; + if (sourceFolderPath !== '') { + sourceFolder = plugin.app.vault.getAbstractFileByPath(this.yaml.folderPath) as TFolder; } - - files = sourceFolder.children; - - files = files.filter((file) => { - const folderPath = plugin.getFolderPathFromString(file.path); - if (!folderPath.startsWith(sourceFolderPath)) { return false; } - const excludedFolder = getExcludedFolder(plugin, file.path); - if (excludedFolder?.excludeFromFolderOverview) { return false; } - if (file.path === ctx.sourcePath) { return false; } - if ((file.path.split('/').length - sourceFolderPath.split('/').length) - 1 < this.yaml.depth) { - return true; + if (!this.yaml.disableTitle) { + if (sourceFolder && sourceFolderPath !== '') { + titleEl.innerText = this.yaml.title.replace('{{folderName}}', sourceFolder.name); + } else if (sourceFolderPath == '' ) { + titleEl.innerText = this.yaml.title.replace('{{folderName}}', 'Vault'); + } else { + titleEl.innerText = this.yaml.title.replace('{{folderName}}', ''); } - }); + } + if (!sourceFolder && sourceFolderPath !== '') { return new Notice('Couldn\'t find the folder'); } + if (sourceFolderPath == '') { + const rootFolders: TAbstractFile[] = []; + plugin.app.vault.getAllLoadedFiles().filter(f => f instanceof TFolder).forEach((file) => { + if (file instanceof TFolder && !file.path.includes('/')) { + rootFolders.push(file); + } + }); + files = rootFolders; + } else if (sourceFolder) { + files = sourceFolder.children; + } + console.log(files); + files = this.filterFiles(files, plugin, sourceFolderPath, this.yaml.depth, this.pathBlacklist); if (!this.yaml.includeTypes.includes('folder')) { files = this.getAllFiles(files, sourceFolderPath, this.yaml.depth); } if (files.length === 0) { - // create button to edit the overview const editButton = root.createEl('button', { cls: 'folder-overview-edit-button' }); editButton.innerText = 'Edit overview'; editButton.addEventListener('click', (e) => { e.stopImmediatePropagation(); e.preventDefault(); e.stopPropagation(); - new FolderOverviewSettings(plugin.app, plugin, parseYaml(source), ctx, el).open(); + console.log(this) + new FolderOverviewSettings(plugin.app, plugin, this.yaml, ctx, el).open(); }, { capture: true }); } files = this.sortFiles(files, this.yaml, plugin); @@ -126,28 +138,42 @@ export class FolderOverview { } }); } else if (this.yaml.style === 'explorer') { - } else if (this.yaml.style === 'testing') { - console.log('testing'); - this.cloneFileExplorerView(plugin, ctx, root, this.yaml, this.pathBlacklist); + if (this.plugin.app.workspace.layoutReady) { + this.cloneFileExplorerView(plugin, ctx, root, this.yaml, this.pathBlacklist); + } else { + this.plugin.app.workspace.onLayoutReady(() => { + this.cloneFileExplorerView(plugin, ctx, root, this.yaml, this.pathBlacklist); + }); + } } - if (this.yaml.includeTypes.length > 1 && this.yaml.style !== 'grid' && (!this.yaml.showEmptyFolders || this.yaml.onlyIncludeSubfolders)) { + if (this.yaml.style !== 'list') { return; } + if (this.yaml.includeTypes.length > 1 && (!this.yaml.showEmptyFolders || this.yaml.onlyIncludeSubfolders)) { this.removeEmptyFolders(ul, 1, this.yaml); } } cloneFileExplorerView(plugin: FolderNotesPlugin, ctx: MarkdownPostProcessorContext, root: HTMLElement, yaml: yamlSettings, pathBlacklist: string[]) { - const folder = plugin.getEL(plugin.getFolderPathFromString(ctx.sourcePath)) - const folderElement = folder?.parentElement; - const tFolder = plugin.app.vault.getAbstractFileByPath(plugin.getFolderPathFromString(ctx.sourcePath)); - console.log(folderElement) + const folder = plugin.getEL(this.yaml.folderPath) + let folderElement = folder?.parentElement; + const tFolder = plugin.app.vault.getAbstractFileByPath(this.yaml.folderPath); + if (!folderElement && yaml.folderPath.trim() !== '') return; + folderElement = document.querySelector('div.nav-files-container') as HTMLElement; if (!folderElement) return; const newFolderElement = folderElement.cloneNode(true) as HTMLElement; newFolderElement.querySelectorAll('div.nav-folder-title ').forEach((el) => { const folder = plugin.app.vault.getAbstractFileByPath(el.getAttribute('data-path') || ''); if (!(folder instanceof TFolder)) return; - if (el.parentElement?.classList.contains('is-collapsed')) { - folder.collapsed = true; + if (this.yaml.storeFolderCondition) { + if (folder.collapsed) { + el.classList.add('is-collapsed'); + } else { + el.classList.remove('is-collapsed'); + } } else { - folder.collapsed = false; + if (el.parentElement?.classList.contains('is-collapsed')) { + folder.collapsed = true; + } else { + folder.collapsed = false; + } } if (el.classList.contains('has-folder-note')) { const folderNote = getFolderNote(plugin, folder.path); @@ -156,6 +182,14 @@ export class FolderOverview { }); if (tFolder instanceof TFolder) { this.addFiles(tFolder.children, root); + } else if (yaml.folderPath.trim() === '') { + const rootFolders: TAbstractFile[] = []; + plugin.app.vault.getAllLoadedFiles().filter(f => f instanceof TFolder).forEach((file) => { + if (file instanceof TFolder && !file.path.includes('/')) { + rootFolders.push(file); + } + }); + this.addFiles(rootFolders, root); } newFolderElement.querySelectorAll('div.tree-item-icon').forEach((el) => { if (el instanceof HTMLElement) { @@ -170,10 +204,8 @@ export class FolderOverview { } async addFiles(files: TAbstractFile[], childrenElement: HTMLElement) { - const folders = files.filter((file) => file instanceof TFolder); - const filesWithoutFolders = files.filter((file) => !(file instanceof TFolder)); - console.log(folders); - console.log(filesWithoutFolders); + const folders = files.filter((file) => file instanceof TFolder).sort(); + const filesWithoutFolders = this.sortFiles(files.filter((file) => !(file instanceof TFolder)), this.yaml, this.plugin); for (const child of folders) { if (child instanceof TFolder) { const folderNote = getFolderNote(this.plugin, child.path); @@ -194,8 +226,6 @@ export class FolderOverview { if (!child.collapsed) { folderTitle.classList.remove('is-collapsed'); const childrenElement = folderElement?.createDiv({ cls: 'tree-item-children nav-folder-children' }); - console.log(folderElement); - console.log(child.children); this.addFiles(child.children, childrenElement); } else { folderTitle.classList.add('is-collapsed'); @@ -221,7 +251,7 @@ export class FolderOverview { if (child instanceof TFile) { if (this.pathBlacklist.includes(child.path)) { continue; } const extension = child.extension.toLowerCase() == 'md' ? 'markdown' : child.extension.toLowerCase(); - if (!this.yaml.includeTypes.includes(extension)) { continue; } + if (!this.yaml.includeTypes.includes(extension as includeTypes)) { continue; } const fileElement = childrenElement.createDiv({ cls: 'tree-item nav-file', }); @@ -290,7 +320,7 @@ export class FolderOverview { return files.filter((file) => { if (pathBlacklist.includes(file.path)) { return false; } const folderPath = plugin.getFolderPathFromString(file.path); - if (!folderPath.startsWith(sourceFolderPath)) { return false; } + if (!folderPath.startsWith(sourceFolderPath) && sourceFolderPath !== '') { return false; } const excludedFolder = getExcludedFolder(plugin, file.path); if (excludedFolder?.excludeFromFolderOverview) { return false; } if ((file.path.split('/').length - sourceFolderPath.split('/').length) - 1 < depth) { @@ -304,7 +334,6 @@ export class FolderOverview { yaml.sortBy = plugin.settings.defaultOverview.sortBy || 'name'; } return files.sort((a, b) => { - // sort by folder first if (a instanceof TFolder && !(b instanceof TFolder)) { return -1; } @@ -312,13 +341,48 @@ export class FolderOverview { return 1; } if ((a instanceof TFolder) && (b instanceof TFolder)) { - if (yaml.sortBy === 'name') { + if (a.name > b.name) { + return -1; + } else if (a.name < b.name) { + return 1; + } + } + if (!(a instanceof TFile) || !(b instanceof TFile)) { return -1; } + if (yaml.sortBy === 'created') { + if (yaml.sortByAsc) { + if (a.stat.ctime < b.stat.ctime) { + return -1; + } else if (a.stat.ctime > b.stat.ctime) { + return 1; + } + } + if (a.stat.ctime > b.stat.ctime) { + return -1; + } else if (a.stat.ctime < b.stat.ctime) { + return 1; + } + } else if (yaml.sortBy === 'modified') { + if (yaml.sortByAsc) { + if (a.stat.mtime < b.stat.mtime) { + return -1; + } else if (a.stat.mtime > b.stat.mtime) { + return 1; + } + } + if (a.stat.mtime > b.stat.mtime) { + return -1; + } else if (a.stat.mtime < b.stat.mtime) { + return 1; + } + } else if (yaml.sortBy === 'name') { + if (yaml.sortByAsc) { if (a.name < b.name) { + return -1; } else if (a.name > b.name) { return 1; } - } else if (yaml.sortBy === 'nameAsc') { + } else { if (a.name > b.name) { return -1; } else if (a.name < b.name) { @@ -326,44 +390,6 @@ export class FolderOverview { } } } - if (!(a instanceof TFile) || !(b instanceof TFile)) { return -1; } - if (yaml.sortBy === 'created') { - if (a.stat.ctime > b.stat.ctime) { - return -1; - } else if (a.stat.ctime < b.stat.ctime) { - return 1; - } - } else if (yaml.sortBy === 'createdAsc') { - if (a.stat.ctime < b.stat.ctime) { - return -1; - } else if (a.stat.ctime > b.stat.ctime) { - return 1; - } - } else if (yaml.sortBy === 'modified') { - if (a.stat.mtime > b.stat.mtime) { - return -1; - } else if (a.stat.mtime < b.stat.mtime) { - return 1; - } - } else if (yaml.sortBy === 'modifiedAsc') { - if (a.stat.mtime < b.stat.mtime) { - return -1; - } else if (a.stat.mtime > b.stat.mtime) { - return 1; - } - } - // sort by name - if (a.name < b.name && yaml.sortBy === 'name') { - return -1; - } else if (a.name > b.name && yaml.sortBy === 'nameAsc') { - return -1; - } - - if (a.name > b.name && yaml.sortBy === 'name') { - return 1; - } else if (a.name < b.name && yaml.sortBy === 'nameAsc') { - return 1; - } return 0; }); } @@ -436,4 +462,43 @@ export class FolderOverview { }); return allFiles; } +} + +export async function updateYaml(plugin: FolderNotesPlugin, ctx: MarkdownPostProcessorContext, el: HTMLElement, yaml: yamlSettings) { + console.log('update yaml'); + const file = plugin.app.vault.getAbstractFileByPath(ctx.sourcePath); + if (!(file instanceof TFile)) return; + let stringYaml = stringifyYaml(yaml); + await plugin.app.vault.process(file, (text) => { + const info = ctx.getSectionInfo(el); + // check if stringYaml ends with a newline + if (stringYaml[stringYaml.length - 1] !== '\n') { + stringYaml += '\n'; + } + if (info) { + const { lineStart } = info; + console.log(text); + const lineEnd = getCodeBlockEndLine(text, lineStart); + console.log(lineStart, lineEnd); + if (lineEnd === -1 || !lineEnd) return text; + const lineLength = lineEnd - lineStart; + const lines = text.split('\n'); + lines.splice(lineStart, lineLength + 1, `\`\`\`folder-overview\n${stringYaml}\`\`\``); + return lines.join('\n'); + } + return `\`\`\`folder-overview\n${stringYaml}\`\`\``; + }); +} +export function getCodeBlockEndLine(text: string, startLine: number, count = 1) { + let line = startLine + 1; + const lines = text.split('\n'); + while (line < lines.length) { + if (count > 20) { return -1; } + if (lines[line].startsWith('```')) { + return line; + } + line++; + count++; + } + return line; } \ No newline at end of file diff --git a/src/folderOverview/modalSettings.ts b/src/folderOverview/modalSettings.ts new file mode 100644 index 0000000..7531317 --- /dev/null +++ b/src/folderOverview/modalSettings.ts @@ -0,0 +1,295 @@ +import { App, Modal, Setting, MarkdownPostProcessorContext, stringifyYaml, TFile, TFolder } from 'obsidian'; +import { yamlSettings, includeTypes, FolderOverview } from 'src/folderOverview/FolderOverview'; +import FolderNotesPlugin from '../main'; +import ListComponent from 'src/functions/ListComponent'; +import { updateYaml } from 'src/folderOverview/FolderOverview'; +import { FolderSuggest } from 'src/suggesters/FolderSuggester'; +export class FolderOverviewSettings extends Modal { + plugin: FolderNotesPlugin; + app: App; + yaml: yamlSettings; + ctx: MarkdownPostProcessorContext; + el: HTMLElement; + defaultSettings: boolean; + constructor(app: App, plugin: FolderNotesPlugin, yaml: yamlSettings, ctx: MarkdownPostProcessorContext | null, el: HTMLElement | null, defaultSettings?: boolean) { + super(app); + this.plugin = plugin; + this.app = app; + if (!yaml) { + this.yaml = this.plugin.settings.defaultOverview; + } else { + this.yaml = yaml; + } + if (ctx) { + this.ctx = ctx; + } + if (el) { + this.el = el; + } + if (defaultSettings) { + this.yaml = this.plugin.settings.defaultOverview; + this.defaultSettings = true; + } + } + onOpen() { + this.display(); + } + display() { + const { contentEl } = this; + contentEl.empty(); + // close when user presses enter + contentEl.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + this.close(); + } + }); + console.log(this.yaml); + contentEl.createEl('h2', { text: 'Folder overview settings' }); + new Setting(contentEl) + .setName('Disable the title') + .setDesc('Choose if the title should be shown') + .addToggle((toggle) => + toggle + .setValue(this.yaml.disableTitle || false) + .onChange(async (value) => { + console.log(this.yaml) + this.yaml.disableTitle = value; + this.display(); + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml);; + }) + ); + if (!this.yaml.disableTitle) { + new Setting(contentEl) + .setName('Title') + .setDesc('Choose the title of the folder overview') + .addText((text) => + text + .setValue(this.yaml?.title || '{{folderName}} overview') + .onChange(async (value) => { + this.yaml.title = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml);; + }) + ); + } + new Setting(contentEl) + .setName('Folder path for the overview') + .setDesc('Choose the folder path for the overview') + .addSearch((search) => { + new FolderSuggest(search.inputEl, this.plugin) + search + .setPlaceholder('Folder path') + .setValue(this.yaml?.folderPath || '') + .onChange(async (value) => { + if (value === '' || value === '/') { + this.yaml.folderPath = ''; + await updateYaml(this.plugin, this.ctx, this.el, this.yaml);; + return; + } + if (!(this.app.vault.getAbstractFileByPath(value) instanceof TFolder)) return; + this.yaml.folderPath = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml);; + }); + }); + new Setting(contentEl) + .setName('Overview style') + .setDesc('Choose the style of the overview (grid style soon)') + .addDropdown((dropdown) => + dropdown + .addOption('list', 'List') + .addOption('explorer', 'Explorer') + .setValue(this.yaml?.style || 'list') + .onChange(async (value: 'list') => { + this.yaml.style = value; + this.display(); + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }) + ); + if (this.yaml.style === 'explorer') { + new Setting(contentEl) + .setName('Store collapsed condition') + .setDesc('Choose if the collapsed condition should be stored stored until you restart Obsidian') + .addToggle((toggle) => + toggle + .setValue(this.yaml.storeFolderCondition || this.plugin.settings.defaultOverview.storeFolderCondition || true) + .onChange(async (value) => { + this.yaml.storeFolderCondition = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml);; + }) + ); + } + const setting = new Setting(contentEl); + setting.setName('Include types'); + const list = setting.createList((list: ListComponent) => + list + .addModal(this) + .setValues(this.yaml?.includeTypes || this.plugin.settings.defaultOverview.includeTypes || []) + .addResetButton() + ); + if ((this.yaml?.includeTypes?.length || 0) < 8 && !this.yaml.includeTypes?.includes('all')) { + setting.addDropdown((dropdown) => { + if (!this.yaml.includeTypes) this.yaml.includeTypes = this.plugin.settings.defaultOverview.includeTypes || []; + this.yaml.includeTypes = this.yaml.includeTypes.map((type: string) => type.toLowerCase()) as includeTypes[]; + const options = [ + { value: 'markdown', label: 'Markdown' }, + { value: 'folder', label: 'Folder' }, + { value: 'canvas', label: 'Canvas' }, + { value: 'pdf', label: 'PDF' }, + { value: 'image', label: 'Image' }, + { value: 'audio', label: 'Audio' }, + { value: 'video', label: 'Video' }, + { value: 'other', label: 'All other file types' }, + { value: 'all', label: 'All file types' }, + ]; + + options.forEach((option) => { + if (!this.yaml.includeTypes?.includes(option.value as includeTypes)) { + dropdown.addOption(option.value, option.label); + } + }); + dropdown.addOption('+', '+'); + dropdown.setValue('+'); + dropdown.onChange(async (value) => { + if (value === 'all') { + this.yaml.includeTypes = this.yaml.includeTypes?.filter((type: string) => type === 'folder'); + // @ts-ignore + list.setValues(this.yaml.includeTypes); + } + // @ts-ignore + await list.addValue(value.toLowerCase()); + this.display(); + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }); + }); + } + let disableFileTag; + this.yaml.includeTypes?.forEach((type: string) => { + type === 'folder' || type === 'markdown' ? (disableFileTag = true) : null; + }); + if (disableFileTag) { + new Setting(contentEl) + .setName('Disable file tag') + .setDesc('Choose if the file tag should be shown after the file name') + .addToggle((toggle) => { + toggle + .setValue(this.yaml.disableFileTag || this.plugin.settings.defaultOverview.disableFileTag || false) + .onChange(async (value) => { + this.yaml.disableFileTag = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }); + }); + } + if (this.yaml.style !== 'explorer') { + new Setting(contentEl) + .setName('File depth') + .setDesc('File & folder = +1 depth') + .addSlider((slider) => + slider + .setValue(this.yaml?.depth || 2) + .setLimits(1, 10, 1) + .onChange(async (value) => { + this.yaml.depth = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }) + ); + } + + new Setting(contentEl) + .setName('Sort files by') + .setDesc('Choose how the files should be sorted') + .addDropdown((dropdown) => + dropdown + .addOption('name', 'Name') + .addOption('created', 'Created') + .addOption('modified', 'Modified') + .setValue(this.yaml?.sortBy || 'name') + .onChange(async (value: 'name' | 'created' | 'modified') => { + this.yaml.sortBy = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }) + ) + .addDropdown((dropdown) => { + dropdown + .addOption('desc', 'Descending') + .addOption('asc', 'Ascending') + if (this.yaml.sortByAsc) { + dropdown.setValue('asc'); + } else { + dropdown.setValue('desc'); + } + dropdown.onChange(async (value) => { + this.yaml.sortByAsc = value === 'asc'; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }); + }); + if (this.yaml.style === 'list') { + new Setting(contentEl) + .setName('Show folder names of folders that appear empty in the folder overview') + .setDesc('Show the names of folders that appear to have no files/folders in the folder overview. That\'s mostly the case when you set the file depth to 1.') + .addToggle((toggle) => { + toggle + .setValue(this.yaml.showEmptyFolders || this.plugin.settings.defaultOverview.showEmptyFolders || false) + .onChange(async (value) => { + this.yaml.showEmptyFolders = value; + this.yaml.onlyIncludeSubfolders = false; + this.display(); + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }); + }); + + if (this.yaml.showEmptyFolders) { + new Setting(contentEl) + .setName('Only show first empty subfolders of current folder') + .addToggle((toggle) => { + toggle + .setValue(this.yaml.onlyIncludeSubfolders || this.plugin.settings.defaultOverview.onlyIncludeSubfolders || false) + .onChange(async (value) => { + this.yaml.onlyIncludeSubfolders = value; + if (this.defaultSettings) { + return this.plugin.saveSettings(); + } + await updateYaml(this.plugin, this.ctx, this.el, this.yaml); + }); + }); + } + } + + } + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} + diff --git a/src/functions/ListComponent.ts b/src/functions/ListComponent.ts index 07799a0..602087f 100644 --- a/src/functions/ListComponent.ts +++ b/src/functions/ListComponent.ts @@ -1,5 +1,7 @@ import { Setting } from 'obsidian'; -import { FolderOverviewSettings } from '../modals/folderOverview'; +import { FolderOverviewSettings } from '../folderOverview/modalSettings'; +import { includeTypes } from 'src/folderOverview/FolderOverview'; +import { updateYaml } from 'src/folderOverview/FolderOverview'; export default class ListComponent { containerEl: HTMLElement; controlEl: HTMLElement; @@ -20,13 +22,17 @@ export default class ListComponent { setValues(values: string[]) { this.listEl.empty(); this.values = values; - this.modal.yaml.includeTypes = values; + this.modal.yaml.includeTypes = values as includeTypes[]; if (values.length !== 0) { values.forEach((value) => { this.addElement(value); }); } - this.modal.updateYaml(); + if (this.modal.defaultSettings) { + this.modal.plugin.saveSettings(); + return this; + } + updateYaml(this.modal.plugin, this.modal.ctx, this.modal.el, this.modal.yaml); return this; } addElement(value: string) { @@ -46,7 +52,7 @@ export default class ListComponent { async addValue(value: string) { this.values.push(value); this.addElement(value); - this.modal.yaml.includeTypes = this.values; + this.modal.yaml.includeTypes = this.values as includeTypes[]; return this; } addResetButton() { diff --git a/src/functions/folderNoteFunctions.ts b/src/functions/folderNoteFunctions.ts index 396d4a2..d744568 100644 --- a/src/functions/folderNoteFunctions.ts +++ b/src/functions/folderNoteFunctions.ts @@ -126,7 +126,6 @@ export function extractFolderName(template: string, changedFileName: string) { export function getFolderNote(plugin: FolderNotesPlugin, folderPath: string, storageLocation?: string, file?: TFile) { if (!folderPath) return null; - console.log(folderPath); const folder = { path: folderPath, name: plugin.getFolderNameFromPathString(folderPath), diff --git a/src/main.ts b/src/main.ts index 7d7b591..3fc3928 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { Plugin, TFile, TFolder, TAbstractFile, MarkdownPostProcessorContext, parseYaml } from 'obsidian'; +import { Plugin, TFile, TFolder, TAbstractFile, MarkdownPostProcessorContext, parseYaml, Notice } from 'obsidian'; import { DEFAULT_SETTINGS, FolderNotesSettings, SettingsTab } from './settings'; import { Commands } from './commands'; import { FileExplorerWorkspaceLeaf } from './globals'; @@ -7,9 +7,8 @@ import { handleFileRename, handleFolderRename } from './events/handleRename'; import { createFolderNote, extractFolderName, getFolderNote, getFolder } from './functions/folderNoteFunctions'; import { getExcludedFolder } from './excludedFolder'; import { FrontMatterTitlePluginHandler } from './events/frontMatterTitle'; -import { createOverview as createFolderOverview } from './folderOverview'; -import { FolderOverviewSettings } from './modals/folderOverview'; -import { FolderOverview } from './folderOverviewClass'; +import { FolderOverviewSettings } from './folderOverview/modalSettings'; +import { FolderOverview } from './folderOverview/FolderOverview'; import './functions/ListComponent'; export default class FolderNotesPlugin extends Plugin { observer: MutationObserver; @@ -160,25 +159,18 @@ export default class FolderNotesPlugin extends Plugin { if (!this.settings.syncDelete) { return; } this.app.vault.delete(folderNote); })); - this.registerMarkdownCodeBlockProcessor('folder-overview', (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { - const observer = new MutationObserver(() => { - const editButton = el.parentElement?.childNodes.item(1); - if (editButton) { - editButton.addEventListener('click', (e) => { - e.stopImmediatePropagation(); - e.preventDefault(); - e.stopPropagation(); - new FolderOverviewSettings(this.app, this, parseYaml(source), ctx, el).open(); - }, { capture: true }); - } + if (this.app.workspace.layoutReady) { + this.registerMarkdownCodeBlockProcessor('folder-overview', (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + this.handleOverviewBlock(source, el, ctx); }); - observer.observe(el, { - childList: true, - subtree: true, + } else { + this.app.workspace.onLayoutReady(() => { + console.log('layout ready'); + this.registerMarkdownCodeBlockProcessor('folder-overview', (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + this.handleOverviewBlock(source, el, ctx); + }); }); - const folderOverview = new FolderOverview(this, ctx, source, el); - folderOverview.create(this, parseYaml(source), el, ctx); - }); + } if (this.app.workspace.layoutReady) { this.loadFileClasses(); } else { @@ -186,6 +178,32 @@ export default class FolderNotesPlugin extends Plugin { } } + handleOverviewBlock(source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) { + console.log('handling overview block'); + const observer = new MutationObserver(() => { + const editButton = el.parentElement?.childNodes.item(1); + if (editButton) { + editButton.addEventListener('click', (e) => { + e.stopImmediatePropagation(); + e.preventDefault(); + e.stopPropagation(); + new FolderOverviewSettings(this.app, this, parseYaml(source), ctx, el).open(); + }, { capture: true }); + } + }); + observer.observe(el, { + childList: true, + subtree: true, + }); + try { + const folderOverview = new FolderOverview(this, ctx, source, el); + folderOverview.create(this, parseYaml(source), el, ctx); + } catch (e) { + new Notice('Error creating folder overview (folder notes plugin) - check console for more details'); + console.error(e); + } + } + getFileNameFromPathString(path: string): string { return path.substring(path.lastIndexOf('/' || '\\') >= 0 ? path.lastIndexOf('/' || '\\') + 1 : 0); } diff --git a/src/modals/folderOverview.ts b/src/modals/folderOverview.ts deleted file mode 100644 index c748b0a..0000000 --- a/src/modals/folderOverview.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { App, Modal, Setting, MarkdownPostProcessorContext, stringifyYaml, TFile } from 'obsidian'; -import { yamlSettings } from 'src/folderOverview'; -import FolderNotesPlugin from '../main'; -import ListComponent from 'src/functions/ListComponent'; -export class FolderOverviewSettings extends Modal { - plugin: FolderNotesPlugin; - app: App; - yaml: yamlSettings; - ctx: MarkdownPostProcessorContext; - el: HTMLElement; - constructor(app: App, plugin: FolderNotesPlugin, yaml: yamlSettings, ctx: MarkdownPostProcessorContext, el: HTMLElement) { - super(app); - this.plugin = plugin; - this.app = app; - this.yaml = yaml; - this.ctx = ctx; - this.el = el; - if (!this.yaml) { - this.yaml = { - title: this.plugin.settings.defaultOverview.title, - disableTitle: this.plugin.settings.defaultOverview.disableTitle, - depth: this.plugin.settings.defaultOverview.depth, - type: this.plugin.settings.defaultOverview.type, - includeTypes: this.plugin.settings.defaultOverview.includeTypes, - style: this.plugin.settings.defaultOverview.style, - disableFileTag: this.plugin.settings.defaultOverview.disableFileTag, - sortBy: this.plugin.settings.defaultOverview.sortBy, - showEmptyFolders: this.plugin.settings.defaultOverview.showEmptyFolders, - onlyIncludeSubfolders: this.plugin.settings.defaultOverview.onlyIncludeSubfolders, - }; - } - } - onOpen() { - this.display(); - } - display() { - const { contentEl } = this; - contentEl.empty(); - // close when user presses enter - contentEl.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - this.close(); - } - }); - contentEl.createEl('h2', { text: 'Folder overview settings' }); - new Setting(contentEl) - .setName('Disable the title') - .setDesc('Choose if the title should be shown') - .addToggle((toggle) => - toggle - .setValue(this.yaml.disableTitle || false) - .onChange(async (value) => { - this.yaml.disableTitle = value; - this.display(); - await this.updateYaml(); - }) - ); - if (!this.yaml.disableTitle) { - new Setting(contentEl) - .setName('Title') - .setDesc('Choose the title of the folder overview') - .addText((text) => - text - .setValue(this.yaml?.title || '{{folderName}} overview') - .onChange(async (value) => { - this.yaml.title = value; - await this.updateYaml(); - }) - ); - } - const setting = new Setting(contentEl); - setting.setName('Include types'); - const list = setting.createList((list: ListComponent) => - list - .addModal(this) - .setValues(this.yaml?.includeTypes || this.plugin.settings.defaultOverview.includeTypes || []) - .addResetButton() - ); - if ((this.yaml?.includeTypes?.length || 0) < 8 && !this.yaml.includeTypes?.includes('all')) { - setting.addDropdown((dropdown) => { - if (!this.yaml.includeTypes) this.yaml.includeTypes = this.plugin.settings.defaultOverview.includeTypes || []; - this.yaml.includeTypes = this.yaml.includeTypes.map((type: string) => type.toLowerCase()); - const options = [ - { value: 'markdown', label: 'Markdown' }, - { value: 'folder', label: 'Folder' }, - { value: 'canvas', label: 'Canvas' }, - { value: 'pdf', label: 'PDF' }, - { value: 'image', label: 'Image' }, - { value: 'audio', label: 'Audio' }, - { value: 'video', label: 'Video' }, - { value: 'other', label: 'All other file types' }, - { value: 'all', label: 'All file types' }, - ]; - - options.forEach((option) => { - if (!this.yaml.includeTypes?.includes(option.value)) { - dropdown.addOption(option.value, option.label); - } - }); - dropdown.addOption('+', '+'); - dropdown.setValue('+'); - dropdown.onChange(async (value) => { - if (value === 'all') { - this.yaml.includeTypes = this.yaml.includeTypes?.filter((type: string) => type === 'folder'); - // @ts-ignore - list.setValues(this.yaml.includeTypes); - } - // @ts-ignore - await list.addValue(value.toLowerCase()); - await this.updateYaml(); - this.display(); - }); - }); - } - let disableFileTag; - this.yaml.includeTypes?.forEach((type: string) => { - type === 'folder' || type === 'markdown' ? (disableFileTag = true) : null; - }); - if (disableFileTag) { - new Setting(contentEl) - .setName('Disable file tag') - .setDesc('Choose if the file tag should be shown after the file name') - .addToggle((toggle) => { - toggle - .setValue(this.yaml.disableFileTag || this.plugin.settings.defaultOverview.disableFileTag || false) - .onChange(async (value) => { - this.yaml.disableFileTag = value; - await this.updateYaml(); - }); - }); - } - - new Setting(contentEl) - .setName('File depth') - .setDesc('File & folder = +1 depth') - .addSlider((slider) => - slider - .setValue(this.yaml?.depth || 2) - .setLimits(1, 10, 1) - .onChange(async (value) => { - this.yaml.depth = value; - await this.updateYaml(); - }) - ); - - new Setting(contentEl) - .setName('Overview style') - .setDesc('Choose the style of the overview (grid style soon)') - .addDropdown((dropdown) => - dropdown - .addOption('list', 'List') - .setValue(this.yaml?.style || 'list') - .onChange(async (value: 'list') => { - this.yaml.style = value; - await this.updateYaml(); - }) - ); - - new Setting(contentEl) - .setName('Sort files by') - .setDesc('Choose how the files should be sorted') - .addDropdown((dropdown) => - dropdown - .addOption('name', 'Name descending') - .addOption('created', 'Created descending') - .addOption('modified', 'Modified descending') - .addOption('nameAsc', 'Name ascending') - .addOption('createdAsc', 'Created ascending') - .addOption('modifiedAsc', 'Modified ascending') - .setValue(this.yaml?.sortBy || 'name') - .onChange(async (value: 'name' | 'created' | 'modified' | 'nameAsc' | 'createdAsc' | 'modifiedAsc') => { - this.yaml.sortBy = value; - await this.updateYaml(); - }) - ); - - new Setting(contentEl) - .setName('Show folder names of empty folders') - .setDesc('That includes subfolders of the current folder when you are on file depth 1') - .addToggle((toggle) => { - toggle - .setValue(this.yaml.showEmptyFolders || this.plugin.settings.defaultOverview.showEmptyFolders || false) - .onChange(async (value) => { - this.yaml.showEmptyFolders = value; - this.yaml.onlyIncludeSubfolders = false; - this.display(); - await this.updateYaml(); - }); - }); - - if (this.yaml.showEmptyFolders) { - new Setting(contentEl) - .setName('Only show first empty subfolders of current folder') - .addToggle((toggle) => { - toggle - .setValue(this.yaml.onlyIncludeSubfolders || this.plugin.settings.defaultOverview.onlyIncludeSubfolders || false) - .onChange(async (value) => { - this.yaml.onlyIncludeSubfolders = value; - await this.updateYaml(); - }); - }); - } - - - } - onClose() { - const { contentEl } = this; - contentEl.empty(); - } - async updateYaml() { - const file = this.plugin.app.vault.getAbstractFileByPath(this.ctx.sourcePath); - if (!(file instanceof TFile)) return; - let stringYaml = stringifyYaml(this.yaml); - await this.plugin.app.vault.process(file, (text) => { - const info = this.ctx.getSectionInfo(this.el); - // check if stringYaml ends with a newline - if (stringYaml[stringYaml.length - 1] !== '\n') { - stringYaml += '\n'; - } - if (info) { - const { lineStart } = info; - const lineEnd = this.getCodeBlockEndLine(text, lineStart); - if (lineEnd === -1 || !lineEnd) return text; - const lineLength = lineEnd - lineStart; - const lines = text.split('\n'); - lines.splice(lineStart, lineLength + 1, `\`\`\`folder-overview\n${stringYaml}\`\`\``); - return lines.join('\n'); - } - return `\`\`\`folder-overview\n${stringYaml}\`\`\``; - }); - } - getCodeBlockEndLine(text: string, startLine: number, count = 1) { - let line = startLine + 1; - const lines = text.split('\n'); - while (line < lines.length) { - if (count > 20) { return -1; } - if (lines[line].startsWith('```')) { - return line; - } - line++; - count++; - } - return line; - } -} - diff --git a/src/settings.ts b/src/settings.ts index c788993..77e0d40 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -5,7 +5,8 @@ import { extractFolderName, getFolderNote } from './functions/folderNoteFunction import { addExcludeFolderListItem, ExcludedFolder, addExcludedFolder, ExcludePattern, addExcludePatternListItem } from './excludedFolder'; import { FrontMatterTitlePluginHandler } from './events/frontMatterTitle'; import ConfirmationModal from "./modals/confirmCreation"; -import { yamlSettings } from './folderOverview'; +import { yamlSettings } from './folderOverview/FolderOverview'; +import { FolderOverviewSettings } from './folderOverview/modalSettings'; export interface FolderNotesSettings { syncFolderName: boolean; ctrlKey: boolean; @@ -61,12 +62,18 @@ export const DEFAULT_SETTINGS: FolderNotesSettings = { syncDelete: false, showRenameConfirmation: true, defaultOverview: { + folderPath: '', title: '{{folderName}} overview', disableTitle: false, depth: 3, includeTypes: ['folder', 'markdown'], style: 'list', disableFileTag: false, + sortBy: 'name', + sortByAsc: true, + showEmptyFolders: false, + onlyIncludeSubfolders: false, + storeFolderCondition: true, }, useSubmenus: true, syncMove: true, @@ -127,6 +134,18 @@ export class SettingsTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName('Manage folder overview defaults') + .setDesc('Manage the default settings for the folder overview plugin') + .addButton((button) => + button + .setButtonText('Manage') + .setCta() + .onClick(async () => { + new FolderOverviewSettings(this.plugin.app, this.plugin, this.plugin.settings.defaultOverview, null, null, true).open(); + }) + ); + const setting = new Setting(containerEl); const desc = document.createDocumentFragment(); desc.append( @@ -210,11 +229,9 @@ export class SettingsTab extends PluginSettingTab { .onChange(async (value) => { this.plugin.settings.syncMove = value; await this.plugin.saveSettings(); - } - ) + }) ); } - const disableSetting = new Setting(containerEl); disableSetting.setName('Disable folder collapsing'); disableSetting.setDesc('Disable the ability to collapse folders by clicking exactly on the folder name'); @@ -449,16 +466,16 @@ export class SettingsTab extends PluginSettingTab { new Setting(containerEl) - .setName('Create folder note for every folder') - .setDesc('Create a folder note for every folder in the vault') - .addButton((cb) => { - cb.setIcon('plus'); - cb.setTooltip('Create folder notes'); - cb.onClick(async () => { - - new ConfirmationModal(this.app, this.plugin).open(); + .setName('Create folder note for every folder') + .setDesc('Create a folder note for every folder in the vault') + .addButton((cb) => { + cb.setIcon('plus'); + cb.setTooltip('Create folder notes'); + cb.onClick(async () => { + + new ConfirmationModal(this.app, this.plugin).open(); + }); }); - }); const manageExcluded = new Setting(containerEl) diff --git a/src/suggesters/FolderSuggester.ts b/src/suggesters/FolderSuggester.ts index e0c3b6d..cf62d1e 100644 --- a/src/suggesters/FolderSuggester.ts +++ b/src/suggesters/FolderSuggester.ts @@ -11,7 +11,8 @@ export enum FileSuggestMode { export class FolderSuggest extends TextInputSuggest { constructor( public inputEl: HTMLInputElement, - private plugin: FolderNotesPlugin + private plugin: FolderNotesPlugin, + public folder?: TFolder, ) { super(inputEl); } @@ -29,7 +30,13 @@ export class FolderSuggest extends TextInputSuggest { getSuggestions(input_str: string): TFolder[] { const folders: TFolder[] = []; const lower_input_str = input_str.toLowerCase(); - this.plugin.app.vault.getAllLoadedFiles().forEach((folder: TAbstractFile) => { + let files: TAbstractFile[] = []; + if (this.folder) { + files = this.folder.children; + } else { + files = this.plugin.app.vault.getAllLoadedFiles(); + } + files.forEach((folder: TAbstractFile) => { if ( folder instanceof TFolder && folder.path.toLowerCase().contains(lower_input_str) &&