mirror of
https://github.com/caffa/Obsidian-Current-Folder-Note-Display-Plugin.git
synced 2026-07-22 09:50:27 +00:00
432 lines
14 KiB
TypeScript
432 lines
14 KiB
TypeScript
import { App, Notice, Plugin, PluginSettingTab, Setting, TFile } from 'obsidian';
|
|
import { ItemView, WorkspaceLeaf } from "obsidian";
|
|
import { MarkdownView } from "obsidian";
|
|
|
|
interface CurrentFolderNotesDisplaySettings {
|
|
excludeTitlesFilter: string;
|
|
includeTitleFilter: string;
|
|
prettyTitleCase: boolean;
|
|
includeSubfolderNotes: boolean;
|
|
includeCurrentFileOutline: boolean;
|
|
}
|
|
|
|
const DEFAULT_SETTINGS: Partial<CurrentFolderNotesDisplaySettings> = {
|
|
excludeTitlesFilter: '_index',
|
|
includeTitleFilter: '',
|
|
prettyTitleCase: true,
|
|
includeSubfolderNotes: false,
|
|
includeCurrentFileOutline: true,
|
|
}
|
|
|
|
export default class CurrentFolderNotesDisplay extends Plugin {
|
|
settings: CurrentFolderNotesDisplaySettings;
|
|
|
|
fileChangeHandler(file: TFile) {
|
|
if (file instanceof TFile && file.path === this.file.path) {
|
|
this.load();
|
|
}
|
|
}
|
|
|
|
async onload() {
|
|
await this.loadSettings();
|
|
|
|
// This adds a settings tab so the user can configure various aspects of the plugin
|
|
this.addSettingTab(new CurrentFolderNotesDisplaySettingTab(this.app, this));
|
|
|
|
|
|
// add a panel to the right sidebar - view
|
|
// this.registerView(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY, (leaf) => new CurrentFolderNotesDisplayView(leaf));
|
|
this.registerView(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY, (leaf) => new CurrentFolderNotesDisplayView(leaf, this));
|
|
// Add a ribbon icon
|
|
this.addRibbonIcon('folder', 'Activate Folder Notes Display', () => {
|
|
// new Notice('This is a notice!');
|
|
this.activateView();
|
|
});
|
|
|
|
// Add a command to open the view
|
|
this.addCommand({
|
|
id: 'activate-folder-notes-display',
|
|
name: 'Open Pane',
|
|
callback: () => {
|
|
this.activateView();
|
|
}
|
|
});
|
|
|
|
// when file is changes (opened) in the editor, update the view
|
|
this.registerEvent(this.app.workspace.on('file-open', async (file) => {
|
|
// let views = this.app.workspace.getLeavesOfType(CurrentFolderNotesDisplayView);
|
|
// let view = this.app.workspace.getActiveViewOfType(CurrentFolderNotesDisplayView);
|
|
this.refreshView();
|
|
|
|
}));
|
|
|
|
// when a file is saved, update the view
|
|
this.registerEvent(this.app.vault.on('modify', async (file) => {
|
|
this.refreshView();
|
|
}));
|
|
|
|
// when a file is deleted, update the view
|
|
this.registerEvent(this.app.vault.on('delete', async (file) => {
|
|
this.refreshView();
|
|
}));
|
|
|
|
// when a file is created, update the view
|
|
this.registerEvent(this.app.vault.on('create', async (file) => {
|
|
this.refreshView();
|
|
}));
|
|
|
|
// when a file is renamed, update the view
|
|
this.registerEvent(this.app.vault.on('rename', async (file) => {
|
|
this.refreshView();
|
|
}));
|
|
|
|
}
|
|
|
|
onunload() {
|
|
|
|
}
|
|
|
|
async activateView() {
|
|
const { workspace } = this.app;
|
|
let leaf: WorkspaceLeaf | null = null;
|
|
const leaves = workspace.getLeavesOfType(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY);
|
|
|
|
if (leaves.length) {
|
|
// A leaf with our view already exists, use that
|
|
leaf = leaves[0];
|
|
} else {
|
|
// Our view could not be found in the workspace, create a new leaf
|
|
// in the right sidebar for it
|
|
leaf = workspace.getRightLeaf(false);
|
|
if (leaf) {
|
|
await leaf.setViewState({ type: VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY, active: true });
|
|
}
|
|
}
|
|
|
|
if (!leaf) {
|
|
new Notice('Could not create a new leaf for the view');
|
|
return;
|
|
}
|
|
workspace.revealLeaf(leaf);
|
|
}
|
|
|
|
async refreshView() {
|
|
const { workspace } = this.app;
|
|
const leaves = workspace.getLeavesOfType(VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY);
|
|
if (leaves.length) {
|
|
// A leaf with our view already exists, use that
|
|
const view = leaves[0].view as CurrentFolderNotesDisplayView;
|
|
await view.displayNotesInCurrentFolder();
|
|
} else {
|
|
new Notice('Could not find the view');
|
|
this.activateView();
|
|
}
|
|
|
|
// // if this view is not open do nothing
|
|
// let view = this.app.workspace.getActiveViewOfType(CurrentFolderNotesDisplayView);
|
|
// if (view) {
|
|
// await view.displayNotesInCurrentFolder();
|
|
// }
|
|
|
|
|
|
|
|
}
|
|
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
}
|
|
|
|
|
|
export const VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY = "current-folder-notes-view";
|
|
|
|
export class CurrentFolderNotesDisplayView extends ItemView {
|
|
plugin: CurrentFolderNotesDisplay;
|
|
|
|
constructor(leaf: WorkspaceLeaf, plugin: CurrentFolderNotesDisplay) {
|
|
super(leaf);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
getViewType() {
|
|
return VIEW_TYPE_CURRENT_FOLDER_NOTES_DISPLAY;
|
|
}
|
|
|
|
getDisplayText() {
|
|
return "CurrentFolderNotesDisplay view";
|
|
}
|
|
|
|
async onOpen() {
|
|
|
|
await this.displayNotesInCurrentFolder();
|
|
}
|
|
|
|
async onClose() {
|
|
// close current view
|
|
|
|
}
|
|
|
|
// main.ts
|
|
|
|
// Function to create clickable headings
|
|
createClickableHeadings(container: HTMLElement, currentFileContent: string, currentFilePath: string): void {
|
|
const headings: RegExpMatchArray | null = currentFileContent.match(/^(#+)\s+(.*)$/gm);
|
|
if (headings) {
|
|
headings.forEach((heading: string) => {
|
|
const headingLevelMatch: RegExpMatchArray | null = heading.match(/^(#+)/);
|
|
if (headingLevelMatch) {
|
|
const headingLevel: number = headingLevelMatch[0].length;
|
|
const headingText: string = heading.replace(/^(#+)\s+/, '');
|
|
const p: HTMLElement = container.createEl('p', { text: headingText });
|
|
p.style.marginLeft = `${headingLevel * 10}px`;
|
|
p.addEventListener('click', async () => {
|
|
this.app.workspace.openLinkText('#' + headingText, currentFilePath, false);
|
|
// do an escape to deselect the text
|
|
// const selection = window.getSelection();
|
|
// if (selection) {
|
|
// selection.removeAllRanges();
|
|
// }
|
|
});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async displayNotesInCurrentFolder(): Promise<void> {
|
|
const container = this.containerEl.children[1];
|
|
container.empty();
|
|
|
|
// container.createEl("h5", { text: "Current Folder Notes" });
|
|
// smaller title
|
|
container.createEl("h6", { text: "Current Folder Notes" });
|
|
|
|
// Get the current file's path
|
|
const activeFile = this.app.workspace.getActiveFile();
|
|
const currentFilePath = activeFile ? activeFile.path : '';
|
|
|
|
// Extract the parent folder path
|
|
const parentFolderPath = currentFilePath.substring(0, currentFilePath.lastIndexOf('/'));
|
|
|
|
// Get all markdown files in the vault
|
|
const allMarkdownFiles = this.app.vault.getMarkdownFiles();
|
|
|
|
// Filter the files to only include those in the parent folder
|
|
let parentFolderFiles = allMarkdownFiles.filter(file => file.path.startsWith(parentFolderPath));
|
|
|
|
const includesFilter = this.plugin.settings.includeTitleFilter;
|
|
if (includesFilter && includesFilter.length > 0) {
|
|
// convert this text to a list of words seperated by commas
|
|
let possibleFilteredFiles = parentFolderFiles;
|
|
if (includesFilter.includes(',') || includesFilter.includes(' ')) {
|
|
let includeWords = includesFilter.split(',');
|
|
// remove spaces from the words
|
|
includeWords.forEach((word, index) => {
|
|
includeWords[index] = word.trim();
|
|
});
|
|
/* The line `console.log(includeWords);` is logging the array `includeWords` to the console. This
|
|
is helpful for debugging and understanding the content of the array at that point in the code
|
|
execution. It allows you to see the individual words extracted from the `includesFilter` string
|
|
and trimmed of any extra spaces. */
|
|
// console.log(includeWords);
|
|
// filter out notes that do not include any of the words in the filter and do it in a case insensitive way
|
|
possibleFilteredFiles = parentFolderFiles.filter(file => includeWords.some(word => file.basename.toLowerCase().includes(word.toLowerCase())));
|
|
// const possibleFilteredFiles = parentFolderFiles.filter(file => file.basename.includes(includesFilter));;
|
|
} else {
|
|
// filter out notes that do not include the words in the filter and do it in a case insensitive way
|
|
possibleFilteredFiles = parentFolderFiles.filter(file => file.basename.toLowerCase().includes(includesFilter.toLowerCase()));
|
|
}
|
|
// filter out notes that do not include the words in the filter
|
|
// const possibleFilteredFiles = parentFolderFiles.filter(file => file.basename.includes(includesFilter));
|
|
if (possibleFilteredFiles.length == 0) {
|
|
container.createEl('p', { text: `No notes found in the current folder that include "${includesFilter}"` });
|
|
return;
|
|
}
|
|
parentFolderFiles = possibleFilteredFiles;
|
|
}
|
|
|
|
|
|
let parentFolderFilesNoSubfolders = parentFolderFiles;
|
|
if (!this.plugin.settings.includeSubfolderNotes) {
|
|
// Exclude notes in subfolders from the list
|
|
parentFolderFilesNoSubfolders = parentFolderFiles.filter(file => !file.path.substring(parentFolderPath.length + 1).includes('/'));
|
|
}
|
|
|
|
// Get the exclude filter
|
|
const excludeFilter = this.plugin.settings.excludeTitlesFilter;
|
|
|
|
let filteredFiles = parentFolderFilesNoSubfolders;
|
|
let possibleFilteredFiles = parentFolderFilesNoSubfolders;
|
|
if (excludeFilter.length > 0) {
|
|
// let the exclude filter work with a list of words seperated by commas
|
|
if (excludeFilter.includes(',')) {
|
|
let excludeWords = excludeFilter.split(',');
|
|
// remove spaces from the words
|
|
excludeWords.forEach((word, index) => {
|
|
excludeWords[index] = word.trim();
|
|
});
|
|
// filter out notes that do not include any of the words in the filter and do it in a case insensitive way
|
|
possibleFilteredFiles = parentFolderFilesNoSubfolders.filter(file => !excludeWords.some(word => file.basename.toLowerCase().includes(word.toLowerCase())));
|
|
} else {
|
|
|
|
// Filter out notes that match the exclude filter
|
|
possibleFilteredFiles = parentFolderFilesNoSubfolders.filter(file => !file.basename.includes(excludeFilter));
|
|
|
|
}
|
|
if (possibleFilteredFiles.length == 0) {
|
|
container.createEl('p', { text: `No notes found in the current folder that do not include "${excludeFilter}"` });
|
|
return;
|
|
}
|
|
filteredFiles = possibleFilteredFiles;
|
|
}
|
|
|
|
// If there are no notes in the folder, display a message
|
|
if (filteredFiles.length === 0) {
|
|
container.createEl('p', { text: 'No notes in this folder' });
|
|
return;
|
|
}
|
|
|
|
// Sort the files by name
|
|
filteredFiles.sort((a, b) => a.basename.localeCompare(b.basename));
|
|
|
|
// check for headings
|
|
let MyHeadings = false;
|
|
let currentFileContent = '';
|
|
if (this.plugin.settings.includeCurrentFileOutline) {
|
|
const currentFile = this.app.workspace.getActiveFile();
|
|
if (currentFile) {
|
|
currentFileContent = await this.app.vault.read(currentFile);
|
|
MyHeadings = true;
|
|
}
|
|
}
|
|
|
|
// Iterate over the files and add their names to the container
|
|
filteredFiles.forEach(file => {
|
|
const p = container.createEl('p');
|
|
const a = p.createEl('a', { text: file.basename });
|
|
if (this.plugin.settings.prettyTitleCase) {
|
|
a.innerText = a.innerText.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
|
|
}
|
|
// make the items similar to file explorer
|
|
a.className = 'folder-notes-style';
|
|
// If the file path matches the current file path, assign the 'current-file' class
|
|
if (file.path === currentFilePath) {
|
|
a.className += ' current-file';
|
|
a.innerText = '> ' + a.innerText;
|
|
|
|
if (MyHeadings) {
|
|
this.createClickableHeadings(container as HTMLElement, currentFileContent, currentFilePath);
|
|
}
|
|
|
|
}
|
|
|
|
// make pretty when hover
|
|
a.onmouseover = () => {
|
|
a.classList.add('hover-style');
|
|
}
|
|
// and remove when not hovering
|
|
a.onmouseout = () => {
|
|
a.classList.remove('hover-style');
|
|
}
|
|
|
|
// When a note is clicked, open it in the workspace
|
|
a.addEventListener('click', () => {
|
|
this.app.workspace.openLinkText(file.basename, parentFolderPath);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
class CurrentFolderNotesDisplaySettingTab extends PluginSettingTab {
|
|
plugin: CurrentFolderNotesDisplay;
|
|
|
|
constructor(app: App, plugin: CurrentFolderNotesDisplay) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
|
|
display(): void {
|
|
const {containerEl} = this;
|
|
|
|
containerEl.empty();
|
|
|
|
// heading for filters
|
|
containerEl.createEl("h2", { text: "Title filters" });
|
|
|
|
// button to reset the settings
|
|
// new Setting(containerEl)
|
|
// .setName('Reset settings')
|
|
// .setDesc('Reset the settings to their default values')
|
|
// .addButton(button => button
|
|
// .setButtonText('Reset')
|
|
// .onClick(async () => {
|
|
// this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS, { excludeTitlesFilter: '' });
|
|
// await this.plugin.saveSettings();
|
|
// this.display();
|
|
// }));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Exclude titles filter')
|
|
.setDesc('What notes to exclude from the view. This can be a list of words seperated by commas.')
|
|
.addText(text => text
|
|
.setPlaceholder('_Index')
|
|
.setValue(this.plugin.settings.excludeTitlesFilter)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.excludeTitlesFilter = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
new Setting(containerEl)
|
|
.setName('Includes titles filter')
|
|
.setDesc('Only include notes with this in their title. This can be a list of words seperated by commas.')
|
|
.addText(text => text
|
|
.setPlaceholder('Chapter')
|
|
.setValue(this.plugin.settings.includeTitleFilter)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.includeTitleFilter = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
// heading for options
|
|
containerEl.createEl("h2", { text: "Options" });
|
|
|
|
// option to do a pretty title case for the notes
|
|
new Setting(containerEl)
|
|
.setName('Pretty title case')
|
|
.setDesc('Convert the note titles to Title Case')
|
|
// option to reset this setting
|
|
.addToggle(toggle => toggle
|
|
.setValue(this.plugin.settings.prettyTitleCase)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.prettyTitleCase = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
// option to include subfolder notes
|
|
new Setting(containerEl)
|
|
.setName('Include subfolder notes')
|
|
.setDesc('Include notes in subfolders of the current folder')
|
|
.addToggle(toggle => toggle
|
|
.setValue(this.plugin.settings.includeSubfolderNotes)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.includeSubfolderNotes = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
// option to include outline of current file
|
|
new Setting(containerEl)
|
|
.setName('Include outline of current file')
|
|
.setDesc('Include the outline of the current file in the view')
|
|
.addToggle(toggle => toggle
|
|
.setValue(this.plugin.settings.includeCurrentFileOutline)
|
|
.onChange(async (value) => {
|
|
this.plugin.settings.includeCurrentFileOutline = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
|
|
|
|
}
|
|
}
|