File explorer clone progress

This commit is contained in:
Lost Paul 2023-08-10 21:54:22 +02:00
parent 5e066e8f17
commit 2d34383cd0
10 changed files with 635 additions and 827 deletions

View file

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

View file

@ -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 = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="svg-icon right-triangle"><path d="M3 8L12 17L21 8"></path></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;
}

View file

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

View file

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

View file

@ -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() {

View file

@ -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),

View file

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

View file

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

View file

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

View file

@ -11,7 +11,8 @@ export enum FileSuggestMode {
export class FolderSuggest extends TextInputSuggest<TFolder> {
constructor(
public inputEl: HTMLInputElement,
private plugin: FolderNotesPlugin
private plugin: FolderNotesPlugin,
public folder?: TFolder,
) {
super(inputEl);
}
@ -29,7 +30,13 @@ export class FolderSuggest extends TextInputSuggest<TFolder> {
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) &&